Unit-testing JavaScript timing events with Jasmine, Aurelia and ES2015
While working on an Aurelia application, I found myself needing to unit-test a recurring function; a function that is called every so many seconds. This is called a timing event in JavaScript, and is achieved through the setTimeout and setInterval functions.
The setup
We have the following Home module, named home.js
, defined in our Aurelia application.
import {inject} from "aurelia-framework";
export class Home {
constructor() {
}
activate() {
// we want activate to return a promise, so...
return Promise.all([
// initialization work here
]).then(() => this.startLoop());
}
startLoop() {
setInterval(() => this.getRandomSuggestion(), 7000);
}
getRandomSuggestion() {
// fetch a random suggestion from the server
}
}
Note: Aurelia uses ECMAScript 2015 syntax. if you're unfamiliar with it take a look at Babel or ES6 Katas, and of course, at our Aurelia resources round-up
Very little happens here other than some initialization. The main thing is that the activate
function calls startLoop
, which in turn sets up a loop that will call getRandomSuggestion
every 7 seconds. So how do we test this?
Aurelia, Jasmine and ECMAScript 2015
At CodeNamed, our JavaScript testing framework of choice is Jasmine, a Behaviour-Driven Development (BDD) framework. Why? Because...
It does not depend on any other JavaScript frameworks. It does not require a DOM. And it has a clean, obvious syntax so that you can easily write tests.
And basically because it damn-easy to learn and work with and because it has good documentation. Speaking of which, the code examples in there aren't written in ECMASCript 2015 (ES2015 for short), but in the current, more familiar to everyone JavaScript syntax.
How do you get Jasmine to work with ES2015? I'm assuming that if you're reading this post you already have an Aurelia application set-up, and that it's probably based on the Aurelia-Skeleton-Navigation, but if you don't, or it isn't, go take a look at it since it contains a nice, basic unit-testing set-up using Jasmine and ES2015
Setting up the tests
This is what the initial set-up of home.specs.js
looks like:
import {Home} from "../../wwwroot/src/home";
describe("the Home module", () => {
let sut;
beforeEach(() => {
sut = new Home();
});
// Insert tests here
});
We import the Home
module, and, in the beforeEach
, we initialize said module into the sut
variable. S.U.T stands for System Under Test, and it's just a TDD/BDD way to refer to the module, class, etc. that you are testing.
If you've never worked with Jasmine before, beforeEach
does exactly what it's name suggests: it is run before each of the tests defined in the current test module, and it is used to initialize classes, variables or mock data so that every test starts afresh, ensuring no test is dependent on any other test's results. There is also an afterEach
method, which is used to clean up after each test has run.
The tests
In an ideal TDD/BDD scenario you'd be writing a specs/test first, making it fail, and then write just enough code to make it pass. For the sake of readability I've already shown you the Home
module's code, but I'll follow the steps I originally took and, even for that simple bit of code, we'll have two tests.
First-off, we want to test that the loop is started and the best way to achieve this to "spy" on it. Jasmine spies are a way to mock an object or method within that object; Jasmine basically substitutes the original object or method with a spy object, thereby allowing the test framework to check the interactions with that object or method.
So, we want to verify that the method startLoop
is called when the activate
method is called, therefore, we "spy" on startLoop
and then use one of the several handy methods that Jasmine spies provide: toHaveBeenCalled
it("starts the random suggestion loop", (done) => {
spyOn(sut, "startLoop");
sut.activate().then(() => {
expect(sut.startLoop).toHaveBeenCalled();
done();
});
});
done
is just a callback that Jasmine allows you to pass to signal that some asynchronous task is completed. In our case it's necessary because activate
returns a promise; without a call to done()
our test would never finish.
Testing timing events
Testing methods that rely on the computer clock can be tricky, especially because you don't want to have to wait for the actual, real time to pass on every test run.
So how do we test that we are indeed fetching a new suggestion every so many seconds? Again, Jasmine to the rescue: Jasmine provides a clock API
. When you call clock.install()
, Jasmine substitutes the Window.setInterval
and Window.setTimer
methods with an implementation of its own. This allows Jasmine once again to check on calls to these methods and to simulate some of their functionality to manipulate time.
The first thing we do in our test is to set up Jasmine's clock by calling jasmine.clock().install()
. This allows us to simulate the passage of time by calling jasmine.clock().tick
, and passing it the amount of milliseconds we need the clock to move forward.
Here is the full test:
it("fetches a new random suggestion every 7 seconds", (done) => {
jasmine.clock().install();
spyOn(sut, "getRandomSuggestion");
sut.startLoop();
jasmine.clock().tick(14000);
expect(sut.getRandomSuggestion.calls.count()).toEqual(2);
jasmine.clock().uninstall();
});
After initializing the clock
, we want to spy on getRandomSuggestion
; we'll then wind the clock 14 seconds forward. Since we want getRandomSuggestion
to be called every 7 sec, we can expect the method to have been called twice.
Always remember to call jasmine.clock().uninstall()
at the end of the test so that the original setInterval
and setTimer
functions are restored. If you have several tests depending on clock API
, a much better approach is to install it in the beforeEach
and uninstall it in the afterEach
methods.
And there you have it. If you have any questions or suggestions let us know in the comments!