firebase

Jasmine Unit Tests in an RxJs + Firebase app: Part 2

For today’s post, we’ll cover some useful test execution shortcuts and then dive into how to test code that uses AngularFire’s database.list.push() method to write data into the database.

Jasmine Test Execution Shortcuts

Part 1 covered describe and it, functions that are part of Jasmine that provide BDD-style tests. We looked at the following simple test:


describe("The Alert Helper", () => {

   // DI, setup, and beforeEach code elided

  it("should create an error alert with a provided message", () => {
    spyOn(AlertCtrlStub.prototype, "create").and.callThrough();

    alertHelper.triggerErrorAlert("This is a message!");

    expect(alertHelper.prototype.create).toHaveBeenCalledWith({
      message: "This is a message!",
      buttons: [
        {
          text: "Ok",
          role: "cancel",
        },
      ],
    });
  });

As you can see describe can be used to mark a suite of tests and each individual test can live in an it.

Now consider what happens when you build up enough tests in your application that it takes awhile for your entire test suite to run. What happens is you start to have a really long development cycle while implementing new features. Especially if, like me, you prefer a TDD-style way of working. In our application, I was finding it painful and tedious to write a test, run npm test from the command line and wait for all tests ever written so far to run just so I could see the test I just wrote fail, then implement the code that I thought would make the test pass, then run npm test again and run all tests ever written, find out that the code I wrote didn’t pass the test, go back to the code, etc., etc.

Phew! Just thinking about that makes me tired. More coffee, be back in a second.

f and x
OK, I’m back. So strangely enough, Jasmine has a couple of tricks up its sleeve that are documented, but are easy to miss. Namely, that you can add “f” or “x” to the front of a describe or an it to change which tests are run.

e.g.:

  • fit and fdescribe
  • xit and xdescribe

Adding an “f” means making an it or a describe “focused.” This is perfect for my aforementioned use case of doing TDD-style development. What “focused” means is that only that single test (if it’s marked fit) or single suite of tests (if it’s marked fdescribe) will execute during the next test run. There is more information in the Jasmine docs.

Just remember to use this only during development and not to commit a fit or fdescribe since you want all your tests running, obviously!

Adding an “x” means making an it or a describe “excluded.” Actually, the Jasmine documentation calls this “pending” for some reason. But whatever. The point is that any single test (if it’s marked xit) or any suite of tests (if it’s marked xdescribe) will not execute during the next test run. Jasmine docs have the xit and xdescribe stuff as a section under the intro titled “Pending Specs”.

These shortcuts saved my life! I hope you find them handy. Now, on to the AngularFire testing stuff.

Testing Firebase writes (pushes or sets)

As I mentioned in part 1, by no means do I mean the examples below to be prescriptive or otherwise thought of as “the way to do things.” Probably there are better ways. But they have worked for me and my team. I encourage anyone to provide suggestions and feedback in the comments section.

Part 1 showed one way to test reading from Firebase using AngularFire’s database.object method. Writing to Firebase with AngularFire is even more nested, adding an additional level. It uses a call to database.list.push! Here is the example given in the AngularFire documentation:

const items = db.list('/items');
items.push({ name: newName });

(Note that you can also use set which overwrites, rather than adds to, a path in firebase.)

Testing it

We implemented a simple notification feature that lets users in our application know when some other user has interacted with their post or their user profile in a way they should know about. For example, if someone bids on their post, then they get a little toast notification that pops out of the top of their screen for five seconds telling them so.

To make this work, we first write the new notification to a path under their user profile. This is seen in the following method, which you can see is a very straightforward usage of the AngularFire push method:

public sendNotification(uid: string, notification: INotification): void {
    let notificationRef: FirebaseListObservable<any> = this.af.database.list(`/userProfile/${uid}/notification/`);
    notificationRef.push(notification);
  }

If you think that it is hard to test code that calls database.object, well now you see something with a third level: database.list.push!

Our test fixture for this

To make it possible to test whether or not database.list.push is called when the sendNotification function is called, we have to create some kind of stub that has “list” and “push” functions.

We can use nearly the same strategy as found in part 1, but a big difference here is that list is a function, whereas object is a property. Luckily, this actually makes things easier since anywhere that a function is used, we can use a Jasmine spy.

We’ll work backwards then, creating push, then list, and finally creating the stub object.

Push:

let pushSpy: Spy = jasmine.createSpy("push");

As you can see, we can just create the spy and ignore any returnValue definition since the push function itself is a void call that doesn’t return a value back.

List:

let listSpy: Spy = jasmine.createSpy("list").and.returnValue({
      push: pushSpy,
    });

Here we return an object with a “push” function. We define push as the spy we created for it.

The stub of AngularFire:

let angularFireStub: any = {
      database: {
        list: listSpy,
      },
  };

I hope this is easy to follow. Here in the stub we have defined database as an object that defines “list” as the listSpy we created. In turn, we defined listSpy as having the push function which is actually pointing at the pushSpy. Phew! Visually, this looks like this:

download

The code for it, when put all together, looks like this:

  let pushSpy: Spy = jasmine.createSpy("push");

  let listSpy: Spy = jasmine.createSpy("list").and.returnValue({
      push: pushSpy,
    });

  let angularFireStub: any = {
      database: {
        list: listSpy,
      },
  };

Since these are defined within the describe block they can be used for all tests in the suite, so just like in part 1 you’ll want to use TestBed and useValue to DI this stub into the subject under test:

beforeEach(() => {
    TestBed.configureTestingModule({
      providers: [
        { provide: AuthData, useClass: AuthMock },
        { provide: AngularFire, useValue: angularFireStub },
        NotificationData,
      ],
    });
    listSpy.calls.reset();
    pushSpy.calls.reset();
  });

Notice that there’s one gotcha. You will want to reset the spies between each call. To accomplish this, you can add a call to .calls.reset() in the beforeEach block. You can see this above in lines 9 and 10. If you don’t do this, the spies will record the number of calls to list and push across tests, causing funky results for any toHaveBeenCalledTimes expectations you make.

With that setup, an example test might look like this:

it("should create a notification with the provided values", () => {
    let notification: INotification = // elided
    notificationData.sendNotification("123", notification);
    expect(listSpy).toHaveBeenCalledWith(`/userProfile/123/notification/`);
    expect(listSpy).toHaveBeenCalledTimes(1);
    expect(pushSpy).toHaveBeenCalledWith(notification);
    expect(pushSpy).toHaveBeenCalledTimes(1);
  });

This is a good example test. We first make sure that the code is pointing at the right path (line 4) by expecting that the correct path was passed to the list function. Next we make sure we only call that one time (line 5). Third, we expect that the notification object we passed was passed along to the push function (line 6). Finally, we expect that push is only called once (line 7).

If you have code that uses set instead of push, you can literally find/replace in this code with the word set.

Conclusion

I hope this helps. Let me know in the comments if there’s anything I missed or that can be better. I don’t know if there will be a part 3 to this series but if anyone is interested in that, let me know what exactly it should cover.

Leave a Reply

Your email address will not be published. Required fields are marked *