Custom matchers are one of Jasmine’s most powerful, and yet underutilized, features.
Here at Pivotal Labs, we write tests for a few reasons: to drive feature development, to catch regressions, and to provide code documentation to other developers. Because of this, it is important that our tests are not only comprehensive, but also descriptive and legible.
Project-Specific Matchers
Many Jasmine users only use the default set of matchers. These default matchers are extremely powerful, but, when dealing with complicated and/or non-intuitive code, they can result in confusing specs and failure messages. Say, for instance, you have the following spec:
it('deactivates the account', function() {
account.deactivate();
expect(account.get('status').statusCode).toEqual(5);
});
When the test fails, it will look something like this:
Notice it is not immediately clear from the failure message what you were asserting (for example: maybe you were testing the length of a collection of activated accounts). Also, quickly scanning the spec itself, it is not obvious what you are testing for.
If there are only a couple specs in your suite that care about deactivated accounts, this is probably good enough. However, if you find yourself repeatedly checking for deactivated accounts, it probably makes sense to write a custom matcher. Custom matchers DRY up your code at the concept level, which increases spec readability, unlike simply removing spec duplication using other techniques.
Here is what the same spec would look like with a custom matcher:
it('deactivates the account', function(){
account.deactivate();
expect(account).toBeDeactivated();
});
And here is what the failure message would look like:
To get this behavior, I used this custom matcher:
beforeEach(function(){
jasmine.addMatchers({
toBeDeactivated: function() {
return {
compare: function(account){
var accountStatusCode = account.get('status').statusCode;
var result = { pass: accountStatusCode === 5 };
if(result.pass) {
result.message = "Expected account with status code '" + accountStatusCode + " NOT to be deactivated.";
} else {
result.message = "Expected account with status code '" + accountStatusCode + "' to be deactivated.";
}
return result;
}
}
}
}
});
Another Example
I’ll admit the previous example was a bit contrived, however I regularly see specs like this:
it('hides the secrets', function() {
expect(secretView.$('p.secrets').hasClass('hide')).toBeTruthy();
});
This spec results in this unhelpful failure message:
If your project has a unified way of hiding content (and it should) then it makes sense to have a toBeHidden()
custom matcher.
Project-Independent Matchers
Custom matchers aren’t just limited to the domain of an individual project. For example, if you are writing browser JavaScript, you will likely want to check the ‘href’ attribute of anchor tags.
Here is an example of what your Jasmine spec might look like without a custom matcher
it('links to the correct show page', function(){
expect(gizmoView.$('a.show').attr('href')).toEqual('/gizmo/7');
});
This is reasonable, though the spec still doesn’t read as easily as it could. Here is what this same test would look like using my team’s toHaveHref()
custom matcher.
it('links to the correct show page', function(){
expect(gizmoView.$('a.showPageLink')).toHaveHref('/gizmo/7');
});
I personally like this failure message better. If you disagree, custom matchers empower you to create failure messages that cater to your team’s preferences.
Checking the href of an anchor has nothing specific to do with the project domain, and will probably be useful on any project with links. There are a few collections of project-agnostic custom matchers like Jasmine-Matchers and Jasmine-jQuery, but there is currently no centralized place to find already-written matchers.
The Future of Jasmine Matchers
Custom Jasmine matchers currently hold an interesting position in the Jasmine community. Though they provide great opportunities to improve spec readability and DRY up your tests, most Jasmine users do not employ them. Most of the project-independent custom matchers I use are either repeatedly rewritten for each project, or passed on using email and GitHub gists.
The proliferation of these matchers (or lack thereof) is not an easy problem to solve. Each project has different requirements and dependencies. Most of the matchers I use have Underscore and jQuery dependencies, but not every project will have these libraries. If I’m writing a Node server, I probably don’t need Backbone-specific matchers, though I might want a good set of Q matchers for my promises. For these reasons, it probably does not make sense to have one central matcher repository. On the other hand, I don’t want to hunt down a set of matchers for every JavaScript library I use.
How do you, dear reader, share your custom matchers between projects? How can the Jasmine community evolve to better support the open exchange of these matchers?