Clean Swift TDD Part 3 – Worker

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 the fetchOrders() method.
  • Add an ordersStore variable of type OrdersStoreProtocol to the OrdersWorker class.
  • Add an initializer to with an ordersStore as an argument.

Your OrdersWorker.swift file should now look like:

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:

Create a new OrdersCoreDataStore.swift file:

Create a new OrdersAPI.swift file:

Later, it’ll be easy to swap between these three data stores, as follows:

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:

And ListOrdersInteractorTests:

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.

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.

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:

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 single fetchOrders() method.
  • Next, you created the three data stores: (1) OrdersMemStore, (2) OrdersCoreDataStore, and (3) OrdersAPI. They all conform to the OrdersStoreProtocol.
  • 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 on ordersStore, you created the OrdersMemStoreSpy. You use GCD to simulate returning orders asynchronously.
  • You wrote the testFetchOrdersShouldReturnListOfOrders() test to verify the data store’s fetchOrders() 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.

Raymond
I've been developing in iOS since the iPhone debuted, jumped on Swift when it was announced. Writing well-tested apps with a clean architecture has been my goal.

13 Comments

  1. Is there a reason why Swift can’t downcast sut.ordersStore to a OrdersMemStoreSpy type? My tests won’t pass because of this.

    1. Nevermind, I was creating a worker using OrdersMemStore instead of OrdersMemStoreSpy. My bad.

    2. 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 the OrdersStoreProtocol can be assigned to sut.ordersStore.

  2. 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?

    1. Hi Aleksander,

      The ListOrdersWorker is intended for more complex business logic specific to the ListOrders scene. The OrdersWorker 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 shared OrdersWorker so as to not having to duplicate it elsewhere. Does it make sense?

      1. 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.

  3. 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).

    1. 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.

  4. 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?

    1. 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.

  5. Hi, I’m having issues with OrdersMemStoreSpy and the completionHandler needing @escaping (which then breaks the interface).

    Thoughts?

Leave a Comment

Your email address will not be published. Required fields are marked *