JavaScript Unit Testing With Jasmine: Part 2

Last week, we started strolling through Jasmine and seeing what it had to offer us with regards to unit testing. Today, we’ll be finishing that up with some of the more advanced features included with Jasmine so that you can see the whole package and get started unit testing your own JavaScript. Trust me, even its advanced features are simple to use, so there shouldn’t be anything holding you back from reading this and getting started doing your own unit testing.

Spies and Mocks

We’ll start this one off with spies. Spies are really cool and take advantage of JavaScript’s dynamic nature to allow you to get some interesting metadata about what is happening behind the scenes in some objects. For instance, if you’re testing a function that takes a callback argument, you might want to be certain that the callback was indeed called properly. You can spy on the callback method to see if it was called and even what arguments it was called with and how many times it was called. Take a look below to see all the really cool things you get from using spyOn, the method you call to spy on a function. This code is taken directly from the Jasmine documentation.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
describe("A spy", function() {
var foo, bar = null;

beforeEach(function() {
foo = {
setBar: function(value) {
bar = value;
}
};

spyOn(foo, 'setBar');

foo.setBar(123);
foo.setBar(456, 'another param');
});

it("tracks that the spy was called", function() {
expect(foo.setBar).toHaveBeenCalled();
});

it("tracks its number of calls", function() {
expect(foo.setBar.calls.length).toEqual(2);
});

it("tracks all the arguments of its calls", function() {
expect(foo.setBar).toHaveBeenCalledWith(123);
expect(foo.setBar).toHaveBeenCalledWith(456, 'another param');
});

it("allows access to the most recent call", function() {
expect(foo.setBar.mostRecentCall.args[0]).toEqual(456);
});

it("allows access to other calls", function() {
expect(foo.setBar.calls[0].args[0]).toEqual(123);
});

it("stops all execution on a function", function() {
expect(bar).toBeNull();
});
});

It’s simple to use spyOn; just pass it an object, and the name of a method on that object that you want to spy on. If you look closely, you might realize that spyOn is replacing the original function with a spy that intercepts the function calls and tracks a lot of potentially useful information about them. The problem we run into above is that once we’ve replaced the original function, we’ve lost its capabilities. We can remedy that with andCallThrough. If you chain andCallThrough() after calling spyOn, the spy will then pass any calls to it through to the original function. Here’s another bit of code from the docs to show off andCallThrough:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
describe("A spy, when configured to call through", function() {
var foo, bar, fetchedBar;

beforeEach(function() {
foo = {
setBar: function(value) {
bar = value;
},
getBar: function() {
return bar;
}
};

spyOn(foo, 'getBar').andCallThrough();

foo.setBar(123);
fetchedBar = foo.getBar();
});

it("tracks that the spy was called", function() {
expect(foo.getBar).toHaveBeenCalled();
});

it("should not effect other functions", function() {
expect(bar).toEqual(123);
});

it("when called returns the requested value", function() {
expect(fetchedBar).toEqual(123);
});
});

Sometimes you don’t want it to call through to the original. Maybe you just want the spy to return a specific value so that you can test to see what happens when that value is returned. Or maybe you just want it to return a single value for consistency’s sake. Well, you can tell a spy to return a specified value with andReturn. It’s used similarly to andCallThrough, but obviously it is used to return a specific value instead of calling through to the original function. It takes a single argument, which is the value to be returned.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
describe("A spy, when faking a return value", function() {
var foo, bar, fetchedBar;

beforeEach(function() {
foo = {
setBar: function(value) {
bar = value;
},
getBar: function() {
return bar;
}
};

spyOn(foo, 'getBar').andReturn(745);

foo.setBar(123);
fetchedBar = foo.getBar();
});

it("tracks that the spy was called", function() {
expect(foo.getBar).toHaveBeenCalled();
});

it("should not effect other functions", function() {
expect(bar).toEqual(123);
});

it("when called returns the requested value", function() {
expect(fetchedBar).toEqual(745);
});
});

