The history of test doubles

Last updated Aug 30, 2024 Published Nov 5, 2023

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

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
Kent Beck in his iconic book about TDD named "TDD by Example" already set the bar for what we know today as mocks.

eXtreme programming has TDD embeded into its practices, which leads to having it as a reference guide in regards to test doubles. For XP teams, understanding the differences and how to use them become a key difference in comparasion with non-extreme teams.

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

Mezaros wrote XUnit patterns that is a reference guide for the field of test code. In his book he elaborates on the approach of refactoring and maintaining test code and start a discussion on the effects that bad test code has. This books is recommended for any developer.

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

For TDD focused content you can check this dedicated page with detailed information about the practice from beginners to advanced concepts.

Test doubles

Test Doubles are essential tools in software testing, enabling developers to isolate and control the behavior of external dependencies. They are commonly used in Test-Driven Development (TDD) and come in various forms such as Dummies, Fakes, Stubs, Mocks, and Spies. This section will delve into the world of Test Doubles, providing definitions, examples, and use cases to enhance your understanding and application of these testing tools.

Dummy

Dummies are test doubles that have no implemented behavior but are used to fill method arguments. They can be useful when a method requires an object to be passed but does not use it

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

Fakes are test doubles that implement the same interface as the production code but provide canned or simplified responses. They are useful when the real implementation is slow or resource-intensive. For example, a fake email service could always return a success response, bypassing the need for actual email sending or a authenticated user that always return true or false regardless of its credentials.

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

Stubs are test doubles that return predefined or canned responses to calls made during the test. They are useful when you want to isolate a unit of work and control the behavior of a specific function or method.

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

Mocks are test doubles that record method calls, arguments, and return values, allowing for verification of specific behavior during the test.

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

Spies are a type of test double used in unit testing to monitor and record the interactions of a method within a system, allowing verification of method calls and parameters after the fact. Spies are particularly useful when you want to verify the behavior of a system after execution without pre-defining expectations, offering a flexible and less intrusive approach to verifying interactions.

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')
    });
});

Spies are used to check interactions without changing the original code behaviour.

Spies vs Mocks?

Unlike mocks, which require expectations to be set before execution, spies allow you to assert on behavior after the fact, making them ideal for scenarios where the exact flow isn’t known upfront

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 (https://web.archive.org/web/20230822035303/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