2014-12-08 2 views
3

Следуя руководству AngularFire, я синхронизировал переменную scope с массивом Firebase. Мой код в основном так же, как учебник (Шаг 5):Как издеваться над Firebase в тестах Karma для Angular app

https://www.firebase.com/docs/web/libraries/angular/quickstart.html

Все в мое приложение работает, но я действительно путают о том, как правильно дразнить Firebase вызов в моих модульных тестов Кармы. Я предполагаю, что что-то вроде использования $ обеспечивает издевательство над данными? Но тогда $ add не будет работать в моих методах контроллера. Помогите?

+1

Попробуйте использовать MockFirebase. https://github.com/katowulf/mockfirebase –

ответ

0

Код статьи: this gist который описывается подробно.

Обсуждение все включено в комментарии. Здесь многое предстоит рассмотреть.

Что не так с издевается

Рассмотрим эту структуру данных, используемый для получения списка имен на основе членства

/users/<user id>/name 
/rooms/members/<user id>/true 

Теперь давайте создадим пару простых классов без какого-либо реального рассмотрения для тестирования структуры, предполагая, что мы будем использовать макет Firebase для их проверки. Заметьте, что я вижу подобные ошибки постоянно в дикой природе; этот пример не далеко за ушами или преувеличенный (это довольно приручить в сравнении)

class User { 
    // Accepts a Firebase snapshot to create the user instance 
    constructor(snapshot) { 
    this.id = snapshot.key; 
    this.name = snapshot.val().name; 
    } 

    getName() { return this.name; } 

    // Load a user based on their id 
    static load(id) { 
    return firebase.database().ref('users').child(uid) 
     .once('value').then(snap => new User(snap)); 
    } 
} 

class MembersList { 
    // construct a list of members from a Firebase ref 
    constructor(memberListRef) { 
    this.users = []; 

    // This coupling to the Firebase SDK and the nuances of how realtime is handled will 
    // make our tests pretty difficult later. 
    this.ref = memberListRef; 
    this.ref.on('child_added', this._addUser, this); 
    } 

    // Get a list of the member names 
    // Assume we need this for UI methods that accept an array, so it can't be async 
    // 
    // It may not be obvious that we've introduced an odd coupling here, since we 
    // need to know that this is loaded asynchronously before we can use it. 
    getNames() { 
    return this.users.map(user => user.getName()); 
    } 

    // So this kind of stuff shows up incessantly when we couple Firebase into classes and 
    // it has a big impact on unit testing (shown below) 
    ready() { 
     // note that we can't just use this.ref.once() here, because we have to wait for all the 
     // individual user objects to be loaded, adding another coupling on the User class's internal design :(
     return Promise.all(this.promises); 
    } 

    // Asynchronously find the user based on the uid 
    _addUser(memberSnap) { 
     let promise = User.load(memberSnap.key).then(user => this.users.push(user)); 
     // note that this weird coupling is needed so that we can know when the list of users is available 
     this.promises.push(promise); 
    } 

    destroy() { 
     this.ref.off('child_added', this._addUser, this); 
    } 
} 

/***** 
Okay, now on to the actual unit test for list names. 
****/ 

const mockRef = mockFirebase.database(/** some sort of mock data */).ref(); 

