How to write better Jasmine tests with mocks

How to write better Jasmine tests with mocks

Jasmine project logo

Copyright (c) Pivotal Labs

I recently started using Jasmine to write my JavaScript tests. I really like it – it made writing tests actually kind of fun. I especially love that you can write tests with mocks in a similar manner as you would using Mockito in the Java world.

A mock is basically a lightweight object that imitates the API and (to a degree) the behavior of other objects. This is useful if you have an object A that interacts with object B, but you want to write a spec that only tests the implementation of A. I think this is generally a good idea, but I’m not here to discuss the pros and cons of integration tests. In any case it saves you the code and time needed to set up a ‘real’ object B, which can add up if B is part of a complex architecture.

Note: All code snippets (or variants of them) shown here can be found in this Jasmine suite [1]. It’s Jasmine 1.3 and 2.0 compatible and also has some additional examples/tricks.

Creating a Mock

Jasmine has something approximating mocks: ‘spy objects’. The jasmine.createSpyObj method can be called with a list of names, and returns an object which consists only of spies of the given names. This is great, but maintaining a list of method names in your test code needs space, requires you to know the exact API of the mocked object, and can easily fail due to typos or API changes. In many cases you can avoid these issues with just a few extra lines:

window.mock = function( constr, name ) {
  var keys = [];
  for( var key in constr.prototype ) {
    keys.push( key );
  }
  return keys.length > 0 ? jasmine.createSpyObj( name || "mock", keys ) : {};
};

To create a mock, give the constructor as the first argument, and a name as the second (optional). For example:

var Foo = function(){};
Foo.prototype.callMe = function() {};
var foo = mock( Foo );

foo.callMe();

expect( foo.callMe ).toHaveBeenCalled();

Now, this mock function admittedly has its limits, which we will discuss later. For now, let’s see what we can do with it.

Working with Mocks

The most obvious way to work with a mock is to use it as a parameter for methods and constructors. For example, we can easily test that a method sets the background of a given jQuery selection to red:

var el = mock( $ );

someObject.methodUnderTest( el );

expect( el.css ).toHaveBeenCalledWith( "background", "red" );

If your tested code is supposed to register event listeners on your mocked object, you can test these as well with another helper. This implementation assumes the API for registering listener is object.addListener( type, listener ), but it can easily be adapted to most APIs that follow this pattern.

For Jasmine 1.3:

function getListener( mock, type ) {
  var spy = mock.addListener; // or "on", "addEventListener", etc...
  for( var i = 0; i < spy.callCount; i++ ) {
    if( spy.argsForCall[ i ][ 0 ] === type ) {
      return spy.argsForCall[ i ][ 1 ];
    }
  }
  throw new Error( "Listener " + type + " not found" );
}

For Jasmine 2.0:

function getListener( mock, type ) {
  var spy = mock.addListener; // or "on", "addEventListener", etc...
  for( var i = 0; i < spy.calls.count(); i++ ) {
    if( spy.calls.argsFor( i )[ 0 ] === type ) {
      return spy.calls.argsFor( i )[ 1 ];
    }
  }
  throw new Error( "Listener " + type + " not found" );
}

Now you can, for example, test if a change listener registered on your mocked object sets the event property “x” to 1:

var eventMock = {};
var foo = mock( Foo );
someSetUpCode( foo );

getListener( foo, "changeXYZ" )( eventMock );

expect( eventMock.x ).toBe( 1 );

When the object you want to mock is created internally in your tested code, we may have a bit of a problem:

var bar = function() {
  // Foo would not be mock-able:
  var fooInstance = new Foo();
  fooInstance.callMe();
};

That’s why I would recommend to use factories in your code:

// Type to mock:
var Foo = function() {};
Foo.prototype.callMe = function() {};
Foo.createInstance = function() {
  return new Foo();
};
// Code to test:
var bar = function() {
  var fooInstance = Foo.createInstance();
  fooInstance.callMe();
};
// Test:
var foo = mock( Foo );
spyOn( Foo, "createInstance" ).andReturn( foo );

bar();

expect( foo.callMe ).toHaveBeenCalled();

By the way, the mock function also works (for the most part – see next chapter) with native/DOM constructors. For example:

var xhr = mock( XMLHttpRequest );

xhr.send();

expect( xhr.send ).toHaveBeenCalled();

Limitations

It is a common pattern to add methods to an object in its constructor to achieve some form of privacy, which prevents us from creating spies automatically:

var Foo = function( x ) {
  this.getX = function() {
    return x;
  };
};
var foo = mock( Foo );
expect( foo.getX ).not.toBeDefined();

The mock created above won’t have any spies – you will need to add them yourself afterwards or use jasmine.createSpyObj directly.

If the code you test uses the instanceof operator, on your mock it won’t be identified correctly. Also, if you have “constants” (i.e. properties) on your prototype, they will not be available on the mock (they would actually have a spy in their place). These problems could be solved by a more sophisticated version of mock:

window.mock = function( constr, name ) {
  var HelperConstr = new Function();
  HelperConstr.prototype = constr.prototype;
  var result = new HelperConstr();
  for( var key in constr.prototype ) {
    try {
      if( constr.prototype[ key ] instanceof Function ) {
      result[ key ] = jasmine.createSpy( ( name || "mock" ) + '.' + key );
    }
  } catch( ex ) {
  }
  return result;
};

This works fine…

var Foo = function() {};
Foo.prototype.type = "Foo";

var foo = mock( Foo );

expect( foo.type ).toBe( "Foo" );
expect( foo instanceof Foo ).toBeTruthy();

… except when the prototype has native getters, which most DOM objects do. In such case, attempting to check the property type (or overwrite it) may throw an exception…

var xhr = mock( XMLHttpRequest );
xhr.onreadystatechange = function(){};
xhr.send();
 
expect( xhr.send ).toHaveBeenCalled();
// Just the attempt to read onreadystatechange throws an error in most browser
expect( xhr.onreadystatechange ).toBeDefined(); 

In the Jasmine Suite that accompanies this article I have outlined a third version of mock, which at least supports the prototype properties, but it’s not an ideal solution. On the other hand, using instanceof to check for anything but built-in constructors (e.g. Array, Function) is rarely useful in my opinion, so perhaps I just worry too much about this issue.

Final Thoughts…

Like with many JavaScript tricks, this only works when your application code uses specific patterns (e.g. prototypes instead of constructor-added methods, factories instead of new calls, duck-typing instead of instanceof). Personally, I have no problems adjusting my code here and there to be able to write better tests.

Of course mocks are not always the right choice. For static objects, Jasmine’s spyOn functions should be used, and there is usually no point in mocking very simple objects that have no dependencies. And usually what’s important is that you use mocks at all, not how you create them – just using object literals can work fine too.

Resources:
[1]
https://gist.github.com/tbuschto/9766267