Testing View Controller – Part 1

Now that you’ve tested the interactor and presenter in the Clean Swift VIP cycle. It’s time to finish up with the star of the show.

Drum roll please… View Controller

You’re going to finally take down the massive view controller. Massive leads to a massive post. So I’ll break this post into two parts:

In part 1, you’ll see the familiar setup and spy. You’ll then write tests for the expiration date picker to make sure a date is formatted as desired. We’ll diverge to look at the new invisible UI component and how it comes into play when testing view controllers. Finally, you’ll finish up by writing tests for the shipping method picker.

In part 2, you’ll write tests to make sure the keyboard behave as expected in all edge cases in which the text fields become first responder. You’ll also learn how to make running tests the same as running the app. Finally, you’ll test to make sure the pickers are configured properly.

Let’s get started.

Create the XCTestCase for the view controller

In Xcode, choose File -> New -> File, then select Test Case Class. Enter CreateOrderViewControllerTests for class. Make sure the CleanStoreTests target is checked. Click Create. The CreateOrderViewControllerTests.swift file is created.

The CreateOrderViewController’s input and output protocols are defined as follows:

Setup and Spy

With the interactor and presenter, setting up the subject under test is as simple as instantiating them, like CreateOrderInteractor() and CreateOrderPresenter(). In fact, the Clean Swift configurator creates them in the same way before hooking them up to the VIP cycle for real use. They are just POSOPlain Old Swift Object – with no parent class.

However, CreateOrderViewController inherits, either directly or indirectly, from UIViewController. It is loaded from the storyboard instead of created programmatically. This loading does a lot of things behind the scenes. Building the view hierarchy. Setting up the responder chain. Displaying the view on the screen. Among others.

Simply instantiating a view controller with CreateOrderViewController() doesn’t really fully set it up, ready to be used or tested.

But if you were to do all these things manually, you would need to dig deep into Apple’s documentation on view and view controller life cycles.

Fortunately, you don’t have to do that. You can just load it from the storyboard when you are running tests as if you are running the app.

You’ve seen similar code when you segue from one view controller to another. First, you get a bundle and a storyboard. And then call the instantiateViewControllerWithIdentifier() method with an identifier which you give to your view controller you create in the storyboard. It is almost always a good idea to set the identifier to the view controller class name.

But there are two very, very important things happening on the last line:

  1. Asking for the view property of createOrderViewController causes the view to be loaded. The viewDidLoad() method is called as a result.
  2. After the view is loaded, all the IBOutlets are also set up and ready to be used in out tests. For example, you can assert that a text field outlet’s text equal to a string you expect.

Let’s go undercover

The CreateOrderViewControllerOutputSpy is very similar to CreateOrderPresenterOutputSpy.

When the formatExpirationDate() method is invoked, it records the fact that it is invoked. It also remembers the CreateOrder_FormatExpirationDate_Request argument for later inspection.

The only new thing here is the shippingMethods variable property being spied on. What is a property? It is just a getter method.

When does it expire?

Now that all the boring setup stuff is done, let’s write some tests.

You first create a view model with the original iPhone release date of “6/29/07”. Then invoke the displayExpirationDate() method with the view model as its argument. Finally, you make sure the expirationDateTextField IBOutlet’s text property contains the same date string.

The displayExpirationDate() method simply causes the expiration date to be displayed in the UI. It doesn’t invoke any method on its output. The flow of control ends here. There is nothing more to do.

In fact, the displayExpirationDate() method is the only method in the CreateOrderViewControllerInput protocol. And you’ve just tested it. But there are a lot more code in the view controller. So, are you done?

Nope. You aren’t done just yet.

You haven’t touched the shippingMethods variable and formatExpirationDate() method in the CreateOrderViewControllerOutput protocol. You certainly want to make sure the view controller somehow invokes them, right? Otherwise, why are they even there?

But the presenter doesn’t call any input methods to trigger them. And the VIP cycle stops at the displayExpirationDate() method. So, who call shippingMethods and formatExpirationDate()?

Introducing Sue Richards – the Invisible Woman

There is the Invisible Woman in the Fantastic Four. There is also an invisible component in Clean Swift.

  • V – View controller
  • I – Interactor
  • P – Presenter
  • U – User interface

It turns out there is one other component – the UI – in the VIP cycle. The UI only talks to the view controller and vice versa. It isn’t part of the VIP cycle, but plays an important role. It accepts user inputs such as touches and swipes, and passes these events to the view controller for handling.

Clean Swift - UI Component

  • The green line is the boundary between the UI and view controller.
  • The arrows indicate the flow of control: From the UI to the view controller back to the UI.
  • The input/output protocol pairs of the view controller and UI constitute the UI/view controller boundary.

Imagine the UI component has its own input and output protocols, and the view controller has another pair of UI-facing input and output protocols.

Think of the IBActions in the view controller are methods declared in the UI-facing input protocol. And IBOutlets in the view controller are variables declared in the UI-facing output protocol.

When the user taps a submit button, the UI invokes the submitButtonTapped() IBAction method in the view controller’s UI-facing input protocol.

When the view controller wants to update the text of a name label, it invokes a setter method named nameLabel.text = “Steve” in the view controller’s UI-facing output protocol.

This “invisible” UI component in the VIP cycle is the key for understanding and writing the remaining tests for CreateOrderViewController.

Picking an expiration date

You’ve now made the invisible UI component visible. You also know IBAction are in the view controller’s UI-facing input protocol. That means you need to test the expirationDatePickerValueChanged() method.

Let’s write the test now.

First, you instantiate the CreateOrderViewControllerOutputSpy and make it the output of your subject under test. Later, you can ask the spy for the results of the spy mission. You want to make sure a method is called when it is supposed to be called. And it is called with the argument you expect.