For the final andXxx spy method, we have andCallfake, which will take a function argument. Rather than passing through to the original function, this method will make it so that the spy passes through to call the function that you specified as its argument. It’ll even return any values returned from your new fake function.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
describe("A spy, when faking a return value", function() {
var foo, bar, fetchedBar;

beforeEach(function() {
foo = {
setBar: function(value) {
bar = value;
},
getBar: function() {
return bar;
}
};

spyOn(foo, 'getBar').andCallFake(function() {
return 1001;
});

foo.setBar(123);
fetchedBar = foo.getBar();
});

it("tracks that the spy was called", function() {
expect(foo.getBar).toHaveBeenCalled();
});

it("should not effect other functions", function() {
expect(bar).toEqual(123);
});

it("when called returns the requested value", function() {
expect(fetchedBar).toEqual(1001);
});
});

Now, you might be wondering, what if I don’t have an object already that I want the spy to work with? I just want to create a spy without any existing objects or functions. Is this possible? You bet! First, let’s take a look at how to create a spy function from thin air, then we’ll move on to explore the idea of making an entire spy object.

You make a spy function with jasmine.createSpy and you pass in a name. It’ll return the spy function for you. The name seems a bit useless because it isn’t used as an identifier that we can refer to it as, but as you can see below, it can be used with the spies identity property in error messages to specify where an error occurred. Here it is:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
describe("A spy, when created manually", function() {
var whatAmI;

beforeEach(function() {
whatAmI = jasmine.createSpy('whatAmI');

whatAmI("I", "am", "a", "spy");
});

it("is named, which helps in error reporting", function() {
expect(whatAmI.identity).toEqual('whatAmI')
});

it("tracks that the spy was called", function() {
expect(whatAmI).toHaveBeenCalled();
});

it("tracks its number of calls", function() {
expect(whatAmI.calls.length).toEqual(1);
});

it("tracks all the arguments of its calls", function() {
expect(whatAmI).toHaveBeenCalledWith("I", "am", "a", "spy");
});

it("allows access to the most recent call", function() {
expect(whatAmI.mostRecentCall.args[0]).toEqual("I");
});
});

Finally, let’s create an object with all spy methods using jasmine.createSpyObj. As with createSpy, it takes a name, but it also takes an array of strings that will be used as the names of the spy functions attached to the object. The name is used the exact same way that it is used with createSpy: identifying objects during Jasmine error results.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
describe("Multiple spies, when created manually", function() {
var tape;

beforeEach(function() {
tape = jasmine.createSpyObj('tape', ['play', 'pause', 'stop', 'rewind']);

tape.play();
tape.pause();
tape.rewind(0);
});

it("creates spies for each requested function", function() {
expect(tape.play).toBeDefined();
expect(tape.pause).toBeDefined();
expect(tape.stop).toBeDefined();
expect(tape.rewind).toBeDefined();
});

it("tracks that the spies were called", function() {
expect(tape.play).toHaveBeenCalled();
expect(tape.pause).toHaveBeenCalled();
expect(tape.rewind).toHaveBeenCalled();
expect(tape.stop).not.toHaveBeenCalled();
});

it("tracks all the arguments of its calls", function() {
expect(tape.rewind).toHaveBeenCalledWith(0);
});
});

Testing Asynchronous Functions

Asynchronous programming isn’t simple, at least not as simple as straight-forward synchronous programming. This makes people scared to test asynchronous functions even more, but Jasmine makes it really simple to test asynchronous functions too. Let’s take a look at an example using an AJAX request with jQuery:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
describe("Asynchronous Tests", function() {
it("is pretty simple with <code>runs</code>, <code>waitsFor</code>, <code>runs</code>", function() {
var finished = false,
worked = false;

runs(function() {
$.ajax('/example/').always(function(){
finished = true;
}).done(function(){
worked = true;
});
});

waitsFor(function(){
return finished;
}, "AJAX should complete", 1000);

runs(function() {
expect(worked).toBe(true);
});
});
});

This might not make much sense just looking at it, but with a little explanation it’ll seem dead simple and all your fears of asynchronous testing will dissipate. We’ll hop right into the body of the it block to get started. First we created a couple flags. These aren’t always necessary, depending on how the asynchronous function works, but if you need them, these can hold Booleans that specify whether the asynchronous function worked/finished, like I did here. Now we get to the fun part: runs and waitsFor. The first call to runs is where we run an asynchronous function. Then we use waitsFor to determine when/if the asynchronous function finished. This is done by specifying a function that returns a boolean that should be true when the asynchronous work is finished or false before it finishes. This is the first argument passed in. The next one is the error we want to show if it never returns true, and the final argument is the number of milliseconds we should wait before it times out and fails the spec. The function that is passed into waitsFor is run on short intervals until it either returns true or it times out. Then we move on and run the function passed into the next runs call. This is generally where you do your expecting.

