In the previous article, we’ve already talk about Test Double in general. In this article, we will talk about one of test double, called Spy.

Testing a component can be helped with another collaborator component with predefined behavior and  collecting information. This is called spying.

💡  Spies are stubs that also record some information based on how they were called. One form of this might be an email service that records how many messages it was sent. – Martin Fowler

Let’s create a Spy.

Creating a Spy

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 collaborator component, the spy, using the same interface of TransactionCacheStore.

So, from the client point of view, we want to create the SUT instance with Spy as the collaborator, like this :

func test_execute_returnNonEmptyTransactions() {
  let store = TransactionStoreSpy()
  let sut = LoadTransactionsFromLocalUseCase(store: store)
  
  // do something for testing ...
}

In the code above, we create our test instance, the LoadTransactionsFromLocalUseCase with store that is actually a Spy, as the dependency, or a collaborator and inject it on the constructor. So, when we call the sut.execute(), we should able to interact with the store spy, collecting information, or do something else.

Let’s implement the spy

class TransactionCacheStoreSpy: TransactionCacheStore {
  private(set) var loadTransactionsCallCount = 0
  private(set) var loadTransactionsCompletions = [Result<[Transaction], Error>) -> Void]()

  func loadTransactions(completion: (Result<[Transaction], Error>) -> Void) {
    loadTransactionsCallCount += 1
    loadTransactionsCompletions.append(completion)
  }

  func complete(with items: [Transaction]) {
    loadTransactionsCompletions[0](.success(items))
  }
}

This implementation is a spy has behavior as following :

  • Collecting the event using by counting execution, stored inloadTransactionsCallCount
  • Collecting the completions, so that it can be triggered to complete later inside the loadTransactionsCompletions.
  • Adding the complete(with items:) API, so that we can trigger the completion on the test with the desired result.

To elaborate more, this implementation is a Spy, meaning, it actually a stub, which has behavior that with also record event, or any information, 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. The record event functionality is important here, because we are going to use it on the assertion, since it is actually “spying” the event.

This spy test double helps our test for LoadTransactionsFromLocalUseCase component, since it can helps us identify called event using count, or any other methods, like array of enum of custom events. A Spy is also a stub, so it is better to provide some API to defined the behavior that you want, in this case, we want to complete the closure with some array of Transactions , which is resemble the success case.

In the complete test function, we can use the spy like this :

func test_execute_returnNonEmptyTransactions() {
  let store = TransactionStoreSpy()
  let sut = LoadTransactionsFromLocalUseCase(store: store)
  let expectedTransactions = nonEmptyTransactions()
  var receivedTransactions = [Transaction]()

  sut.execute() { result in
    switch result {
      case let .success(transactions):
        receivedTransactions = transactions
      case .failure:
        // do something else
    }
  }
  store.complete(with: expectedTransactions)
  
  
  XCTAssertEqual(store.loadTransactionsCallCount, 1) // Expecting load called once
  XCTAssertEqual(receivedTransactions, expectedTransactions) // Expecting no modification for items
}

Here, we can see that stubs plays well in this scenario. We want to make sure that there is no double calling the load functions from the store by checking the call count. Because a Spy is also a Stub, then we can stub in on the go, by triggering the success case with given items.

We can also improve the test, or add another test, to make sure that the SUT instance is not calling the load transactions from the store collaborator on it’s creation, like below :

func test_init_doesNotLoadTransactions() {
  let store = TransactionStoreSpy()
  _ = LoadTransactionsFromLocalUseCase(store: store)

  XCTAssertEqual(store.loadTransactionsCallCount, 0)
}

In this example, we can guarantee that our SUT is not having an auto-load behavior on the creation, if only we don’t want it happen.

To make it readable, we can adopt an enum as the message and assert it, instead of counting the int.

func test_init_loadTransactionsOnInit() {
  let store = TransactionStoreSpy()
  _ = LoadTransactionsFromLocalUseCase(store: store)

  XCTAssertEqual(store.messages, [ .loadTransactions ])
}

Conclusion

Spy is one of test double, that can helps you to test a component’s behavior and also getting the information, usually the flow’s logic of the SUT, without having a real or production implementation of its collaborator. This test double component provides a way to access the event, or collecting data when we need to check the event is being called, or not being called. It is also provides a way to inject the behavior configured so that the SUT component will responds based on the injected values. Spy defines answer or return and also capturing values others. In other word, a Spy is a Stub with capturing values functionality.

Reference : Dependency Injection Principles, Practices, and Patterns by Steven van Deursen & Mark Seemann Test Double (Martin Fowler) https://martinfowler.com/bliki/TestDouble.html

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s