In this post, you’ll complete the VIP cycle by testing the presenter for the fetch orders use case. You can read about my previous posts on testing the view controller, interactor, and worker.
The key takeaway here in this post is this. The job of the presenter is to format data for display to the user.
Our Order
model’s date attribute is an NSDate
and total is an NSDecimal
. But the labels in the table view cell we use to display information to the user take String
s.
Your view controller and views do not need to know how the date and total are represented in the app. They just need some strings to display to the user. You’ll see how to format the order date and order total in the presenter, such that your view controller is agnostic to the data representation.
If you later decide to store the order date and total differently, you just have to modify the presenter while the view controller can remain intact. And it’ll be easy to unit test because there’s a clear input and output.
You can even extract the formatter to higher level so that it can be reused in the rest of the app where you need to display the order date and total in the same format. It is a great feeling to know that you just need to find the formatter and make changes in one place while you’re well covered with unit tests.
Isolate the dependencies
You are testing the ListOrdersPresenter
and it has one external dependency – ListOrdersPresenterOutput
. So let’s mock it out by creating ListOrdersPresenterOutputSpy
.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
class ListOrdersPresenterOutputSpy: ListOrdersPresenterOutput { // MARK: Method call expectations var displayFetchedOrdersCalled = false // MARK: Argument expectations var listOrders_fetchOrders_viewModel: ListOrders_FetchOrders_ViewModel! // MARK: Spied methods func displayFetchedOrders(viewModel: ListOrders_FetchOrders_ViewModel) { displayFetchedOrdersCalled = true listOrders_fetchOrders_viewModel = viewModel } } |
You want to make sure when you invoke the presenter’s presentFetchedOrders()
method, the output’s displayFetchedOrders()
method is invoked as a result. If that’s the case, the displayFetchedOrdersCalled
variable will be set to true.
You also want to make sure the presenter is formatting the order properly for display. So let’s save the view model passed in to the displayFetchedOrders()
method to the listOrders_fetchOrders_viewModel
variable. You can write assertions to inspect it in the Then of your test case.
Okay, but how should the ListOrders_FetchOrders_ViewModel look like?
Remember, you want to make your view controller agnostic of the data representation of an order in your app. So you don’t want any reference to Order
, NSDate
, or NSDecimal
. The labels take String
s. So let’s create a new DisplayedOrder
struct to represent an order for display.
1 2 3 4 5 6 7 8 9 10 11 12 13 |
struct ListOrders_FetchOrders_ViewModel { struct DisplayedOrder { var id: String var date: String var email: String var name: String var total: String } var displayedOrders: [DisplayedOrder] } |
The DisplayedOrder
struct is only needed in the view model, so you can just stick it inside the ListOrders_FetchOrders_ViewModel
struct. Thanks Swift!
What order information will a user likely need to know? That’s what you’ll put in DisplayedOrder
If your Order
model contains data such as fulfillment date, warehouse location, lot number, …etc, refrain from putting them in DisplayedOrder
. A user doesn’t need to know how the business operates internally.
I also use name instead of separate first and last names in DisplayedOrder
since formatting names is the job of the presenter. You’ll likely do name
= firstName
+ a space + lastName
in your presenter when you format an Order
into a DisplayedOrder
. But let’s not jump too far ahead. Let’s write your test first in the spirit of TDD.
Finally, you are fetching a list of orders, so make displayedOrders
an array of DisplayedOrder
in the ListOrders_FetchOrders_ViewModel
.
Write the test first
You want to test for two things with the presenter’s presentFetchedOrders()
method:
- Format fetched orders for display – convert
[Order]
into[DisplayedOrder]
. - Ask the view controller to display the formatted orders – invoke the
displayFetchedOrders()
method on the output.
Let’s write the first test now:
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 |
func testPresentFetchedOrdersShouldFormatFetchedOrdersForDisplay() { // Given let listOrdersPresenterOutputSpy = ListOrdersPresenterOutputSpy() sut.output = listOrdersPresenterOutputSpy let dateComponents = NSDateComponents() dateComponents.year = 2007 dateComponents.month = 6 dateComponents.day = 29 let date = NSCalendar.currentCalendar().dateFromComponents(dateComponents)! let orders = [Order(id: "abc123", date: date, email: "amy.apple@clean-swift.com", firstName: "Amy", lastName: "Apple", total: NSDecimalNumber(string: "1.23"))] let response = ListOrders_FetchOrders_Response(orders: orders) // When sut.presentFetchedOrders(response) // Then let displayedOrders = listOrdersPresenterOutputSpy.listOrders_fetchOrders_viewModel.displayedOrders for displayedOrder in displayedOrders{ XCTAssertEqual(displayedOrder.id, "abc123", "Presenting fetched orders should properly format order ID") XCTAssertEqual(displayedOrder.date, "6/29/07", "Presenting fetched orders should properly format order date") XCTAssertEqual(displayedOrder.email, "amy.apple@clean-swift.com", "Presenting fetched orders should properly format email") XCTAssertEqual(displayedOrder.name, "Amy Apple", "Presenting fetched orders should properly format name") XCTAssertEqual(displayedOrder.total, "$1.23", "Presenting fetched orders should properly format total") } } |
First, you want to use the ListOrdersPresenterOutputSpy
you created earlier in place of the actual output.
Next, create a known date, then an array of one test order, and a ListOrders_FetchOrders_Response
object with the test order array.
When you invoke the presentFetchedOrders()
method with this response, you can now iterate on the displayedOrders
in the view model to make sure the id, date, email, name, and total are formatted properly as strings for display to the user.
Let’s move on to the second test:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
func testPresentFetchedOrdersShouldAskViewControllerToDisplayFetchedOrders() { // Given let listOrdersPresenterOutputSpy = ListOrdersPresenterOutputSpy() sut.output = listOrdersPresenterOutputSpy let orders = [Order(id: "abc123", date: NSDate(), email: "amy.apple@clean-swift.com", firstName: "Amy", lastName: "Apple", total: NSDecimalNumber(string: "1.23"))] let response = ListOrders_FetchOrders_Response(orders: orders) // When sut.presentFetchedOrders(response) // Then XCTAssert(listOrdersPresenterOutputSpy.displayFetchedOrdersCalled, "Presenting fetched orders should ask view controller to display them") } |
Like the first test, you set up the ListOrdersPresenterOutputSpy
and the ListOrders_FetchOrders_Response
with an array of one test order.
When you invoke the presentFetchedOrders()
method, you simple want to make sure the ListOrdersPresenterOutputSpy
’s displayFetchedOrders()
method is invoked. This’ll ask the view controller to display the orders you have just verified to be properly formatted in the first test.
Draw the boundary
The boundary is again pretty simple. You just need to add the displayFetchedOrders()
method to both the ListOrdersPresenterOutput
and ListOrdersViewControllerInput
protocols.
1 2 3 4 5 6 7 8 9 10 |
protocol ListOrdersPresenterOutput: class { func displayFetchedOrders(viewModel: ListOrders_FetchOrders_ViewModel) } protocol ListOrdersViewControllerInput { func displayFetchedOrders(viewModel: ListOrders_FetchOrders_ViewModel) } |
For now, just add an empty implementation of displayFetchedOrders()
in ListOrdersViewController
to make the compiler happy. You should focus on testing the presenter and not worry about the details of how to display an order to the user.
1 2 3 4 |
func displayFetchedOrders(viewModel: ListOrders_FetchOrders_ViewModel) { } |
Implement the logic
You wrote tests to make sure the presenter:
- Format the orders for display
- Ask the view controller to display the orders
Let’s satisfy those assertions now:
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 |
class ListOrdersPresenter: ListOrdersPresenterInput { weak var output: ListOrdersPresenterOutput! let dateFormatter: NSDateFormatter = { let dateFormatter = NSDateFormatter() dateFormatter.dateStyle = .ShortStyle dateFormatter.timeStyle = NSDateFormatterStyle.NoStyle return dateFormatter }() let currencyFormatter: NSNumberFormatter = { let currencyFormatter = NSNumberFormatter() currencyFormatter.numberStyle = .CurrencyStyle return currencyFormatter }() // MARK: Presentation logic func presentFetchedOrders(response: ListOrders_FetchOrders_Response) { var displayedOrders: [ListOrders_FetchOrders_ViewModel.DisplayedOrder] = [] for order in response.orders { let date = dateFormatter.stringFromDate(order.date!) let total = currencyFormatter.stringFromNumber(order.total!) let displayedOrder = ListOrders_FetchOrders_ViewModel.DisplayedOrder(id: order.id!, date: date, email: order.email!, name: "\(order.firstName!) \(order.lastName!)", total: total!) displayedOrders.append(displayedOrder) } let viewModel = ListOrders_FetchOrders_ViewModel(displayedOrders: displayedOrders) output.displayFetchedOrders(viewModel) } } |
It’s expensive to create NSDateFormatter
and NSNumberFormatter
, so let’s create the dateFormatter
and currencyFormatter
constants using custom getters as you need them.
In the presentFetchedOrders()
method, you want to iterate on the orders
in the response from the interactor. For each order, you’ll convert the date from NSDate
to String
, and total from NSDecimalNumber
to String
. You should also format the name to be a concatenation of the first name, a space, and last name.
Next, create a DisplayedOrder
and append it to the displayedOrders
array.
The other thing you want to do in the presentFetchedOrders()
method is to create the ListOrders_FetchOrders_ViewModel
with the displayedOrders
array. Finally, you can simply invoke the displayFetchedOrders()
method with the view model on the output.
Recap
Here’s what you accomplished in this post.
- First, you isolated the dependency by creating the
ListOrdersPresenterOutputSpy
. - Next, you define the
ListOrders_FetchOrders_ViewModel
to be an array ofDisplayedOrder
to pass type-agnostic fetched orders to the view controller for display. TheDisplayedOrder
struct contains justString
s ready to be used byUILabel
s. - You wrote the
testPresentFetchedOrdersShouldFormatFetchedOrdersForDisplay()
test to make sure fetched orders are properly formatted. - You also wrote the
testPresentFetchedOrdersShouldAskViewControllerToDisplayFetchedOrders()
test to make sure thedisplayFetchedOrders()
method is invoked on the output. - In the
presentFetchedOrders()
method implementation, you use theNSDateFormatter
andNSNumberFormatter
to format the order date and total, respectively. The result is an array ofDisplayedOrder
consisting of only strings suitable for display usingUILabel
s.
In the process, you added the displayFetchedOrders()
method in ListOrdersViewController
. But it’s currently empty. This means fetched orders are formatted for display but not actually being displayed. Let’s tackle this in the next post.
You can find the full source code with tests at GitHub.