Test Doubles: Understanding the Different Types and Their Role in Testing

Dummy, Fake, Stub, Spy, and Mock

Ahmad G. Sufi
6 min readMar 19, 2024

Introduction to Test Doubles:

Test Double is a generic term that describes objects that behave or look like the real objects our code depends on. Faking a server-layer response might be an excellent example of that. It is almost impossible to avoid the use of test doubles, especially in unit testing.

There are several types of test doubles, other than just a mock. Each one of them aims to solve a different part of our test isolation mission.

Dummy

A dummy object is not directly used in the test or code under test, but it is required for the creation of another object required in the code under test.. It’s an object that will never be used in the test. So why do we need it? Well, there are methods that, to initialize them, you have to pass an object from a certain type. In that case, you can create a new dummy class. The dummy can be either a subclass of the original object you need to pass or a new class that conforms to the relevant protocol:

class ManufactureDummy : Manufacture {}

class CarTests: XCTestCase {
func testCarMethod(){
let manufactureDummy = ManufactureDummy()
let car = Car(manufacture: manufactureDummy)
// rest of the test method
}
}

But this argument is not going to be used in our test, and it’s only there for code design purposes. We just want to create our car object and move on. So we create a dummy manufacturer, which is a subclass (or a protocol-based), and pass it to the car init() method.

We can say that dummies can help us initialize objects when we have to deal with a custom init() method.

Fake

Sure, we can say that every test double is a fake. But in this case, fake is an object that always returns the same value. A good example might be a network layer to fake a network response.

class LoginService {
var isLoggedIn : Bool {
return true
}
}
class FakeLoginService : LoginService {
override var isLoggedIn : Bool {
return true
}
}

The simple FakeLoginService always returns true in the isLoggedIn variable getter. You can inject this fake object in tests that require the user to be logged in when running your test.

Stub

Stub is a type of test double used to stand in for a real component or dependency in the system under test. Stubs provide predetermined responses to method calls made during testing, allowing developers to isolate the behavior of the component being tested from its dependencies. Stubs are typically used to simulate the behavior of complex or external components, such as databases, web services, or external APIs, in order to create controlled testing environments and verify the functionality of the system under different conditions.

// Protocol defining the dependency
protocol DataService {
func fetchData() -> String
}

// Class that depends on DataService
class DataManager {
let dataService: DataService

init(dataService: DataService) {
self.dataService = dataService
}

func processData() -> String {
return dataService.fetchData()
}
}

// Stub implementation of DataService for testing
class DataServiceStub: DataService {
func fetchData() -> String {
return "Stubbed Data"
}
}

// Usage example in a test case
import XCTest

class DataManagerTests: XCTestCase {

func testProcessData_withStubbedData_returnsStubbedData() {
// Arrange
let dataServiceStub = DataServiceStub()
let dataManager = DataManager(dataService: dataServiceStub)

// Act
let result = dataManager.processData()

// Assert
XCTAssertEqual(result, "Stubbed Data")
}
}
  • DataService is a protocol defining a method fetchData() to fetch data.
  • DataManager is a class that depends on DataService and has a method processData() that uses the data fetched from the service.
  • DataServiceStub is a stub implementation of DataService that always returns a predefined string ("Stubbed Data").
  • In the test case testProcessData_withStubbedData_returnsStubbedData, we create an instance of DataServiceStub and pass it to DataManager during setup. When processData() is called, it returns the stubbed data ("Stubbed Data"), which we assert against in the test.

Spy

A spy is a type of test double used in unit testing to observe the behavior of an object under test. Unlike stubs or mocks, which provide predetermined responses to method calls, a spy allows you to monitor interactions with the object being tested.

In simpler terms, a spy records information about how methods are called during a test, such as the number of times a method is invoked, the arguments passed to it, and the order of method calls. This information can then be used to verify whether the object under test behaves as expected.

Spies are particularly useful for verifying that certain methods are called with the correct arguments, or for tracking the sequence of method invocations within an object. They help ensure that an object interacts correctly with its collaborators or dependencies.

class LoginViewSpy : LoginViewProtocol {
var messageReceived = ""
func showMessage(message : String) {
messageReceived = message
}
}
func testLoginPresenter_whenTappedOnLoginButtonAndNoNetork_showError() {
// arrange
let loginPresenter = LoginPresenter()
let viewSpy = LoginViewSpy()
loginPresenter.view = viewSpy
// act
loginPresenter.onLoginButtonTapped()
// assert
let messageReceived = viewSpy.messageReceived
XCTAssertEqual(messageReceived, "Error. Please check your network")
}

In the preceding code, we have our LoginPresenter that needs to update its view.

Usually, its View is some kind of a UIViewController that conforms to LoginViewProtocol, but in this case, we created a spy. Just a regular class that conforms to the same protocol and the presenter doesn’t know he updates the Spy and not the real view controller. This Spy saves the message it receives in a variable, and later the method asserts and verifies the received message.

Spies are a widespread test double, and the inspection can go further — you can inspect the order of the calls or even how many calls were made.

Mock

Similar to a stub, but the behavior of the mocked interface can be changed dynamically based on scenarios. It is also similar to a spy, as it allows us to verify that a method was called.

However, the assertion is in the verify method in a mock. In a spy, the assertion is in the test.

A mock is a smarter stub. You verify your test passes through it. so you could make a mock that returns either success or failure, success depending on the condition that could be changed in your test case.

--

--