The history of test doubles

Last updated Nov 5, 2023 Published Nov 5, 2023

The content here is under the Attribution 4.0 International (CC BY 4.0) license

Kent Beck in his iconic book about TDD named “TDD by Example” already set the bar for what we know today as mocks.

Cover of the book "Extreme Programming Explained: Embrace Change, 2nd Edition (The XP Series) 2nd Edition"

Cover of the book "XUnit Test Patterns: Refactoring Test Code"

Today

Daily, developers deal with mocks, stubs, fakes, dummies and other types of test-doubles. Nevertheless, the word that is most used to describe all of them is “mock”.

Diving in the github code, blog posts and other kinds of resources the word that repeat the most is “mock”. Uncle bob suggests that this is because “mock that” and saying “mock” is easier than saying the specific types of mocks.

To that end, many libraries adopt only the mock word to refer to any kind of test double, for example, phpunit uses only two: Mock and Stub.

In kotlin land, the library used to build test doubles is named mockk - even in the name it already declares its focus. No distinction from the start. Therefore, mockk allows different behaviors that fits other types of test doubles.

It is understandable why practitioners often relate to “mocks” for the entire family of test doubles.

The classic way of thinking about test doubles is when someone needs to isolate external dependencies such as:

  • file system access
  • third party libraries
  • network access

Test doubles

Dummy

class AuthenticatedUser {
}

class MarsRover {
    constructor(user: AuthenticatedUser) { }

    execute(command: string) {
        return '0:0:E';
    }
}

describe('Dummy', () => {
    it('mars rover should turn right when command is R', () => {
       const rover = new MarsRover(new AuthenticatedUser());
       const lastPosition = rover.execute('R');

       expect(lastPosition).toEqual('0:0:E');
    });
});
  • user authentication (middleware)
  • The test don’t care how it is used.
  • usually it is required to make a given code to work, for example authentication.
  • parameters of a function

Fakes

class FakeAuthenticatedUser {
    constructor(public name: string) {}
}

class FakeMarsRover {
    constructor(private user: FakeAuthenticatedUser) { }

    execute(command: string) {

        if (this.user.name !== 'Jen') {
            return '0:0:E'
        }
        return '0:0:N';
    }
}

describe('Fake', () => {
    it('mars rover should turn right when user is Jose', () => {
        const rover = new FakeMarsRover(new FakeAuthenticatedUser('Jose'));
        const lastPosition = rover.execute('R');

        expect(lastPosition).toEqual('0:0:E');
    });

    it('user with name Jen should not move rover', () => {
        const rover = new FakeMarsRover(new FakeAuthenticatedUser('Jen'));
        const lastPosition = rover.execute('R');

        expect(lastPosition).toEqual('0:0:N');
    });
});

  • A working implementation of an external library, it should have the same behavior.
    • Sometimes libraries give fakes to help testing
    • For example an in memory database.
    • A specific rule from business can be used

Stubs

import axios from 'axios';

class Message {
    constructor(public position: string) {}
}

class RoverApiService {
    async fetchPosition(roverId: string): Promise<Message> {
        const response = await axios.get('http://localhost:42422');
        return new Message(response.data)
    }
}

class MarsRoverStub {
    constructor(private roverApi: RoverApiService) { }

    async fetchRemotePosition(): Promise<string> {
        const rover: Message= await this.roverApi.fetchPosition('my-rover')

        if (rover.position === '0:0:N')  {
            return 'waiting';
        }

        return '0:0:E';
    }
}

export class RoverApiServiceSuccess {
    async fetchPosition(roverId: string): Promise<Message> {
        return new Message('10:10');
    }
}

describe('Stubs', () => {
    it('mars rover should fetch current position from api', async () => {
        const rover = new MarsRoverStub(new RoverApiServiceSuccess());
        const position = await rover.fetchRemotePosition();

        expect(position).toEqual('0:0:E');
    });
});
  • call to an api with typescript (stub values from json)
  • Stubs returns hard coded values (predefined ones, success, error, or something different)
    • does not record the number of times it was called

Mocks

class PositionRepository {
    save(position: string) {}
}

class MarsRoverMock {
    constructor(private mockedRepository: PositionRepository) { }

