Guy Yogev

Nov 15, 2017

4 min read

Test Doubles For Dummies

Test Doubles is a generic term for any kind of pretend object used in place of a real object for testing purposes.

Doubles comes on many shapes and forms, and often confused between one another. What is the difference between a dummy/mock/faker/stub? well it doesn’t really matter as long as the terminology is agreed between the team members.

At the end, Doubles are tools. The trick is to use the right one for each task.

First, we’ll define the terminology. We’ll use Jasmine, which by its nature spies, stubs & mocks are very similar, but we'll try to pin point the differences.

Dummies

describe('Vet', () => {
let subject;
beforeEach(() => {
subject = new Vet()
})

it('test with dummy', () => {
const dog = {};
expect(subject.measureTemperature(dog)).toEqual('normal');
})
})

Spies

describe("Dog", () => {
let subject;
beforeEach(() => {
subject = new Dog();
})

describe('testing with spies', () => {
let result;
beforeEach(() => {
spyOn(subject, 'bark');
result = subject.speak();
})

it('should bark when speaking', () => {
expect(subject.bark).toHaveBeenCalled();
})

it('should bark loudly when speaking', () => {
expect(subject.bark).toHaveBeenCalledWith(true);
})

it('should bark only once', () => {
expect(subject.bark.calls.count()).toEqual(1);
})

it('should not call the real bark method', () => {
expect(result).toBe(undefined);
})
})
})

Stub

describe('testing with stubs', () => {
beforeEach(() => {
spyOn(subject, 'bark').and.returnValue('meow');
});

it('should meow instead of woff', () => {
const result = subject.speak();
expect(result).toEqual('meow');
});
})

Mocks

describe('testing with mocks', () => {
beforeEach(() => {
spyOn(subject, 'stand').and.callFake(() => {
this.sitting = true;
});
});

it('sit even when stand was called', () => {
subject.sitting = true;
subject.play();
expect(subject.sitting).toEqual(true);
});
})

Fakers

class FakeDog {
constructor() {}
bark() { return "I'm barking"}
sit() { return "I'm sitting" }
stand() { return "I'm staging" }
speak() { return "I'm speaking" }
play() { return "I'm playing" }
}

describe('Dog API', () => {
let subject;
beforeEach(() => {
subject = new FakeDog();
})

it('should have methods', () => {
expect(subject.bark).toBeDefined();
expect(subject.sit).toBeDefined();
expect(subject.stand).toBeDefined();
expect(subject.speak).toBeDefined();
expect(subject.play).toBeDefined();
})
})

describe('Vet', () => {
let subject;
beforeEach(() => {
subject = new Vet()
})

it('test with dummy', () => {
const fakeDog = new FakeDog();
expect(subject.measureTemperature(fakeDog)).toEqual('normal');
})
})

When to use what?!

Prefer state testing over interactions testing

At most cases, testing state is better.

  • Testing interactions doesn’t necessarily tell you that your code is working properly.
  • Testing interactions leads to tests that are harder to maintain.

For example

F = () {
a()
b()
c()
}
  • Testing that F calls a, b & c will pass even if the inner functions are broken, making the test very weak.
  • refactoring a() into betterA() will cause F tests to fail.

Therefore, spies should be used with care.

You’ll need a very good reason to choose interaction over state testing. Here are a few:

  1. Its impossible to test state. For example, you wish to test the interaction with an external module, that doesn’t have a fake.
  2. Triggering the real method doesn’t change the state at all. For example, a method that sends an email.
  3. Triggering the real method is expensive. For example, method that writes 1GB of data into the file system.

Additional notes:

  • If you find your system has a large amount of interaction tests, it would be a good idea to have good coverage of integration tests (such as E2E), that will validate the actual state changes.
  • Prefer testing how the spy was called and not if it was called. expect(subject.bark).toHaveBeenCalled() is weaker than expect(subject.bark).toHaveBeenCalledWith(true).

Prefer real objects over Mocks/Stubs/Fakers

As a rule of thumb, go from the easy to complex. real < stub <= mock < fake

  • If the real object is fast and deterministic — use it.
  • If the real object isn’t fast/deterministic, but simple to interact with — stub/mocks it.
  • If the real object isn’t fast/deterministic, and is hard to interact with — fake it.

Fakers maintenance

  • The team that creates the production module, should be the one that maintain the faker.
  • When creating a faker, it should usually be created at the lowest level possible. For example prefer faking a whole DB over faking all classes that access it.

Originally published at https://guyogev.com on November 15, 2017.