firebase

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

Unit testing can be…well….different in an app built on libraries devoted to functional, asynchronous paradigms. AngularFire 2 may well be the epitome of this. There’s a lot to unpack. It’s TypeScript for one thing, not just plain JS. On top of that, add the RxJs library. Firebase‘s realtime database feature is both real-time and NoSql for another thing. It uses WebSocket so all the RxJs stuff really does sit around emitting events whenever data changes in Firebase. Phew! This does not make things plain and simple!

I’ve gathered some experience with it the past eight months in an Ionic mobile app, and here are the tips and tricks I have gathered, collected, stumbled upon, and cringed over. We used Jasmine as the test framework in my application, and these examples do the same.

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.

First things first: Setup!

The following snippet offers a good starting point for a basic set of unit tests.

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

  let alertHelper: AlertHelper;

  beforeEach(() => {
    TestBed.configureTestingModule({
      providers: [
        AlertHelper,
        { provide: AlertController, useClass: AlertCtrlMock },
      ],
    });
  });

  beforeEach(inject([AlertHelper], (helper: AlertHelper) => {
    alertHelper = helper;
  }));

Line 1 calls Jasmine’s describe function. The string you pass here in argument 1 is what you will see as the first line in any test output — the white line in this screenshot:
JasmineTestOutput

Of course the rest of this is a function or lambda that contains all your unit tests. Calls to beforeEach will predictably happen before each test. The TestBed object is a powerful part of Angular that we’ll cover more in depth as we go, or that you can read about on their site. All you need right now is the basic idea that it allows you some flexible ways to substitute test doubles for dependencies that are not under test in the current test suite.

The Basic Happy Path
After you have the basic setup out of the way, a basic happy path unit test for an Angular 2 service might look something like this.

  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",
        },
      ],
    });
  });

Line 1 is a call to Jasmine’s it function. In the aforementioned screenshot of test output, argument 1 to this call is the string that will show as the green lines (or red if they fail!).

Line 2 here shows the use of a Jasmine spy. Spies have great documentation on Jasmine’s web site, and are a powerful tool. In this example, we spy on a stub. The reason for providing a stub, but also spying on it, is two-fold:

  1. The stub makes sure no real code executes that we don’t want to execute.
  2. By spying on it we can see if it was called, and what values were passed to it when it was called.

We start the test by defining the spy, so that it is active when we later make the calls through the actual code. As the code executes, the spy will keep all the metadata about how many times it was called, and what arguments were passed to it, and so forth.

Finally in line 6 we use expect to make sure that our spy was called with a particular argument. This argument is an object that represents a basic error alert. This is because the thing we are testing is a simple utility function used throughout the app that takes a string and presents an alert with an OK button. If the buttons in this method were changed, or the code changed the string that was passed in, this test would start failing, indicating perhaps that a refactor went awry or something like that.

The Happy Path for an AngularFire 2 call
Any of you who have already been using Angular and Jasmine will be yawning at this point, so let’s move to something a little more spicy: the basics of testing interactions with Firebase. We’ll cover much more on this subject in part 2 of this series, but here is the happy path test for one of these interactions.

In our application users can view a “post” from another user. The code for this is quite basic AngularFire 2: a simple call to object:

public fetchPost(postID: string): Observable<IPost> {
  return this.af.database.object("/post/" + postID);
}

To set up a test suite for this, we use the following:

describe("The Post Data Service", () => {

  let postData: PostData;

  let objectSpy: Spy = jasmine.createSpy("object").and.callFake((path: string) => {
    if(path.includes("123")) {
      return Observable.of({
        title: "Example Post",
        body: "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nulla commodo dui quis.",
      });
    } else {
      return Observable.throw("Invalid path!");
    }
  });

  let afStub: any = {
    database: {
      object: objectSpy,
    },
  };

  beforeEach(() => {
    TestBed.configureTestingModule({
      providers: [
        { provide: AngularFire, useValue: afStub },
        PostData,
      ],
    });
  });

  beforeEach(inject([PostData], (postDataInjected: PostData) => {
    postData = postDataInjected;
  }));

Since the call in the code being tested is return this.af.database.object("/post/" + postID);, we have to stub out the af object and the database object (lines 16-20 above) like this:

let afStub: any = {
    database: {
      object: objectSpy,
    },
  };

There’s no need to use Jasmine for this, bare TypeScript is fine. Once we create a barebones stub for these, we point the “object” property of af.database at a Jasmine function spy we created named objectSpy. objectSpy was created in lines 5-14 and is just a fake which returns an Observable of a post if “123” is passed in and an empty Observable otherwise:

let objectSpy: Spy = jasmine.createSpy("object").and.callFake((path: string) => {
    if(path.includes("123")) {
      return Observable.of({
        title: "Example Post",
        body: "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nulla commodo dui quis.",
      });
    } else {
      return Observable.throw("Invalid path!");
    }
  });

The part that ties this all together is line 25, where we take advantage of TestBed‘s useValue and substitute our stub in place of the actual AngularFire provider. This is called supplying a ValueProvider.

        { provide: AngularFire, useValue: afStub },

Now we can proceed with a simple happy path test:

it("should return all data from a post when the specified ID post exists", () => {
    postData.fetchPost("123").subscribe((post: any) => {
      expect(objectSpy).toHaveBeenCalled();
      expect(post.title).toBe("Example Post");
      expect(post.body).toBe("Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nulla commodo dui quis.");
    });
  });

And a test for the “not found” case:

it("should throw an error when the specified post ID does not exist", () => {
    let value: Observable<IPost> = postData.fetchPost("456");
    value.subscribe((post: any) => {
      fail("This should never be reached");
    }, (error: any) => {
      expect(error).toBe("Invalid path!");
    });
  });

Objection!
Yes, I can hear your objection already. The above test is tautological. What does it actually test? Well, keeping in mind that this is all for the sake of example and teaching some test methods, my answer is that it tests the code. The function in question may rely on AngularFire 2 and Firebase, but the actual function really does have the following functionality:

a) return a post object if the post ID exists
b) return nothing if not

So these tests actually are important and do test this code. And we actually do want to decouple the code from its dependencies by using test doubles.

What’s in part 2?
For part 2 of this series, I’m open to feedback. I’ll probably post it next week or the week after. My plan is to move on to tests on af.database.list and then try to find any oddities and rough edges that we hit in our own code. But like I said, please comment below if there’s anything burning on your mind.

One thought on “Jasmine Unit Tests in an RxJs + Firebase app: Part 1

Leave a Reply

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