Jasmine Basics
You can find Jasmine here. I choose it because:
-
It's a JavaScript framework which does not depend on any other JavaScript frameworks, and it does not require a DOM. So I can apply it on different web apps and platforms.
-
Jasmine (and its syntax) is really popular. In the future, I can also use Jasmine to test AngularJS code and other JavaScript frameworks.
-
Jasmine has good object-oriented support, making code being modular.
Describe
describe
keyword represents the beginning of a test suite.
It takes two parameters: a string
and a function
.
The content of string represents the content of the test suite.
There can be nested test suites/nested specs exist.
Specs
it
keyword represents the beginning of a spec.
It takes two parameters: a string
and a function
.
The content of string represents that title/introduction of the spec.
A spec contains one or more expectation
s that test the state of the code.
A spec with all true expectations is a passing spec
. A spec with one or more false expectations is a failing spec
.
Fail a Spec
You can use fail
function to manually fail a spec. This function takes a failure message or a Error object as a parameter:
1 2 3 4 5 6 7 8 9 10 11 12 |
describe("A spec using the fail function", function() { var foo = function(x, callBack) { if (x) { callBack(); } }; it("should not call the callBack", function() { foo(false, function() { fail("Callback has been called"); }); }); }); |
Setups
beforeEach
, afterEach
, beforeAll
, afterAll
can be used to setup environment before/after test suites/specs.
beforeEach
function is called once before each spec run in test suite.
afterEach
function is called once after each spec run.
i.e. For each spec in a test suite,
beforeEach
function run first, then the spec function, thenafterEach
function. So if a test suite has multiple specs,beforeEach
function andafterEach
function should be run multiple times.
beforeAll
function is called *only once before all the specs run.
afterAll
function is called only once after all the specs run.
Remember if you have a variable which is share between
beforeAll
function, spec function andafterAll
function, this variable will be continuously modified until all specs are done.
Example:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
describe("A spec using beforeEach and afterEach", function() { var foo = 0; beforeEach(function() { foo += 1; }); afterEach(function() { foo = 0; }); //Here foo will be reset to 0 everytime after a spec running, because of afterEach() it("is just a function, so it can contain any code", function() { expect(foo).toEqual(1); }); it("can have more than one expectation", function() { expect(foo).toEqual(1); expect(true).toEqual(true); }); }); |
For nested
describe
blocks, before one spec running,beforeEach
function for all describes will be run. Similarly, after one spec running,afterEach
function for all describes will be run.
Use this
in Setup
this
variable can be shared between beforeEach
, it
and afterEach
function.
this
will be reset every time a spec run.
Example:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
describe("A spec", function() { beforeEach(function() { this.foo = 0; }); it("can use the `this` to share state", function() { expect(this.foo).toEqual(0); this.bar = "test pollution?"; }); it("prevents test pollution by having an empty `this` created for the next spec", function() { expect(this.foo).toEqual(0); expect(this.bar).toBe(undefined); }); }); |
Disable Suites and Specs
Instead of describe
and it
, you can use xdescribe
and xit
to skip this test suite or spec.
Expectations
An expectation in Jasmine is an assertion that is either true
or false
.
Syntax Example:
expect(a).toBe(true);
expect
takes a value, called actual
.
expect
take a matcher function
on second part, which takes the expected value.
Matcher
You can always add a not
in front of any matcher to turn it to a negative assertion.
Example:
expect(a).toEqual(9); expect(a).not.toEqual(9);
List of Matcher
-
toBe
-
toEqual
-
toBeDefined
-
toBeUndefined
-
toBeNull
-
toBeTruthy
-
toBeFalsy
-
toContain
-
toBeLessThan
-
toBeGreaterThan
-
toBeCloseTo
-
toThrow
-
toThrowError
Custom Matchers
A custom matcher at its root is a comparison function that takes an actual value and expected value.
Structure of Custom Matcher
Create a object, which contains one to many custom matchers.
Each custom matcher is a function, which takes util
and customEqualityTesters
as parameters.
util
: contains a set of utility functions for matchers to use. For example:
//this utility function test hyuk property of actual object result.pass = util.equals(actual.hyuk, "gawrsh" + expected, customEqualityTesters);
Check matchersUtil.js for all utility functions.
-
customEqualityTesters
: You may or maynot need customEqualityTester, depending on your choice of utility function.
You custom matcher method must return an object with a compare
function. The compare
function takes actual
and expected
as parameter.
In following code, a result
object is returned. This result
object must have a pass
property, which is true
or false
:
var customMatchers = { sample_custom_method: function(util, customEqualityTesters){ return{ compare: function(actual,expected){ //You may or maynot use expected variable. If not, you can skip parameter when you call this method var result = {}; result.pass = util.utility_function(); //see code above if(result.pass){ result.message = "custom your pass message"; }else{ result.message = "custom your fail message"; } return result; } } } }
You can add
.not
in utility function for result.passYou can skip defining result.message. jasmine will try to craft one for your custom matcher.
If you have too many negative comparison (
.not
), you can addnegativeCompare
function alongsidecompare
, using similar structure to include all.not
methods.
How to Use Custom Matcher
You should register you custom matchers in beforeEach
block, using a function jasmine.addMatchers
:
beforeEach(function() { //You can get all matcher methods in customMatchers jasmine.addMatchers(customMatchers); });
To use it, just call matcher method in expectations:
it("test custom matcher", function(){ expect(your_varaible).sample_custom_method(expected_varaible_in_custom_method); })
Spy
A spy
can stub any function and tracks calls to it and all arguments.
A spy only exists in the describe
or it
block it is defined, and will be removed after each spec.
When you put spy
spyOn()
inbeforeEach()
block, based on howbeforeEach()
work, your spy call will be called before each spec function run.
toHaveBeenCalled and toHaveBeenCalledWith
toHaveBeenCalled
will return true
if the spy was called.
toHaveBeenCalledWith
will return true
if the argument list matches any of the recorded calls to the spy.
Example:
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(); //this spec will call spy function }); it("tracks all the arguments of its calls", function() { expect(foo.setBar).toHaveBeenCalledWith(123); expect(foo.setBar).toHaveBeenCalledWith(456, 'another param'); //this spec detect if spy function has worked. i.e. if the variables which were spied have been modified }); it("stops all execution on a function", function() { expect(bar).toBeNull(); //this spec make sure spy should be removed after each spec function }); });
callThrough
You can add and.callThrough()
after spy function to make sure spy will track all spec calls, instead of being removed after each spec call:
spyOn(foo, 'getBar').and.callThrough();
returnValue
You can add and.returnValue(value)
after spy function to make sure your desired call in spy function will return a value:
spyOn(foo, "getBar").and.returnValue(745); //getBar method for foo object will return value `745`
callFake
You can add and.callFake(function(){})
to create a fake function which can replace the content of your spied function:
describe("A spy, when configured with an alternate implementation", function() { var foo, bar, fetchedBar; beforeEach(function() { foo = { setBar: function(value) { bar = value; }, getBar: function() { return bar; } }; //force `getBar` to return 1001, but notice `bar` variable is not affected by this fake call spyOn(foo, "getBar").and.callFake(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() { //fake function does not impact other real function expect(bar).toEqual(123); }); it("when called returns the requested value", function() { //fake function should work expect(fetchedBar).toEqual(1001); }); });
stub
To remember the state of any spy calling method above, use and.stub()
to get the original stubbing behavior:
describe("A spy", function() { var foo, bar = null; beforeEach(function() { foo = { setBar: function(value) { bar = value; } }; spyOn(foo, 'setBar').and.callThrough(); }); it("can call through and then stub in the same spec", function() { foo.setBar(123); expect(bar).toEqual(123); //use .and.stub() for the spied method foo.setBar.and.stub(); bar = null; //Now bar should always be null foo.setBar(123); expect(bar).toBe(null); }); });
Tracking Properties
After you having spayOn(obj, spied_method)
, following tracking properties can be used when you call the spied method in expect
:
-
.calls.any()
: returnfalse
if spy has not been called at all. returntrue
once at least one call happens. -
.calls.count()
: returns the number of times the spy was called. -
.calls.argsFor(index)
: assume you call a method a few times. Useindex
to get differentexpect
function results:
Example:
it("tracks the arguments of each call", function() { foo.setBar(123); foo.setBar(456, "baz"); expect(foo.setBar.calls.argsFor(0)).toEqual([123]); expect(foo.setBar.calls.argsFor(1)).toEqual([456, "baz"]); });
-
.calls.allArgs()
: assume you call a method a few times, and return all results from these calls inexpect
functions:
Example:
it("tracks the arguments of all calls", function() { foo.setBar(123); foo.setBar(456, "baz"); expect(foo.setBar.calls.allArgs()).toEqual([[123],[456, "baz"]]); });
-
calls.all()
: use this to check all context and argument(i.e.this
variable) inexpect
function:expect(foo.setBar.calls.all()).toEqual([{object: foo, args: [123], returnValue: undefined}]);
-
.calls.mostRecent()
: similar as above, but just return context and arguments for the most recent call. -
.calls.first()
: similar as above, but just return context and arguments for the first call. -
.calls.reset()
: clears all tracking for a spy
jasmine method
createSpy
jasmine.createSpy()
is a function with no implementation. It acts as other spies:
var whatAmI; beforeEach(function() { whatAmI = jasmine.createSpy('whatAmI'); whatAmI("I", "am", "a", "spy"); //it does not have implementation. Only with arguments }); //expect(whatAmI).toHaveBeenCalledWith("I", "am", "a", "spy"); //expect(whatAmI.calls.mostRecent().args[0]).toEqual("I");
createSpyObj
Similar as above, jasmine.createSpyObj
accept an array of strings, and return a object that has a property for each string of the spy:
var tape; beforeEach(function() { tape = jasmine.createSpyObj('tape', ['play', 'pause', 'stop', 'rewind']); tape.play(); tape.pause(); tape.rewind(0); });
anything
jasmine.anything
returns true
if the actual value is not null
or undefined
: expect(1).toEqual(jasmine.anything());
objectContaining
jasmine.objectContaining
tests objects with key-value pairs:
describe("jasmine.objectContaining", function() { var foo; beforeEach(function() { foo = { a: 1, b: 2, bar: "baz" }; }); it("matches objects with the expect key/value pairs", function() { expect(foo).toEqual(jasmine.objectContaining({ bar: "baz" })); expect(foo).not.toEqual(jasmine.objectContaining({ c: 37 })); });
arrayContaining
jasmine.arrayContaining
test objects with values in array:
beforeEach(function() { foo = [1, 2, 3, 4]; }); it("matches arrays with some of the values", function() { expect(foo).toEqual(jasmine.arrayContaining([3, 1])); expect(foo).not.toEqual(jasmine.arrayContaining([6])); });
stringMatching
jasmine.stringMatching()
test if you want match a string in a object or regular expression pattern:
describe('jasmine.stringMatching', function() { it("matches as a regexp", function() { expect({foo: 'bar'}).toEqual({foo: jasmine.stringMatching(/^bar$/)}); expect({foo: 'foobarbaz'}).toEqual({foo: jasmine.stringMatching('bar')}); });
Custom Equality Tester
You can create your own equality tester for your test criteria, using asymmetricMatch
function name:
var tester = { //Use asymmetricMatch as function. No returns asymmetricMatch: function(actual) { var secondValue = actual.split(',')[1]; return secondValue === 'bar'; } }; it("dives in deep", function() { expect("foo,bar,baz,quux").toEqual(tester); });
Clock
jasmine.clock().install()
can be used to setup time dependent testing. To do this, you can put jasmine.clock().install
in beforeEach()
block and jasmine.clock().uninstall()
in afterEach()
block.
Timeout
Use jasmine.clock().tick(milliseconds)
function to simulate timeout in JavaScript: jasmine.clock().tick(1000)
;
Date
You can create a Date
object in JavaScript, then use jasmine.clock().mockDate(date_object)
to simulate date:
var baseTime = new Date(2014, 9, 23); jasmine.clock().mockDate(baseTime);
If you don't provide a time object, jasmine will use current Date.
Ajax
You need to include mock-ajax.js
from here to test Ajax using jasmine.
Make sure you have jasmine.Ajax.install()
in beforeEach()
block and jasmine.Ajax.uninstall()
in afterEach()
to install and uninstall jasmine-ajax.
Define Mock Response
You may want to define a mock response to test your Ajax call response. A good place to define this is in spec/javascripts/helpers/test_responses
.
Example:
var TestResponses = { search: { success: { status: 200, //Make responseText to be your response data responseText: '{"response":{"groups":[{"type":"nearby","name":"Nearby","items":[{"id":"4bb9fd9f3db7b7138dbd229a","name":"Pivotal Labs","contact":{"twitter":"pivotalboulder"}'}]}]}} } } };
Trigger Request
You need to trigger request before you test your Ajax response. You can set a function call, such as:
var xhr = new XMLHttpRequest(); xhr.onreadystatechange = function(args) { if (this.readyState == this.DONE) { doneFn(this.responseText); } }; xhr.open("GET", "/some/cool/url"); xhr.send();
Or:
beforeEach(function() { jasmine.Ajax.install(); onSuccess = jasmine.createSpy('onSuccess'); onFailure = jasmine.createSpy('onFailure'); foursquare = new FoursquareVenueSearch(); foursquare.search('40.019461,-105.273296', { onSuccess: onSuccess, onFailure: onFailure }); request = jasmine.Ajax.requests.mostRecent(); //create a request object. This request will not be triggered until you set response expect(request.url).toBe('venues/search'); expect(request.method).toBe('POST'); expect(request.data()).toEqual({latLng: ['40.019461, -105.273296']}); });
Set Response
Use .respondWith()
to set response. You need to tell jasmine which response you want to test. For example, based on the code in Define Mock Response part, you can test success
response:
describe("on success", function() { beforeEach(function() { request.respondWith(TestResponses.search.success); }); });
You can also set the object that you want to have as response:
jasmine.Ajax.requests.mostRecend().respondWith({ "status": 200, "contentType",'text/plain', "responseText": 'test response' });
Full Steps
After these step, you can create it
blocks to test your Ajax call. Here is a full example for post
from jasmine-ajax Github repo:
describe("FoursquareVenueSearch", function() { var foursquare, request; var onSuccess, onFailure; beforeEach(function() { jasmine.Ajax.install(); onSuccess = jasmine.createSpy('onSuccess'); onFailure = jasmine.createSpy('onFailure'); foursquare = new FoursquareVenueSearch(); foursquare.search('40.019461,-105.273296', { onSuccess: onSuccess, onFailure: onFailure }); request = jasmine.Ajax.requests.mostRecent(); expect(request.url).toBe('venues/search'); expect(request.method).toBe('POST'); expect(request.data()).toEqual({latLng: ['40.019461, -105.273296']}); }); describe("on success", function() { beforeEach(function() { request.respondWith(TestResponses.search.success); }); it("calls onSuccess with an array of Locations", function() { expect(onSuccess).toHaveBeenCalled(); var successArgs = onSuccess.calls.mostRecent().args[0]; expect(successArgs.length).toEqual(1); expect(successArgs[0]).toEqual(jasmine.any(Venue)); }); }); });
Other get
Ajax call examples can be found on jasmine website.
Jasmine-jQuery
jasmine-jquery is great to combine Jasmine with jQuery. It provides:
-
a set of custom matchers for jQuery framework
-
an API for handling HTML, CSS, and JSON fixtures in your specs
Custom Matcher
You can find a list of useful custom matchers, such as toBeCheck()
or toBeSelected()
here.
HTML Fixture
I'm still trying to figure out the best practice to apply HTML Fixture for my working project. HTML Fixture can be used to load HTML content to be used by your tests.
You can find more detailed information about HTML Fixture here. For most of my working projects, I need to limit the complexity of all my JavaScript test. Also, instead of html
, my work is more involved in jsp
, which is really hard to be transferred to pure HTML code for fixture content. Personally I prefer not to write too many fixtures for to simplify my test.
I'll continue to research for the best practice of using HTML Fixture.