Как смоделировать тесты Firebase в Карме для приложения Angular

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

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

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

1 ответ

Скопировано из этой сущности, в которой подробно обсуждается эта тема.

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

Что не так с издевательствами

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

/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 userList = 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.on('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
Другие вопросы по тегам