Using Jasmine Spies to Create Mocks and Simplify the Scope of Your Tests


Jasmine spies are a great and easy way to create mock objects for testing. By using a Spy object, you remove the need to create your own function and class stubs just to satisfy test dependencies.

Some TypeScript Code

Let’s say you have this service for saving a person:

iperson-service.ts

/// <reference path="resources/idata-context.ts" />
/// <reference path="resources/iperson.ts" />
/// <reference path="resources/iperson-validator.ts" />
module JasmineMocks {
  export class PersonService {
    constructor(
      private _validator: IPersonValidator,
      private _dataContext: IDataContext,
    ) {}
 
    save(person: IPerson) {
      if (this._validator.isValid(person)) {
        this._dataContext.savePerson(person);
      } else {
        throw new Error('Person is not valid.');
      }
    }
  }
}

If you were going to test this without mocks, you’d have to create method stubs for your validator and data context then add checks in there to make sure they were called. This is just adding to the complexity of the test and taking you further away from your base code.

createSpyObj

There are a few ways to create mocks with Jasmine. You can

  • use spyOn to create a spy around an existing object
  • use jasmine.createSpy to create a testable function
  • use jasmine.createSpyObj to create an object with a number of internal spy functions

It’s the latter that we’ll be using.

The interface for our validation service looks like this:

iperson-validator.ts

/// <reference path="iperson.ts" />
module JasmineMocks {
  export interface IPersonValidator {
    isValid(person: IPerson): boolean;
  }
}

So, to create out mock, we’ll do this:

var validator = jasmine.createSpyObj('validator', ['isValid']);

What’s this doing?

We’re creating a new Spy object with an alias of validator. This object has one Spy function called isValid.

Once this has been created, we can monitor any calls to isValid and control what it returns.

We can create the mock for our data context object in the same way.

idata-context.ts

/// <reference path="iperson.ts" />
module JasmineMocks {
  export interface IDataContext {
    savePerson(person: IPerson): void;
  }
}

and

var dataContext = jasmine.createSpyObj('dataContext', ['savePerson']);

Let’s start setting up our tests.

Testing Save Works For a Valid Person

Here’s our test function. We’ll go through it line by line afterwards.

/// <reference path="resources/idata-context.ts" />
/// <reference path="resources/iperson.ts" />
/// <reference path="resources/iperson-validator.ts" />
/// <reference path="../bower_components/DefinitelyTyped/jasmine/jasmine.d.ts" />
module JasmineMocks.Tests {
  describe('personService tests', () => {
    var validator: any;
    var dataContext: any;
 
    beforeEach(() => {
      validator = jasmine.createSpyObj('validator', ['isValid']);
      dataContext = jasmine.createSpyObj('dataContext', ['savePerson']);
    });
 
    it('save is called if person is valid', () => {
      // Arrange
      validator.isValid.and.returnValue(true);
      var service = new JasmineMocks.PersonService(
        <IPersonValidator>validator,
        <IDataContext>dataContext,
      );
 
      var validPerson = <IPerson>{};
 
      // Act
      service.save(validPerson);
 
      // Assert
      expect(validator.isValid).toHaveBeenCalledWith(validPerson);
      expect(dataContext.savePerson).toHaveBeenCalledWith(validPerson);
    });
  });
}

Setting up the mocks

We create the mocks on lines 7-13:

var validator: any;
var dataContext: any;
 
beforeEach(() => {
  validator = jasmine.createSpyObj('validator', ['isValid']);
  dataContext = jasmine.createSpyObj('dataContext', ['savePerson']);
});

The two mocks are created as above. We’ll do this in the beforeEach function to make sure that we create clean objects at the start of every test.

We use the any type for the mock objects so that we don’t have issues attaching Jasmine’s and function onto properties.

Inside our test, we use this functionality to set what value we want our service to return.

validator.isValid.and.returnValue(true);

This will cause any calls to isValid to return true.

For TypeScript, we need to cast the two mocks into their required types when we instantiate our service.

var service = new JasmineMocks.PersonService(
  <IPersonValidator>validator,
  <IDataContext>dataContext,
);

And our validPerson object is just an empty literal.

var validPerson = <IPerson>{};

Now that we have our service and objects set up, we can call the function we want to test.

// Act
service.save(validPerson);

In our assertions, we can check to make sure the validator method was called using Jasmine’s toHaveBeenCalledWith function, passing in the same IPerson instance we passed to save.

expect(validator.isValid).toHaveBeenCalledWith(validPerson);

And we can use the same function to make sure savePerson has been called on our data context.

expect(dataContext.savePerson).toHaveBeenCalledWith(validPerson);

That lets us test that everything has worked as expected. But what about testing for errors?

Testing Exceptions

In our service, we throw an error if the IPerson instance is invalid.

save(person: IPerson) {
  if (this._validator.isValid(person)) {
    this._dataContext.savePerson(person);
  } else {
    throw new Error("Person is not valid.");
  }
}

Our test for the error will look like this:

it('exception is thrown if person is not valid', () => {
  // Arrange
  validator.isValid.and.returnValue(false);
  var service = new JasmineMocks.PersonService(
    <IPersonValidator>validator,
    <IDataContext>dataContext,
  );
 
  var invalidPerson = <IPerson>{};
 
  // Act & Assert
  expect(() => {
    service.save(invalidPerson);
  }).toThrow();
 
  expect(validator.isValid).toHaveBeenCalledWith(invalidPerson);
  expect(dataContext.savePerson).not.toHaveBeenCalled();
});

At the start, we’re setting the isValid method to return false this time.

validator.isValid.and.returnValue(false);

We create then our service as before.

var service = new JasmineMocks.PersonService(
  <IPersonValidator>validator,
  <IDataContext>dataContext,
);

Our test for the exception is a little different though. Jasmine uses the toThrow expectation to test for thrown errors. To use this with expect, we need to wrap it in a containing function like so:

expect(() => {
  service.save(invalidPerson);
}).toThrow();

The containing function allows us to separate errors in our Jasmine spec with errors thrown by our test code.

We can then use the toHaveBeenCalledWith method again to check our validation method has been called:

expect(validator.isValid).toHaveBeenCalledWith(invalidPerson);

and the not modifier to ensure that the data context’s savePerson method has not been called:

expect(dataContext.savePerson).not.toHaveBeenCalled();

If you want to grab the code used here, it’s available on GitHub.