// Note how much coupling and code (i.e. bugs and timing issues we might introduce) is needed 
// to make this work, even with a mock 
function testGetNames() { 
    const memberList = new MemberList(mockRef); 

    // We need to abstract the correct list of names from the DB, so we need to reconstruct 
    // the internal queries used by the MemberList and User classes (more opportunities for bugs 
    // and def. not keeping our unit tests simple) 
    // 
    // One important note here is that our test unit has introduced a side effect. It has actually cached the data 
    // locally from Firebase (assuming our mock works like the real thing; if it doesn't we have other problems) 
    // and may inadvertently change the timing of async/sync events and therefore the results of the test! 
    mockRef.child('users').once('value').then(userListSnap => { 
    const userNamesExpected = []; 
    userListSnap.forEach(userSnap => userNamesExpected.push(userSnap.val().name)); 

    // Okay, so now we're ready to test our method for getting a list of names 
    // Note how we can't just test the getNames() method, we also have to rely on .ready() 
    // here, breaking our effective isolation of a single point of logic. 
    memberList.ready().then(() => assertEqual(memberList.getNames(), userNamesExpected)); 

    // Another really important note here: We need to call .off() on the Firebase 
    // listeners, or other test units will fail in weird ways, since callbacks here will continue 
    // to get invoked when the underlying data changes. 
    // 
    // But we'll likely introduce an unexpected bug here. If assertEqual() throws, which many testing 
    // libs do, then we won't reach this code! Yet another strange, intermittent failure point in our tests 
    // that will take forever to isolate and fix. This happens frequently; I've been a victim of this bug. :(
    memberList.destroy(); // (or maybe something like mockFirebase.cleanUpConnections() 
    }); 
} 

Лучший подход посредством надлежащей инкапсуляции и TDD

Хорошо, теперь давайте изменить это, начиная с эффективной конструкцией модульного тестирования и посмотреть, можем ли мы проектировать наши классы с меньшим сцеплением для размещения.

function testGetNames() { 
    const users = new UserList(); 
    userList.add(new User('kato', 'Kato Richardson'); 
    userList.add(new User('chuck', 'Chuck Norris'); 
    assertEqual(userList.getNames(), ['Kato Richardson', 'Chuck Norris']); 
    // Ah, this is looking good! No complexities, no async madness. No chance of bugs in my unit test! 
} 

/** 
Note how our classes will be simpler and better designed just by using a good TDD 
*/ 

class User { 
    constructor(userId, name) { 
     this.id = userId; 
     this.name = name; 
    } 

    getName() { 
     return this.name; 
    } 
} 

class UserList { 
    constructor() { 
    this.users = []; 
    } 

    getNames() { 
    return this.users.map(user => user.getName()); 
    } 

    addUser(user) { 
    this.users.push(user); 
    } 
    // note how we don't need .destroy() and .ready() methods here just to know when the user list is resolved, yay! 
} 

// This all looks great and wonderful, and the tests are going to run wonderfully. But how do we populate 
// the list from Firebase now?? The answer is an isolated service that handles this. 

class MemberListManager { 
    constructor(memberListRef) { 
    this.ref = memberListRef; 
    this.ref.off('child_added', this._addUser, this); 
    this.userList = new UserList(); 
    } 

    getUserList() { 
     return this.userList; 
    } 

    _addUser(snap) { 
    const user = new User(snap.key, snap.val().name); 
    this.userList.push(user); 
    } 

    destroy() { 
     this.ref.off('child_added', this._addUser, this); 
    } 
} 

// But now we need to test MemberListManager, too, right? And wouldn't a mock help here? Possibly. Yes and no. 
// 
// More importantly, it's just one small service that deals with the async and external libs. 
// We don't have to depend on mocking Firebase to do this either. Mocking the parts used in isolation 
// is much, much simpler than trying to deal with coupled third party dependencies across classes. 
// 
// Additionally, it's often better to move third party calls like these 
// into end-to-end tests instead of unit tests (since we are actually testing 
// across third party libs and not just isolated logic) 
// 
// For more alternatives to a mock sdk, check out AngularFire. 
// We often just used the real Firebase Database with set() or push(): 
// https://github.com/firebase/angularfire/blob/master/tests/unit/FirebaseObject.spec.js#L804 
// 
// An rely on spies to stub some fake snapshots or refs with results we want, which is much simpler than 
// trying to coax a mock or SDK to create error conditions or specific outputs: 
// https://github.com/firebase/angularfire/blob/master/tests/unit/FirebaseObject.spec.js#L344 
Смежные вопросы