如何在Angular应用的Karma测试中模拟Firebase [英] How to mock out Firebase in Karma tests for Angular app
问题描述
通过遵循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/library/angular/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屋!