Unit Testing with Jasmine and Async/Await
In this article, you'll learn how to conduct asynchronous unit testing on your code using async/await, Jasmine, and NodeJS technologies.
Join the DZone community and get the full member experience.
Join For FreeToday, 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 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:
- Use the jasmine-await library. (It extends the functions
it()
,beforeEach()
,afterEach()
,beforeAll()
andafterAll()
and wraps them in theasync()
function. So you can always useawait()
to wait for a promise's resolution or rejection.) 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.Test that the app was is initialized successfully. A basic, synchronous test.
Test that async / await actually works with a simple example from the Mozilla docs.
Test that a known user profile can be downloaded from Firebase. This is where the magic happens.
// Use the jasmine-await npm package
var async = require("jasmine-await");
var it = async.it;
var await = async.await;
describe("Test Firebase access with async/await", () => {
let admin = require("firebase-admin");
let serviceAccount = require(__dirname + "/serviceAccountKey.json");
let app = null;
// Initialize the app with loaded credentials
beforeAll( () => {
app = admin.initializeApp({
credential: admin.credential.cert(serviceAccount),
databaseURL: "https://mytestdatabse.firebaseio.com"
});
});
// Test 1: Ensure we've got an initialized app
it("receives an initialized app from firebase", () => expect(app).not.toBe(null) );
// Test 2: 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 3: Read a known profile
it("fetches a known profile from firebase", async () => {
const uid = "1sT1mpFabkVJcXHtTCTsqiLiTrF3";
let profileRef = admin.database().ref('/profile/' + uid);
let snapshot = await profileRef.once('value');
expect(snapshot).not.toBe(null);
expect(snapshot.val().uid).toBe(uid);
process.stdout.write(""+snapshot.val().uid); // For visual proof
});
});
Running this test suite outputs:
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 the async and await keywords from the final test:
// Test 3: Read a known profile
it("fetches a known profile from firebase", () => {
const uid = "1sT1mpFabkVJcXHtTCTsqiLiTrF3";
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);
});
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.
Published at DZone with permission of Cliff Hall, DZone MVB. See the original article here.
Opinions expressed by DZone contributors are their own.
Comments