How do you invoke the method being tested?

You create an NSDate object of the original iPhone release date of “6/29/07”. Then set it to be the expiration date picker’s date property. Because you set the picker’s date programmatically, the expirationDatePickerValueChanged() IBAction method isn’t called. So you need to invoke it explicitly. The sender argument doesn’t really matter, so you can just pass self.

On to the assertions.

  1. When the spied formatExpirationDate() method is called, formatExpirationDateCalled is set to true. So you want to assert that is indeed true.
  2. You then peek inside the createOrder_formatExpirationDate_request that was captured when the method was called. You want to look at the actual date inside the request object and compare it to the expected date.

If both assertions are true, your test case passes.

You’ve just written a test to cover what should happen when the user interacts with the expiration date picker. But there is another picker. What about the shipping method picker?

Picking a shipping method

When the user interacts with the shipping method picker, what methods are invoked to handle these events? Unlike the expiration date picker, you don’t have any IBAction method to handle these events for the shipping method picker.

Fear not. The CreateOrderViewController conforms to the UIPickerViewDataSource and UIPickerViewDelegate protocols. The methods in these protocols work together to handle these events.

Specifically, the CreateOrderViewController implements the following methods:

  • numberOfComponentsInPickerView()
  • pickerView:numberOfRowsInComponent()
  • pickerView:titleForRow:forComponent()
  • pickerView:didSelectRow:inComponent()

These methods aren’t IBAction methods, but they are delegate methods of the UIPickerViewDataSource and UIPickerViewDelegate protocols. The view controller conforms to these protocols and takes on the responsibility of handling these events. So, these protocols are part of the view controller’s UI-facing input protocols now. This is how the UI communicates to the view controller about events triggered by the user. So you also need to write tests for them.

The UI-facing input protocol of the view controller contains IBAction methods and delegate methods of the protocols that the view controller conforms to.

You just need to remember this simple rule to know exactly which methods you need to write tests for in all your view controllers.

Let’s write these tests now. One by one.

First, you grab a reference to the shippingMethodPicker, and then invoke the numberOfComponentsInPickerView() method. Finally, you make sure the number of components is always 1. Kinda boring. But it makes sure there aren’t any extra or missing components in the picker.

Again, you grab a reference to the shippingMethodPicker first, and then invoke the pickerView:numberOfRowsInComponent() method to get the number of rows, or choices, of the picker. You want to make sure the number of rows equals the number of available shipping methods so your customers can choose the shipping speed.

By now, grabbing a reference to the shippingMethodPicker variable should be standard procedure. You invoke the pickerView:titleForRow:forComponent() method three times because there are three available shipping methods. The assertions simple perform some string comparisons to make sure the correct shipping methods are displayed to the customers. You don’t want to promise one-hour delivery!

It’s the shippingMethodPicker variable again! When you invoke the pickerView:didSelectRow:inComponent() method for the second row, you want to make sure the shippingMethodTextField’s text property is set accordingly.

Since you’ve already tested the three shipping method titles are properly displayed in the last test, you can just call the pickerView:didSelectRow:inComponent() method once here. You are testing strings in your last test, and the mechanism in this test.

A few words about writing trivial tests

As you can see, the implementation of these picker view delegate methods are very trivial. Do you really need to test a method that always returns 1?

I personally don’t really have a preference. Some people think they are important while others think they are a waste of time. But since they are so simple to implement and test, how much time do they actually waste? Not much at all. They do have some benefits down the road.

What if you need to display a different set of available shipping methods to customers with a Prime account? You’ll have to change the implementation. At that time, it’ll be nice to already have some tests to show what the picker is supposed to do. When you make the new changes, the tests will fail and let you know why.

If you are new to writing tests, it is best to write these simple tests. They serve as good practices and give you the early wins to generate some momentum.

That’s it for part 1. Stay tuned for part 2. You may want to review the Clean Swift architecture before next week’s post.

You can find the full source code at GitHub.

Get the Clean Swift Xcode Templates

Subscribe below to get my Xcode templates and learn how to apply the VIP cycle to your projects, extract business and presentation logic into interactor and presenter, navigate to different scenes using multiple storyboards, and write fast, maintainable tests with confidence to make changes.

I promise I'll never send you spam. You can unsubscribe at any time.

5 responses

  1. I learned a lot from your article, just want to thank you, great work.
    And I found a problem, when I followed your code, the “let bundle = NSBundle(forClass: self.dynamicType)” won’t compile because the compiler can’t find my “Main” storyboard. So I just changed the bundle to nil, and it worked.

    • Hi Andres,

      Thank you for the nice words! The answer to your question is yes. In fact, I already updated the Xcode templates to simply get a storyboard from the main bundle.

  2. I first implemented my ViewController as you do here and had no problem for the tests, but then I tested the VC in my app and found that it was very slow to update the table: it would take a long time between the calls of numberOfRowsInSection and cellForRowAtIndexPath.
    I search a bit on what could be the problem and it would seem that forcing the update to be done in the main thread would solve the problem so I changed accordingly the program:
    dispatch_async(dispatch_get_main_queue(), tableView.reloadData)

    This of course broke some of the tests.
    I have been able to repair the test the verify that reloadData is called by using an expectation like you do in one of your examples, but I didn’t find how to repair the tests that verify that a label text has been correctly set as the test seems to end always before the text is updated.
    One possible thing would be make the test wait for 1 sec or 2 but that would give me a test that can pass or fail depending on the time the view will take to update the text and I wonder if there is no other solution.

    Would you know something about this problem?

    • Hi Yoann,

      Yes, calling the completion handler in the main thread from the OrdersWorker will fix the slowness issue. I’m making a lot of updates to the CleanStore app that will have a new scene and all passing tests.

      Stay tuned.

Leave a Reply

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