The fun part is that you can continue alternating between runs and waitsfor (potentially) inifinitely. So, if you want to run another asynchronous function in the second runs and then do another waitsfor and finally call runs once again to complete your tests, it’s entirely possible. You’ll see me do this in an article soon when I talk about testing Socket.IO.

Mocking the JavaScript Clock

If you have code that runs with setTimeout or setInterval, you can skip the asynchronous testing and just use Jasmine to control the clock, allowing you to run that code synchronously. Just tell jasmine to use its own mock clock with jasmine.Clock.useMock() and then use jasmine.Clock.tick([number]) to move the clock ahead whenever you want.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
describe("Manually ticking the Jasmine Mock Clock", function() {
var timerCallback;

// It is installed with a call to jasmine.Clock.useMock in a spec or
// suite that needs to call the timer functions.
beforeEach(function() {
timerCallback = jasmine.createSpy('timerCallback');
jasmine.Clock.useMock();
});

// Calls to any registered callback are triggered when the clock is
// ticked forward via the jasmine.Clock.tick function, which takes
// a number of milliseconds.
it("causes a timeout to be called synchronously", function() {
setTimeout(function() {
timerCallback();
}, 100);

expect(timerCallback).not.toHaveBeenCalled();

jasmine.Clock.tick(101);

expect(timerCallback).toHaveBeenCalled();
});

it("causes an interval to be called synchronously", function() {
setInterval(function() {
timerCallback();
}, 100);

expect(timerCallback).not.toHaveBeenCalled();

jasmine.Clock.tick(101);
expect(timerCallback.callCount).toEqual(1);

jasmine.Clock.tick(50);
expect(timerCallback.callCount).toEqual(1);

jasmine.Clock.tick(50);
expect(timerCallback.callCount).toEqual(2);
});
});

As simple as the asynchronous testing is, I still would rather use this when I can. It’s fun to have that much power. Of course, this doesn’t actually affect the clock, but who cares? It feels like it does, right?

Matching Types with jasmine.any

Sometimes, trying to test for a specific value is too strict and you just want to make sure it is of a specific type, like a number or object. In this case jasmine.any comes to the rescue. You can use it in any matcher to check a value’s type instead of comparing it to an exact value.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
describe("jasmine.any", function() {
it("matches any value", function() {
expect({}).toEqual(jasmine.any(Object));
expect(12).toEqual(jasmine.any(Number));
});

describe("when used with a spy", function() {
it("is useful for comparing arguments", function() {
var foo = jasmine.createSpy('foo');
foo(12, function() {
return true
});

expect(foo).toHaveBeenCalledWith(jasmine.any(Number), jasmine.any(Function));
});
});
});

It takes a constructor name and compares it to the constructor of the value. This means, you can test it against your custom types too, not just the built in ones.

Disabling Specs and Suites

Sometimes you don’t want a spec or suite to run, whether it is because it takes too long, or you know it will fail and don’t want to deal with it until later. You could always comment it out, but then if you want to turn all of the commented out specs back on, it’s difficult to do a search and replace. Instead you can prepend describe or it with an “x”, and the suite or spec will be skipped just as if it was commented out, but a simple search for xdescribe can be replaced with describe. The same goes for xit and it.

1
2
3
4
5
6
7
8
9
10
11
12
xdescribe("A disabled suite or spec", function() {
var foo;

beforeEach(function() {
foo = 0;
foo += 1;
});

xit("will be skipped", function() {
expect(foo).toEqual(1);
});
});

Conclusion

Well that’s pretty much all you need to know to get started with unit testing using the Jasmine framework. I hope that its simplicity will draw you in and that if you’ve been holding off on unit testing, you’ll start now. God bless and happy coding.

Author: Joe Zimmerman

Author: Joe Zimmerman Joe Zimmerman has been doing web development ever since he found an HTML book on his dad's shelf when he was 12. Since then, JavaScript has grown in popularity and he has become passionate about it. He also loves to teach others though his blog and other popular blogs. When he's not writing code, he's spending time with his wife and children and leading them in God's Word.