• Login
  • Apply
Back to Blog

Spies, Stubs, and Mocks: An Introduction to Testing Strategies

You have put in days of work on your current feature, and are about to integrate it with your team’s master version. You’ve built out a lot of new functionality, and even made use of a new library or two. It looks like everything is working, but how can you be sure your code will play nice with the team’s code? In this blog, we will break down some helpful methods for testing your code and all of its ingredients—spies, stubs, and mocks.

As developers, we are well aware of the importance of testing. We add features to an increasingly complex application, and it becomes paramount to ensure that none of the code we are adding breaks any existing functionality. This is why unit tests are the foundation of most testing suites—they have strict outcomes, are relatively quick to write, and allow developers to identify issues early in the development cycle.

Writing good unit tests depends heavily on isolation—making sure we are only testing the functionality of one thing at a time, in order to verify the integrity of a specific part of the application. If your test is dependent on external factors and suddenly starts to fail, it can be a Herculean task to figure out exactly what is causing the failing result. Since applications tend to be constructed of interlocking modules, external fetches, and conditional logic, how can we go about teasing apart the functionality, so we can test it piece by piece?

This is where spies come in.

aa1

SPIES IN OUR MIDST

Spies are essentially functions that have the ability to track information about the instance of a function being called. They can record many types of data, from the number of invocations of a specific function to the arguments passed into it and the resulting returned value. Here’s an example:

/** I’m using the testing framework Sinon.js for our examples,
* since its sole focus is spies, stubs, and mocks, it’s relatively
* simple and is compatible with other unit-testing frameworks.*/
const sinon = require('sinon');

// create a spy
const ourSpy = sinon.spy();

// invoke the spy
ourSpy('testing', 1, 2, 3);

// use methods to access specifics about the invocation
console.log(ourSpy.called); // logs -> true
console.log(ourSpy.firstCall.args); // logs -> [ 'testing', 1, 2, 3 ]
console.log(ourSpy.secondCall) // logs -> null

In the above example, we created an anonymous spy called ourSpy, a call to which returns an object with properties that contain information about the specific call. If we want to make sure the function was invoked, we can use the ‘called’ method to check. We can also use the firstCall property to see exactly what arguments ourSpy was called with, but if we try to utilize the secondCall property, we get null, since ourSpy was only called once!

We can also use our spies to wrap our own functions:

// our function
const users = {
createUser: function(firstName, lastName, age) {
this.firstName = firstName;
this.lastName = lastName;
this.age = age;
}
}

// set up a spy on the function
const spyOnCreateUser = sinon.spy(users, 'createUser')

// invoke the function with some data
users.createUser('Peregrin', 'Took', 29);
users.createUser('Samwise', 'Gamgee', 36);
users.createUser('Bilbo', 'Baggins', 129);

// use the spy to see information about function calls
console.log(spyOnCreateUser.callCount) // logs -> 3
console.log(spyOnCreateUser.calledWith('Bilbo')) // logs -> true

// clean up
spyOnCreateUser.restore();

Notice that here, we use a method called restore on our spy once we’ve completed our test. This resets our function and removes the spy—if we leave the spy in place, it could affect future tests on the same function and give us misleading results.

We started out with a pretty basic example. We could use a spy to wrap a specific method on an object to gather precise data about what that method is doing (you could also spy on the object in its entirety, but this is not optimal since, remember, our goal is to break our code down into in most granular components!). This is especially useful when testing methods that are part of dependencies or libraries you haven’t written yourself, and you want to be especially sure they are executing as expected.

TEST DOUBLES

If our code were an espionage movie, our hero might disguise herself as catering staff in order to infiltrate a gala full of high-profile people and obtain sensitive information. She creates an alternative version of herself, a double, and uses it to move through her world and gather information accordingly. Similarly, we can tell our functions to perform as specific, alternate versions of themselves. Gerard Meszaros, in his book xUnit Test Patterns, calls this strategy creating a “test double”—a reference to the stunt doubles required to perform technical or dangerous maneuvers in film.

aa2Stunt double

We have already examined spies, which are the simplest test doubles—simply tracking information about function calls without actually changing the nature of the function itself.

In the above examples of spies, the outcome of a function is only affected by the arguments we pass to it. But in other cases, a function might interface with collaborators, or other units of code—making a database call, or directly reading or manipulating state. In these cases, we need a way to test our process without actually being dependent on these external factors, which might give us unexpected results should we refactor, or make our test suite take forever to run, wasting developer time.

STUBS

Stubs are similar to spies, but they allow you to replace a function entirely, so you can force it to do very specific things, like return a certain value.

Let’s say we want to test a function that saves a user in a database, and we’re using jQuery’s Ajax implementation to do so. Because the following function makes an asynchronous call to a specific URL, testing this in isolation would be difficult.

function saveBook(book, callback) {
$.post('/books', {
title: book.title,
author: book.author,
published: book.published
}, callback);
}


Using a stub, we can completely replace our Ajax call, so we’re never actually hitting a server and waiting for a response—simplifying our test dramatically. Here’s an example where we are testing whether the request was made with expected arguments.

// in this case, we want to check if our stub was called with certain arguments:
describe('saveBook', function() {
it('should send correct parameters to the expected URL', function() {

// stub the 'post' method, similarly to how we spied on functions before
const post = sinon.stub($, 'post');

// define the results we expect
const expectedUrl = '/books';
const expectedParams = {
title: 'The Fellowship of the Ring',
author: 'J. R. R. Tolkien',
published: 1954
};

// set up the user object that will be saved as a result of the request
const book = {
title: expectedParams.title,
author: expectedParams.author,
published: expectedParams.published
}

// actual invocation of the function we're testing, followed by clean up
saveBook(book, function(){} );
post.restore();

// here we use sinon's built in assertions to ensure that our stubbed request was called with the correct parameters to the correct URL
sinon.assert.calledWith(post, expectedUrl, expectedParams);
});
});


We use stubbing when we need to use data to test an outcome, but the specifics of the data don’t really matter.

What happens if we need to test more than just one function at once?

MOCKS

Mocks are similar to stubs, but much more complex and robust. Like stubs, they help you verify a result, but they are also used to determine how that result was realized. They allow you to set up expectations ahead of time, and verify the results after the test has run.

Be careful though—because mocks have built-in expectations, they can cause tests to fail when used not used as expected. You have to be very intentional with their implementation. You could have several stubs running in a test file, but it’s best practice to have only a single mock, and if you don’t need to set up expectations for a specific function call, it’s best to stick with a stub.

Here’s an example where we mock an object that has methods to allow us to store users:

// an object with methods that get, set, and delete users from storage
const userStorage = {
/// ...
}

describe('incrementUsers', function() {
it('should increment the number of users by one', function() {
// create a mock of our object
const userStorageMock = sinon.mock(userStorage);

// set up expectations
userStorageMock.expects('get').withArgs('data').returns(0);
userStorageMock.expects('set').once().withArgs('data', 1);

// invoke the function in our application that makes use of userStorage
incrementUsers();

// clean up
userStorageMock.restore();

// verify that our expectations are correct
userStorageMock.verify();
});
});


With this syntax, userStorageMock.expects(‘get’) sets up an expectation that the userStorageMock.get method will be called, and will return 0 (since we have no stored users). When we call verify(), that is when we check the actual results of our call against our expectation.

It’s not always a straightforward task to write unit tests, but utilizing these techniques is an indispensable part of testing your code in an efficient and maintainable manner. All of the above can help reduce the complexity of your tests. The world of testing can be a wild one, with many different approaches and frameworks to familiarize yourself with.