Testing async lazy assertion

Testing lazy async assertion by spying on objects.

I use a library of lazy assertions lazy-ass in production code and in my testing code (see testing without matchers blog post). When testing an AngularJs promise-returning function, it is easy to test the success path, but sometimes it is harder to test the failure path, because the thrown exception triggers $exceptionHandler and disrupts the entire program. Here is how I test angular code that can throw exceptions asynchronously.

Imagine we have a service that loads a resource. If the resource cannot be loaded, the service throws an error. We never throw an error directly, instead we using global window.la function to only throw an error when a predicate is false

1
2
3
4
5
6
7
8
9
10
11
12
function loadResource(params) {
la(check.object(params), 'invalid params', params);
function loaded(result) {
// success!
...
}
function failed(err) {
la(false, 'failed to load', params, err); // 1
}
return $http.get(serviceUrl, { params: params })
.then(loaded, failed);
};

We can easily test the success path using Jasmine test framework and running through Karma plugin

1
2
3
4
5
6
7
8
9
10
11
describe('loads resource', function () {
beforeEach(function () {
// setup $httpBackend
});
it('loads successfully', function () {
loadResource({ foo: 'bar' }).then(function (result) {
// check result
});
$httpBackend.flush();
});
});

In order to test the failure path we need to make sure we intercept and prevent la from firing in // 1 otherwise a global error handler will kick in. Luckily, Jasmine has built-in method spying and mocking. We are going to use the plain spyOn feature.

1
2
3
4
5
6
7
8
9
10
describe('throws error when load fails', function () {
beforeEach(function () {
// setup $httpBackend to fail
});
it('throws', function () {
spyOn(window, 'la'); // 1
loadResource({ invalid: 'argument' });
$httpBackend.flush(); // 2
});
});

When we call spyOn(window, 'la'); Jasmine will overwrite la method, replacing the actual function with a spy. Thus failed(err) function call will no longer throw an error. Instead the call will be recorded inside the spy. We can verify that the spy has the expected call. Because window.la(...) was called several times as part of the defensive input checks (at the start of loadResource), we need to verify the last call to window.la

1
2
3
4
5
6
7
8
9
10
11
describe('throws error when load fails', function () {
beforeEach(function () {
// setup $httpBackend to fail
});
it('throws', function () {
spyOn(window, 'la');
loadResource({ invalid: 'argument' });
$httpBackend.flush();
expect(window.la.mostRecentCall.args[1]).toEqual('failed to load');
});
});

We can verify more arguments than just the error message. We could even avoid using expect matcher and instead use the lazyAss assertion itself. Because Jasmine only mocked window.la, and window.la and window.lazyAss are aliases to the same function, the other alias remains fully functioning

1
2
3
4
5
6
7
8
9
10
11
12
describe('throws error when load fails', function () {
beforeEach(function () {
// setup $httpBackend to fail
});
it('throws', function () {
spyOn(window, 'la');
loadResource({ invalid: 'argument' });
$httpBackend.flush();
lazyAss(window.la.mostRecentCall.args[1] === 'failed to load',
'incorrect failure arguments', window.la.mostRecentCall.args);
});
});

You do not need to worry about restoring the original function. When the spec finishes running, all spies are automatically restored back to the original functions.