Test Doubles - AutolabJS/autolabcli GitHub Wiki

A test double is an object that can stand in for a real object in a test, similar to how a stunt double stands in for an actor in a movie. These are sometimes all commonly referred to as “mocks”, but it's important to distinguish between the different types of test doubles since they all have different uses.

The main reasons for using test doubles are -

  • Isolate the code under test
  • Speed up test execution
  • Make execution deterministic
  • Simulate special conditions

The most common types of test doubles are -

  • stubs
  • fakes
  • spies
  • mocks

Stubs

A stub has no logic, and only returns what we tell it to return. Stubs can be used when we need an object to return specific values in order to get our code under test into a certain state. While it's usually easy to write stubs by hand, using a mocking framework is often a convenient way to reduce boilerplate.

For example -

Assume we have a function which does some processing and prints result to the terminal.

printResult(arg1, arg2) {
    let result = arg1 + arg2;
    console.log(result);
}

We can stub the log method of console, to check if the passed result is as expected -


describe('myAPI.hello method', function () {
    
    let logStub;
    beforeEach(function () {
        // stub out the `hello` method
        logStub = sinon.stub(console, 'log');
    });

    afterEach(function () {
        // restore the functionality of stub
        logStub.restore();
    });

    it('should be called once', function () {
        printResult(1, 2);
        helloStub.should.have.been.calledWithExactly(3);
    });
});

Fakes

Compared to test stubs, a fake object is a more elaborate variation of a test double. Whereas a test stub can return hard-coded return values and each test may instantiate its own variation to return different values to simulate a different scenario, a fake object is more like an optimized, thinned-down version of the real thing that replicates the behavior of the real thing, but without the side effects and other consequences of using the real thing. A fake doesn’t use a mocking framework: it’s a lightweight implementation of an API that behaves like the real implementation, but isn't suitable for production (e.g. an in-memory database). Fakes can be used when we can't use a real implementation in our test (e.g. if the real implementation is too slow or it talks over the network).

For example -

Assume that a functions connects to a database though a class which has the following functions -

save(user);
findById(id);

These function connects to the actual database, which is unnecessary and time-consuming during actual test. Hence we can use a fake class that has these functions but stores data in an array.

class FakeUserRepository {

  constructor() {
    this.database = new Array(50);
  }

  save(user) {
   if (findById(user.getId()) === null)
     database.push(user);
  }

  findById(id) {
   for (const user of database) {
     if (user.getId() === id) return user;
   }
   return null;
  }

}

Here the FakeUserRepository uses an array instead of connecting to the database, which simplifies the test. This isolates the object under test from its dependency ( remote database in this case) and also improves the running time of the tests since real database queries can take upto a few seconds.

Spies

A test spy is an object that records its interaction with other objects throughout the code base. When deciding if a test was successful based on the state of available objects alone is not sufficient, we can use test spies and make assertions on things such as the number of calls, arguments passed to specific functions, return values and more.

Test spies are useful to test both callbacks and how certain functions are used throughout the system under test.

For example -

Assume we have a function that performs an asynchronous task and later calls a callback function -

function anAsyncFunction(username, callback) {
  setTimeout(function() {
    callback(username);
  }, 1000);

Here, we can use a spy to investigate whether the callback function has been called with the right parameter.

callbackSpy = createSpy();
anAsyncFunction('TestUser', callbackSpy);
callbackSpy.should.have.been.called.with('TestUser');

Mocks

A mock has expectations about the way it should be called, and a test should fail if it’s not called that way. Mocks are used to test interactions between objects, and are useful in cases where there are no other visible state changes or return results that you can verify (e.g. if your code reads from disk and you want to ensure that it doesn't do more than one disk read, you can use a mock to verify that the method that does the read is only called once). Mock objects can be much more precise by failing the test as soon as something unexpected happens.

For example -

Assume we want to mock a database though a class which has the following functions -

save(user);
findById(id);
mockDatabase = createMock(Database);
mockDatabase.expects('save').withArgs('TestUser').once();
mockDatabase.expects('findById').withArgs(30).once();

myObj.connectToDatabaseAndSave('TestUser');
myObj.connectToDatabaseAndFindNyId(30);

mockDatabase.verify();

A mock verifies that it executes exaclty as expected. For any other invocation—whether it’s to a different method or to findById() with parameter other than what we’ve told the mock object about—the mock will throw an exception, effectively failing the test. Similarly, the mock will complain if findById() is called more than once and it will complain if an expected interaction never took place.

Test Doubles Frameworks Used

  • Sinon is widely used in JavaScript to create test doubles. The three important test doubles of sinon are -
    • Spies - Which are same as the spies mentioned above.
    • Stubs - Which are same as stubs mentioned above plus they can do everything that a spy can.
    • Mocks - Which are same as the mocks mentioned above.
  • Fake Server, provided by sinon, is used to faking the server.
  • mock-socket is used as a mocking library for socket.io.

More about sinon and mock-socket is covered in their respective section.

Guidelines for using test doubles

Please note that we use only mocks in unit tests. Both mocks and spies are used in integration tests. Stubs should be used only when we need to stub a function that is computationally intensive and would take a few seconds to compute. This is because we need the test doubles in unit tests to have pre-programmed behavior as well as pre-programmed expectations. Spies are used along with mocks in integration tests. This is because we want to peek into the properties of a function call during the actual execution of that function using spies.

Note:

  1. Ideally, to mock the output provided by console.log(), sinon mocks must be used. But, it is observed that doing so produces erratic behavior. Hence the log() function of console is stubbed out using sinon stubs. This is the only place where stubs are used in unit tests. An example of how to do so is present in the stubs section of the sinon page.
  2. The unit tests for the controllers stub the logger dependency instead of mocking them because of the erratic behavior of caporal, upon mocking of the logger, when it binds to the logger.

References

  1. Ch 11: Using Test Doubles, xUnit Test Patterns, Gerard Meszaros, Addison-Wesley, 2007.
  2. Ch 16: Mocking and Stubbing, Test-Driven JavaScript Development, Christian Johansen, Addison-Wesley, 2010.
  3. Ch 17: Writing Good Unit Tests, Test-Driven JavaScript Development, Christian Johansen, Addison-Wesley, 2010.
  4. Ch 3: Test Doubles, Effective Unit Testing, Lasse Koskela, Manning Publications, 2013.