Test Double : Stub

Arifin Firdaus
3 min readJan 9, 2022

In the last article, we’ve already talk about Test Double in general. In this article, we will talk about on of test double, called stub.

Testing a component can be helped with another collaborator component with predefined behavior. This is called stubbing.

💡 Stubs provide canned answers to calls made during the test, usually not responding at all to anything outside what’s programmed in for the test. — Martin Fowler

Let’s create a Stub.

Creating a Stub

Let say we have a component, a use case, LoadTransactionsFromRemoteUseCase. This use case will have a success case, and failure case as a return completion value.

protocol LoadTransactionsUseCase {
func execute(completion: @escaping (Result<[Transaction], Error>) -> Void)
}

And we have an implementation for that use case, named LoadTransactionsFromRemoteUseCase

class LoadTransactionsFromLocalUseCase: LoadTransactionsUseCase {
private let store: TransactionCacheStore

init(store: TransactionCacheStore) {
self.store = store
}

func execute(completion: @escaping (Result<[Transaction], Error>) -> Void) {
store.loadTransactions { result in
switch result {
case .success(let transactions):
completion(.success(transactions))
case .failure(let error):
completion(.failure(error))
}
}
}
}

We can create the non real with predefined store with the stub, using the same interface of TransactionCacheStore.

So, from the client point of view, we want to create with both success and fail store like this :

func test_execute_returnNonEmptyTransactions() {
let nonEmptyItems = createNonEmptyItems()
let storeStub = TransactionStoreStub(result: .success(nonEmptyItems))
let sut = LoadTransactionsFromLocalUseCase(store: storeStub)

// do something for testing ...
}

In the code above, we create our test instance, the LoadTransactionsFromLocalUseCase with the predefined behavior using the stub and inject it on the constructor. so, when we call the sut.execute() , we should get the success case with non empty items behavior.

Let’s implement the stub

class TransactionCacheStoreStub: TransactionCacheStore {
private let result: Result<[Transaction], Error>

init(result: Result) {
self.result = result
}
func loadTransactions(completion: (Result<[Transaction], Error>) -> Void) {
completion(result)
}
}

This implementation is a stub has behavior as following :

  • the success or failure behavior can both be injected
  • Using constructor or intializer injection, since it is the most preferred way

To elaborate more, this implementation is a stub, meaning, it has predefined behavior for unit testing purpose. The behavior can be both success and failure scenarios. If success, returns array of transaction. If failure, return any Error type.

This stub test double helps our test for LoadTransactionsFromLocalUseCase component, since it can returning a success case with empty item, with items, and with certain error without communicating to a real backend. Therefore, we can verify later that our system under test, or SUT, is able to returning all of those 3 cases correctly.

Conclusion

Stub is one of test double, that can helps you to test a component’s behavior, the SUT, without having a real or production implementation of its collaborator. This test double component provides a predefined value configured so that the SUT component will responds based on the Stub values. Stub only defines asnwer or return , not capturing values nor others.

Reference :

Dependency Injection Principles, Practices, and Patterns by Steven van Deursen & Mark Seemann

Test Double (Martin Fowler) https://martinfowler.com/bliki/TestDouble.html

--

--

Arifin Firdaus

Software Engineer — iOS | Apple Developer Academy Graduate | Brawijaya University | arifinfrds.com | LinkedIn : linkedin.com/in/arifin