    execute(command: string) {
        this.mockedRepository.save('0:0:E');
        return '0:0:E';
    }
}

describe('Mocks', () => {
    it('mars rover should store its position whenever a command is sent', () => {
        const mockedRepository = {
            save: jest.fn()
        }

        const rover = new MarsRoverMock(mockedRepository);
        rover.execute('R');

        expect(mockedRepository.save).toHaveBeenCalledWith('0:0:E');
    });
});

Add an example here (email service, database repository)

  • any code from bank kata for example
  • if something was called with given params and number x of times
  • can return values but usually it is secondary (it depends on the language)
  • mocks are not used to check result from values.
  • It is also normal to use many mocks to test code written without test first.

Spys

import axios from 'axios';

class Message {
    constructor(public position: string) {}
}

class RoverApiService {
    async fetchPosition(roverId: string): Promise<Message> {
        const response = await axios.get('http://localhost:42422');
        return new Message(response.data)
    }
}

class MarsRoverSpy {
    constructor(private roverApi: RoverApiService) { }

    async fetchRemotePosition(): Promise<string> {
        const rover: Message= await this.roverApi.fetchPosition('my-rover')

        if (rover.position === '0:0:N')  {
            return 'waiting';
        }

        return '0:0:E';
    }
}

export class RoverApiServiceSuccessSpy {
    async fetchPosition(roverId: string): Promise<Message> {
        return new Message('0:0:N');
    }
}

describe('Spy', () => {
    it('should verify the call to the api', async () => {
        const roverApiServiceSuccess = new RoverApiServiceSuccessSpy();

        const spyOn = jest.spyOn(roverApiServiceSuccess, 'fetchPosition');

        const rover = new MarsRoverSpy(roverApiServiceSuccess);
        const result = await rover.fetchRemotePosition();

        expect(spyOn).toHaveBeenCalled()
        // expect(result).toEqual('waiting')
    });
});

Tools

  • java
    • JUnit, TestNG, or Spock
    • Mockito, JMock, or PowerMock
    • Hamcrest or Fest-Assert
  • javascript
    • jest, mocha, chai

Outside-in

  • TDD by example Kent Beck - 2002
  • xUnit 2007 - (Test Double Patterns - page 522)
    • “My first and overriding piece of advice on this subject is: When there is any way to test without a database, test without the database!” - Gerard Meszaros, xUnit Test Patterns - chapter 13
  • Growing Object Oriented Software Guided By Tests - 2009 (page 230)
    • 2009 - http://www.mockobjects.com
      • the era before xp but in 1999
      • London group was discussing the effects that testing their code were having and adding getters and setters for testing felt wrong
      • the idea was to focus on the composition of components instead by Brad Crox - now it is known as dependency injection
      • Connextra started to testout the idea of no getters in the code
        • it forces delegation and composition
      • this lead to emerging discussions about the messages between objects
      • the refactoring done was showing some patterns with variables called expectedURL or expectedSomething
        • They decided to call that mock
      • Around same time Extreme Tuesday Club (XTC) was created
      • A paper to XP2000 was submitted with the new approach
      • Reception of the paper was mixed
      • Nat Price created in ruby the same approach
        • later on he ported the library to java and joined Connextra developers to add more features to the library
      • OOPLSA paper published realizing they were writing a language
      • Mock Roles not objects paper was published
  • https://www.codurance.com/publications/2015/05/12/does-tdd-lead-to-good-design
  • https://www.codurance.com/publications/2017/10/23/outside-in-design
    • The premise is that there is no reason fora code to exists if it is not used in production
    • Advantages
      • inside-out
        • early validation of business rules
        • uncover business inconsistencies
        • tackle to core of the system first
    • Disadvantages
      • waster effort
      • build things that are not needed
      • speculative development - start to think all possibilities that might not be used
      • hard to do vertical slices starting from the domain
      • rushed development, rush the front-end development after the back-end (or domain is done)
      • lack of collaboration between front-end teams and back-end teams
    • The front-end changes more often compared to the business rule in the back-end
    • reason for ui to change
      • aesthetic
      • behavioral
      • data - the information displayed to the end user

References