In part 1 and part 2, you tested the ListOrdersViewController
and ListOrdersInteractor
, respectively. But we left it kind of hanging because we mocked out the OrdersWorker
.
In this post, you’ll test the OrdersWorker
to make sure it returns the correct orders.
But first, let’s abstract away the actual details of fetching orders from a data store. You’ll develop the following data stores in the process:
- Memory store
- Core Data store
- API store
The OrdersWorker
can use any of these data stores. It is agnostic to the type of data store you choose to use.
Let the data stores do the heavy lifting
In a production app, orders can be stored in Core Data or over the network. However, for development and running tests as fast as possible, you don’t want to fetch orders from Core Data or the network. Instead, you want to create a memory store. Memory is the fastest storage there is!
Let’s first design the OrdersWorker
with a common API such that swapping data stores becomes trivial.
In OrdersWorker.swift
:
- Add an
OrdersStoreProtocol
and declare thefetchOrders()
method. - Add an
ordersStore
variable of typeOrdersStoreProtocol
to theOrdersWorker
class. - Add an initializer to with an
ordersStore
as an argument.
Your OrdersWorker.swift
file should now look like:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
protocol OrdersStoreProtocol { func fetchOrders(completionHandler: (orders: [Order]) -> Void) } class OrdersWorker { var ordersStore: OrdersStoreProtocol init(ordersStore: OrdersStoreProtocol) { self.ordersStore = ordersStore } func fetchOrders(completionHandler: (orders: [Order]) -> Void) { completionHandler(orders: []) } } |
Next, create these three data stores, and make them conform to the OrdersStoreProtocol
you just defined. Just add an empty fetchOrders()
method for now.
Create a new OrdersMemStore.swift
file:
1 2 3 4 5 6 7 |
class OrdersMemStore: OrdersStoreProtocol { func fetchOrders(completionHandler: (orders: [Order]) -> Void) { } } |
Create a new OrdersCoreDataStore.swift
file:
1 2 3 4 5 6 7 |
class OrdersCoreDataStore: OrdersStoreProtocol { func fetchOrders(completionHandler: (orders: [Order]) -> Void) { } } |
Create a new OrdersAPI.swift
file:
1 2 3 4 5 6 7 |
class OrdersAPI: OrdersStoreProtocol { func fetchOrders(completionHandler: (orders: [Order]) -> Void) { } } |
Later, it’ll be easy to swap between these three data stores, as follows:
1 2 3 4 |
OrdersWorker(ordersStore: OrdersMemStore()) OrdersWorker(ordersStore: OrdersCoreDataStore()) OrdersWorker(ordersStore: OrdersAPI()) |
This technique of passing a different object that conforms to the same protocol as required by the OrdersWorker
’s initializer (or constructor) is called constructor dependency injection.
The details of how to actually fetch orders in each data store is specific and internal to the implementation of the data stores themselves. The OrdersWorker
is just a consumer of the OrdersStoreProtocol
API.
Some housekeeping before TDD
You just changed the way an OrdersWorker
object is instantiated. So, let’s go back to fix the ListOrdersInteractor
to make ordersWorker
use the OrdersMemStore
for testing purpose:
1 2 |
var ordersWorker = OrdersWorker(ordersStore: OrdersMemStore()) |
And ListOrdersInteractorTests
:
1 2 |
let ordersWorkerSpy = OrdersWorkerSpy(ordersStore: OrdersMemStore()) |
Now your project should compile again.
Isolate the dependencies
As always, it is very useful to first identify and isolate any dependency. You are testing the OrdersWorker
and its ordersStore
variable points to an external data store. Whether it is in memory, Core Data, or over the network, it is a dependency. Let’s mock it out.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
class OrdersMemStoreSpy: OrdersMemStore { // MARK: Method call expectations var fetchedOrdersCalled = false // MARK: Spied methods override func fetchOrders(completionHandler: (orders: [Order]) -> Void) { fetchedOrdersCalled = true let oneSecond = dispatch_time(dispatch_time_t(DISPATCH_TIME_NOW), 1 * Int64(NSEC_PER_SEC)) dispatch_after(oneSecond, dispatch_get_main_queue(), { completionHandler(orders: [Order(), Order()]) }) } } |
To simulate the asynchronous (background and non-blocking) nature of fetching orders, let’s use GCD to postpone invoking the completionHandler
by 1 second.
The fetchOrders()
method returns immediately. After 1 second, the completionHandler
will fire in the main queue. We’ll just create a couple orders and return them as an array.
This OrdersMemStoreSpy
goes inside the OrdersWorkerTests
class, which you’ll see next.
Write the test first
In the last post, you mocked out the OrdersWorker
to test the ListOrdersInteractor
. Now it’s time to TDD the OrdersWorker
to make it fetch some real orders from the data store.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 |
import XCTest class OrdersWorkerTests: XCTestCase { // MARK: Subject under test var sut: OrdersWorker! // MARK: Test lifecycle override func setUp() { super.setUp() setupOrdersWorker() } override func tearDown() { super.tearDown() } // MARK: Test setup func setupOrdersWorker() { sut = OrdersWorker(ordersStore: OrdersMemStoreSpy()) } // MARK: Test doubles class OrdersMemStoreSpy: OrdersMemStore { // MARK: Method call expectations var fetchedOrdersCalled = false // MARK: Spied methods override func fetchOrders(completionHandler: (orders: [Order]) -> Void) { fetchedOrdersCalled = true let oneSecond = dispatch_time(dispatch_time_t(DISPATCH_TIME_NOW), 1 * Int64(NSEC_PER_SEC)) dispatch_after(oneSecond, dispatch_get_main_queue(), { completionHandler(orders: [Order(), Order()]) }) } } // MARK: Tests func testFetchOrdersShouldReturnListOfOrders() { // Given let ordersMemStoreSpy = sut.ordersStore as! OrdersMemStoreSpy // When let expectation = expectationWithDescription("Wait for fetched orders result") sut.fetchOrders { (orders: [Order]) -> Void in expectation.fulfill() } // Then XCTAssert(ordersMemStoreSpy.fetchedOrdersCalled, "Calling fetchOrders() should ask the data store for a list of orders") waitForExpectationsWithTimeout(1.1) { (error: NSError?) -> Void in XCTAssert(true, "Calling fetchOrders() should result in the completion handler being called with the fetched orders result") } } } |
In the setupOrdersWorker()
method, you set up the subject under test, an instance of OrdersWorker
, to use the OrdersMemStoreSpy
you wrote at the beginning of this post.
In the Given, you first get a reference to this spy so you can inspect it later when you write your assertions.
In the When, you’ll use XCTest’s asynchronous testing support. First, make a call to the expectationWithDescription()
method. Then, invoke the fetchOrders()
method. Inside the block, call the fulfill()
method to fulfill the expectation.
In the Then, as usual, you verify the fetchOrders()
method was called. You also invoke waitForExpectationsWithTimeout()
to wait 1.1 seconds (just a bit longer than the dispatch_after
in OrdersMemStoreSpy
) for the expectation to be fulfilled. If the expectation is fulfilled, it means the completion handler is properly executed. Exactly what you want.
Implement the logic
The test is currently failing because the OrdersWorker
is calling the completion handler immediately and return an empty array of orders. No orders!
Let’s make it pass.
Modify the OrdersWorker
’s fetchOrders()
method to use the new ordersStore
variable to fetch orders from the data store as follows:
1 2 3 4 5 6 7 |
func fetchOrders(completionHandler: (orders: [Order]) -> Void) { ordersStore.fetchOrders { (orders: [Order]) -> Void in completionHandler(orders: orders) } } |
Instead of calling the completionHandler
immediately and return an empty array of orders, you invoke the fetchOrders()
method on ordersStore
. When fetchOrders()
completes, you call the completionHandler
inside the fetchOrders()
method’s block.
Also, modify the completionHandler
call to return the orders
result as returned by the ordersStore
.
Run the test now and it should pass. All the code and test for this TDD example can be found on GitHub.
Recap
We broke out of the VIP cycle a bit and steer right into the worker. We then use TDD to drive the development of the OrdersWorker
.
- First, you defined a simple API contract – the
OrdersStoreProtocol
with a singlefetchOrders()
method. - Next, you created the three data stores: (1)
OrdersMemStore
, (2)OrdersCoreDataStore
, and (3)OrdersAPI
. They all conform to theOrdersStoreProtocol
. - The
OrdersWorker
is then free to pick one of these three data stores to use with the help of constructor dependency injection. - To isolate the
OrdersWorker
’s dependency onordersStore
, you created theOrdersMemStoreSpy
. You use GCD to simulate returning orders asynchronously. - You wrote the
testFetchOrdersShouldReturnListOfOrders()
test to verify the data store’sfetchOrders()
method is invoked. - You also use XCTest’s asynchronous testing support to create and wait for an expectation to be fulfilled. When it’s fulfilled, you know the data store’s
fetchOrders()
method has returned some orders.
In the next post, we’ll get back on the VIP cycle to finish off by testing the presenter using TDD.
Is there a reason why Swift can’t downcast sut.ordersStore to a OrdersMemStoreSpy type? My tests won’t pass because of this.
Nevermind, I was creating a worker using OrdersMemStore instead of OrdersMemStoreSpy. My bad.
Hi BossTweed,
The line
sut.ordersStore as! OrdersMemStoreSpy
isn’t a downcast.sut.ordersStore
isn’t a concrete type. It’s a protocol type. Any class conforming to theOrdersStoreProtocol
can be assigned tosut.ordersStore
.Hey Raymond,
First want to thank you about your great work and dedication to bring a better way of organizing and structuring our iOS projects. Back on what is bothering me. What is the reason behind not using some of the workers that are already in the project instead of making a new OrdersWorker class. Why didn’t you put anything inside the ListOrdersWorker?
Hi Aleksander,
The
ListOrdersWorker
is intended for more complex business logic specific to the ListOrders scene. TheOrdersWorker
is meant to be shared and reused across many scenes. As I have to list, create, show orders from different scenes, I chose to put this logic in the sharedOrdersWorker
so as to not having to duplicate it elsewhere. Does it make sense?Hi Raymond.
That makes perfect sense. From what I understand the ListOrdersWorker is intended for some logic specific to ListOrders use case. For example if the client has the option to sort the orders in some way, more complicated than just the date then I should put that logic in a worker to keep the clarity of the Interactor. Please correct me if I am wrong in my assumption.
Yes, you’re correct.
Thanks for your posts. My question is the following:
Why you invoke Worker call on the main thread? I think better solution, is invoke something on the main thread only into an presenter, in case, when we call method presenter.output protocol (something, that should be passed into ViewController for displaying, because all UI tasks should be invoked on main thread).
Hi Oleksandr,
If the work to be done is short and synchronous, there’s no need to use a background thread. Asynchronous vs synchronous and VIP cycle are separate considerations.
Thanks for this great post. I have got a question.
How do I nest workers?
Say for instance, using the OrdersWorker to fetch orders, one of the property for order is image over the network. I need to use Async call to download the image, how do I make this working with VIP?
Hi Ke,
It depends.
If your requirement states that you need to construct a complete order with image already fetched and available, you can fire the async calls to fetch the images immediately after the orders have been fetched but before you bubble the results up to the interactor and so forth.
However, if your requirement states that you can display the order details first while waiting for the images to be fetched and rendered in the views, you can bubble the results up to the view and fire the async calls to fetch the images when an order needs to be displayed to the user.
Hi, I’m having issues with OrdersMemStoreSpy and the completionHandler needing @escaping (which then breaks the interface).
Thoughts?
Hi Chris,
Swift 4 requires you to explicitly write
@escaping
. Check out the GitHub repo or the handbook for the latest working code examples.