Unit Testing with Jasmine and Async/Await

Today, I wanted to write some unit tests against a Firebase database using Jasmine.

Connecting to Firebase from Node is just as easy as it is from a browser. I’ve done it before on a previous version of the Firebase SDK, so I didn’t expect a lot of problems there.

It was really just the butt-ugly tests I was worried about.

Testing asynchronous operations with Jasmine has always been supported; originally with the runs() and waitsFor() methods, which made for somewhat verbose tests, and later in 2.0 with done(), which is, IMHO, a clumsy promise + callback-to-move-on hack.

So, I decided to try a different approach. While async/await didn’t make it into ECMAScript 2016 (ES7), it is available in Chrome’s V8 as an experimental feature, and is supported in Node 7.6 without special flags. I’d been hearing a lot about it so I decided to give it a whirl. Spoiler alert: It’s totally frickin’ awesome!

Even a simple task can sometimes be made simpler

Making asynchronous code look synchronous is a real trick, regardless of what language you’re trying to do it with. Javascript Promises are a big help, but async / await comes closer than anything I’ve seen so far.

The basic premise looks like this:

async function fetchOrSupplyDefault(url) {
  let retval;
  try {
    retval = await fetch(url); 
  } catch(e) {
    retval = ["Default Data"];
  }
  return retval;
}

In the above function, we use the async keyword to indicate that the function will be performing one or more asynchronous operations. The fetch() function returns a promise, so we would normally chain a then() call, passing a function to be called when the promise resolves, making things much more unreadable. Instead, we can prepend the await keyword, and when the promise resolves, the waiting variable retval will be set.

One thing to note here is that return from an async function will be wrapped in Promise.resolve, and should be handled accordingly. Still, that’s already pretty slick. You couldn’t ask for more readable code inside this function.

Now let’s try that in a test

The nice thing about using async/await inside Jasmine is that a try/catch already surrounds each test, so that the framework can tally all the errors in the suite rather than crashing to a halt the first time it encounters one. That makes our code even simpler.

Note: Tip o’ the propeller beanie to Joseph Zimmerman for pointing out in the comments on DZone that my original database test was flawed. The upshot was that I needed to add the jasmine-await npm package. For additional proof I’ve written the fetched data snapshot’s uid property to stdout.

In the following test suite, we:

  1. Use the jasmine-await library. It extends the functions it()beforeEach()afterEach()beforeAll(), and afterAll() and wraps them in the async() function. So you can always use await to wait for a promise’s resolution or rejection.
  2. Use the firebase-admin node library to connect to a database using the serviceAccount.json file that can be downloaded from the Firebase console for the project. This happens in the call to beforeAll(), so that it is done once prior to running the tests. Straightforward stuff. Handy if you need to understand how to connect to Firebase, otherwise not much to linger on here.
  3. Test that the app was is initialized successfully. A basic, synchronous test.
  4. Test that async / await actually works with a simple example from the Mozilla docs.
  5. Test that a known profile can be downloaded. This is where the magic happens.
"use strict";
var async = require("jasmine-await");
var it = async.it;
var await = async.await;

describe("Test Firebase with async/await", () => {
    let admin = require("firebase-admin");
    let serviceAccount = require([__dirname, "/serviceAccountKey.json"].join('/'));
    let app = null;

    // Initialize the app with loaded credentials
    beforeAll( () => {
        app = admin.initializeApp({
            credential: admin.credential.cert(serviceAccount),
            databaseURL: "https://sinewav3-27d08.firebaseio.com"
        });
    });

    // Ensure we've got an initialized app
    it("receives an initialized app from firebase", () => expect(app).not.toBe(null) );

    // Prove that async/await works
    it('tests that async / await works', async () => {
        function resolveAfter2Seconds(x) {
            return new Promise(resolve => {
                setTimeout(() => {
                    resolve(x);
                }, 2000);
            });
        }

        async function add1(x) {
            var a = resolveAfter2Seconds(20);
            var b = resolveAfter2Seconds(30);
            return x + await a + await b;
        }

        var v = await add1(10);
        expect(v).toBe(60);
    });

    // Test reading a profile
    it("fetches a known profile from firebase", () => {
        const uid = "1sT1mpFabkVJcDvkTCTsqiLiTrF3";
        let profileRef = admin.database().ref('/profile/' + uid);
        let snapshot = profileRef.once('value');
        expect(snapshot).not.toBe(null);
        expect(snapshot.val().uid).toBe(uid);
        process.stdout.write(""+snapshot.val().uid); // Visual proof
    });

});

The output looks like this:

Destiny:SineWav3.Client.Shell cliff$ jasmine
Started
..1sT1mpFabkVJcXHtTCTsqiLiTrF3.


3 specs, 0 failures
Finished in 3.034 seconds

Whoa! That really worked? Lets Break it to be Sure.

It was almost too easy. In order to convince myself that async/await was having the intended effect and that the magic wasn’t wrapped up in the Firebase Admin SDK, I removed those keywords from the final test:

    // Test reading a profile
    it("fetches a known profile from firebase", () => {
        const uid = "1sT1mpFabkVJcDvkTCTsqiLiTrF3";
        let profileRef = admin.database().ref('/profile/' + uid);
        let snapshot = profileRef.once('value');
        expect(snapshot).not.toBe(null);
        expect(snapshot.val().uid).toBe(uid);
        process.stdout.write(""+snapshot.val().uid); // Visual proof
    });

Now the output looks like:

Destiny:SineWav3.Client.Shell cliff$ jasmine
Started
..F

Failures:
1) Test Firebase with async/await fetches a known profile from firebase
 Message:
   Failed: snapshot.val is not a function
 Stack:
   TypeError: snapshot.val is not a function
     at it (/Users/cliff/Documents/SineWav3.Client.Shell/spec/db-project-spec.js:49:25)
     at tryBlock (/Users/cliff/Documents/SineWav3.Client.Shell/node_modules/asyncawait/src/async/fiberManager.js:39:33)
     at runInFiber (/Users/cliff/Documents/SineWav3.Client.Shell/node_modules/asyncawait/src/async/fiberManager.js:26:9)

3 specs, 1 failure
Finished in 2.08 seconds

Conclusion

I am generally slow to get onboard with additions to Javascript, since I’d rather allow browsers to widely adopt first. But in the case of Node.js, I only have one JS engine to be concerned about. As long as I’m running a stable version of Node that supports a feature, I’m happy to consider it. In this case, I’m really glad I did, and I highly recommend you give it a try in your own scripts or unit tests. But keep in mind, it’s not something you want to fold into your production browser-based code just yet, since it’s still experimental, at least until ES8.


This article has been reblogged at the following sites:

DZone: http://bit.ly/async-await-in-jasmine

Leave a Reply

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