如何在 Angular 应用的 Karma 测试中模拟 Firebase [英] How to mock out Firebase in Karma tests for Angular app

查看:22
本文介绍了如何在 Angular 应用的 Karma 测试中模拟 Firebase的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

按照 AngularFire 指南,我已将范围变量与 Firebase 数组同步.我的代码和教程(步骤5)基本一致:

By following the AngularFire guide, I have synchronized a scope variable with a Firebase array. My code is basically the same as the tutorial (Step 5):

https://www.firebase.com/docs/web/图书馆/角度/quickstart.html

我的应用程序中的一切正常,但我对如何在 Karma 单元测试中正确模拟 Firebase 调用感到非常困惑.我猜想使用 $provide 来模拟数据?但是 $add 在我的控制器方法中不起作用.帮助?

Everything in my app is working, but I am really confused about how to properly mock the Firebase call in my Karma unit tests. I guess something along the lines of using $provide to mock the data? But then $add wouldn't work in my controller methods. Help?

推荐答案

复制自本要点 详细讨论了这个话题.

Copied from this gist which discusses this topic at length.

讨论全部内联在评论中.这里有很多东西需要考虑.

The discussion is all inlined in the comments. There is a lot to consider here.

模拟有什么问题

考虑这个数据结构,用于根据成员资格获取姓名列表

Consider this data structure, used for getting a list of names based on membership

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

现在让我们创建几个简单的类,并没有真正考虑测试结构,假设我们将使用 Firebase 的模拟来测试它们.请注意,我经常在野外看到此类错误;这个例子并不牵强或夸张(相比之下,它相当温和)

Now let's create a couple simple classes without any real consideration for testing structures, assuming we'll use a mock of Firebase to test them. Note that I see these sorts of errors constantly in the wild; this example is not far fetched or exaggerated (it's rather tame in comparison)

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 获得更好的方法

好的,现在让我们从有效的测试单元设计开始,看看我们是否可以设计我们的类以减少耦合以适应.

Okay, now let's reverse this by starting with an effective test unit design and see if we can design our classes with less coupling to accommodate.

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

这篇关于如何在 Angular 应用的 Karma 测试中模拟 Firebase的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
相关文章
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