Hamcrest For QUnit
Published the
I finally released the first version of a port of the Hamcrest library for QUnit on my GitHub. Hamcrest is a set of matchers functions used to write self-describing assertions in your unit tests. The matchers implemented in this javascript version are mostly based on the ActionScript version by Drew Bourne.
Hamcrest for QUnit is described in details in the rest of this post.
Useful links
- The project on GitHub
- Running the unit tests in your browser
- The tests source code (a good place to view all the matchers and their usage)
If you don't know what unit testing is, I'll recommend that you take a look to the Wikipedia's article before going further in this post.
And for those that already know what Hamcrest is and just want to see what's this implementation have in particular you can just go here directly.
A word about QUnit
QUnit is the unit testing framework used by the jQuery project. It's really simple and easy to use but, as many other test suite, the assertion's results description can be pretty awkward. For instance, when writing the following test :
test( "a sample test", function(){
var anObject = {};
notEqual( anObject, null );
});
QUnit result will look like that :
1. okay
Expected: null
Result: {}
Diff: null {}
It's obvious here that the test result doesn't make any sense. How is it possible that when expecting null and passing {} the result is okay ? In the same way, if I'll make this test fail like this :
test( "a sample test", function(){
notEqual( null, null );
});
We'll get :
1.failed Expected: null Source: ()@file:///path/to/qunit/qunit.js:102
As I didn't specified any more description to the test assertion, the only informations provided here cannot help me understand why the test failed. I'll have to look to the test source code to understand that it was a notEqual assertion. It means that I'll have to specify a meaningful description for each test I write, which can be pretty tough when dealing with hundred of tests. It also means that each time I change a test, I'll have to change its description to match the new assertion. And, as I'm a lazy coder, I really don't like the idea of having to write tons of descriptions for all my tests especially when there's another way around.
However, having meaningful descriptions for test is not a small matter, especially when testing units that have a broad range of usage. The big deal here is that if your description is too poor, or outdated, finding the test that fail, the reason behind the failure and fix it will take some time. That's why it's really important to have the ability to find out which test failed and having good data on the reason of the failure directly in the test results.
Hamcrest and self-describing assertions
The main idea behind Hamcrest is that all assertions should be written with matchers that make tests explicit, either in the source code or in the tests results.
In a nutshell, writing the same assertion as above with Hamcrest will give :
test( "a sample test", function(){
var anObject = {};
assertThat( anObject, notNullValue() );
});
And the result will be :
1.okay
Expected: not null
And: was {}
This is a really simple example, but it demonstrate well that both the source code and the results are legible and clear about the test assertion.
The assertThat function define an assertion. When called without a matcher the function will perform an equalTo(true) comparison.
The assertThat function has the following signature
assertThat( value, matcherOrValue, description )
Let's directly jump to a more complex assertion, closer to what a real test can be. We have a Point object and we need to test if all its properties are properly defined after instanciation. With Hamcrest we'll write:
test( "a sample test", function(){
var aPoint = new Point(15,20);
assertThat( aPoint, hasProperties({
'x' : equalTo( 15 ),
'y' : equalTo( 20 ),
// let's consider that there's possibly
// some approximation in size math
'size' : closeTo( Math.sqrt( 15*15 + 20*20 ), 0.001 )
}));
});
Which will give us :
1. okay Expected: an Object which has properties size : a Number within 0.001 of 25 , x : 15 and y : 20 And: was { "x": 15, "y": 20, "size": 25 }
Javascript specific matchers and behaviors
isA
The isA matcher use the typeof operator for comparison.
assertThat( anObject, isA( "object" ) );
instanceOf
The instanceOf matcher will use both the instanceof operator and Object.prototype.toString to compare the value with the passed-in type.
assertThat( "a string", instanceOf( String ) );
hasProperty
The hasProperty matcher test the existence and the value of an object's property.
assertThat( anObject, hasProperty( "foo" ) ); assertThat( anObject, hasProperty( "foo", equalTo( "bar" ) ) );
If this property is a function it will not be called, if you need to call the method you can either use the hasMethod or the returns matchers. For example, this two assertions are equivalent :
assertThat( anObject, hasProperty( "foo",
returns( "bar" )
.withScope( anObject ) ) );
assertThat( anObject, hasMethod( "foo" )
.returns( "bar" ) );
The withScope call is needed on the returns matcher because only the hasMethod matcher automatically know the scope in which call the method.
hasMethod
The hasMethod matcher allow to test any methods of any object. It's possible to test the returns of the function, raised exception and passing arguments.
// only test the method existence
assertThat( anObject, hasMethod( "foo" ) );
// test if the function returns anything else than undefined
assertThat( anObject, hasMethod( "foo" ).returns() );
// test the function return
assertThat( anObject, hasMethod( "foo" )
.returns( isA( "string" ) ) );
// test the function returns (implicit equalTo) with arguments
assertThat( anObject, hasMethod( "foo" )
.returns( "some return" )
.withArgs( "some", "args" ) );
// test that the function raise an exception
assertThat( anObject, hasMethod( "foo" ).throwsError() );
// test that the function raise an exception with arguments
assertThat( anObject, hasMethod( "foo" )
.throwsError()
.withArgs("some","args"));
// test the exception thrown by the function
assertThat( anObject, hasMethod( "foo" )
.throwsError( isA( "string" ) ) );
The hasMethod matcher automatically set the scope of the call with the tested object. The call is performed as follow :
testedFunction.apply( testedObject, testArguments );
returns
The returns matcher allow to test a function returns. Contrary to the hasMethod matcher, the returns matcher expect the function as value. It can be used to test global functions.
// test that the function returns anything else than undefined
assertThat( aFunction, returns() );
// test the function return
assertThat( aFunction, returns( isA( "string" ) ) );
// test the function return with arguments
assertThat( aFunction, returns( isA( "string" ) )
.withArgs( "some", "args" ) );
// test the function return with a custom scope object
assertThat( aFunction, returns( isA( "string" ) )
.withScope( scopeObject ) );
// test the function return with a custom scope object and arguments
assertThat( aFunction, returns( isA( "string" ) )
.withScope( scopeObject )
.withArgs( "some", "args" ) );
throwsError
The throwsError matcher allow to test that a function throw an exception. Contrary to the hasMethod matcher, the throwsError matcher expect the function as value. It can be used to test global functions.
// test that the function throws anything
assertThat( aFunction, throwsError() );
// test the function thrown exception
assertThat( aFunction, throwsError( isA( "string" ) ) );
// test the function thrown exception
// when called with arguments
assertThat( aFunction, throwsError( isA( "string" ) )
.withArgs( "some", "args" ) );
// test the function thrown exception when called
// with a custom scope object
assertThat( aFunction, throwsError( isA( "string" ) )
.withScope( scopeObject ) );
// test the function thrown exception when called
// with a custom scope object and arguments
assertThat( aFunction, throwsError( isA( "string" ) )
.withScope( scopeObject )
.withArgs( "some", "args" ) );
Going further
For a complete list of matchers I'll recommend that you take a look to the unit tests and their source code, they're all listed here and you can view directly what kind of message they display.
The next step is to provide a quick explanation on how to build custom matchers (it's really simple so I don't think it will take long to write).
If you use Hamcrest4QUnit feel free to leave a comment here. And if you have any feedback, find a issue, think of an improvement, there's the GitHub issues for that.
See you space cowboy...
This post is part of the "Hamcrest For QUnit" series.
Next post in series : Creating custom Hamcrest matchers
Wants to leave a word ?