Two ways of testing a segue

An unsolicited code review.

Recently I was reading Test-Driven iOS Development with Swift 3 and there is a chapter where they test moving from one UIViewController to another. This got me thinking about the various ways to perform a segue/present a UIViewController and the ease of testing each.

There are two ways I want to talk about.

  1. Use a UIControl with an @IBAction to a UIViewController and present the UIViewController in code — this is the one that the book recommends
  2. Use a UIControl with a UIStoryboard segue

Of course I think #2 (my way) is better but I hope you’ll read this and choose the way that works for you and your project.

The first way

The book recommends having a UIViewController embedded in a UINavigationController with a UIBarButtonItem that has an IBAction which presents a different UIViewController that’s instantiated from a UIStoryboard.

TLDR; here’s a picture:

The segue/presentation happens in code

The tests look like this:

Breaking it down:

  1. In the setup, instantiate the view controller from the storyboard and load the view so the UINavigationItem is available
  2. In the first test, make sure that the UIBarButtonItem exists and has a UIViewController as its target.
  3. Makes sure the current controller is not presenting another UIViewController
  4. Make the controller you’re testing the rootViewController. This is so that another controller can be presented on it. Only a controller loaded into the window can present another controller
  5. ‘Manually’ perform the action that’s attached to your UIBarButtonItem
  6. Assert that your custom UIViewController subclass was presented
  7. Get a reference to the presented view controller
  8. Make sure it’s being loaded from the storyboard, ie. it’s outlets are not nil

The code looks like this:

The Good:

  • It’s very clear what’s being tested
  • It tests the target and the action of a UIControl which feels very clean. Check out the docs on UIControl for more on the target-action mechanism
  • Using an @IBAction makes it very easy to programmatically invoke an action that’s wired to a particular UI element

What I don’t like about this:

  • Knowing that the UIBarButtonItem has a target set to self is not particularly useful since we have no insight into what that method does
  • It encourages implementation that is cumbersome to write, burdensome to maintain or update, does not conform to best practices of iOS design, and does not leverage UIStoryboard and UIKit's strengths
  • The test name knows too much about the implementation of the code. There’s no reason the test should know the name of the method that’s doing the work.
  • The desired flow of the app is not immediately obvious from looking at the UIStoryboard file
Not totally clear from looking at the storyboard what this does. Adds something I guess?

The Second Way

In my setup I have a UIStoryboard segue with a present modally option that links the two UIViewControllers.

The tests look like this:

Breaking it down:

All of the setup is the same so I’ll just focus on the differences here.

  1. The test no longer cares about the target of UIBarButtonItem. Also, the previous iteration of this test was called testHasAddBarButtonWithSelfAsTarget but only ever tested the second part of that title.
  2. Makes sure that the button is actually an ‘add’ item. The next test looks at the functionality of the button so there’s nothing more to test here.
  3. The name reflects the behavior you want to see without mentioning an internal method of your class
  4. Gets the target and the action of the button explicitly, you no longer need to make the logical leap to the previous test to see that the target is your controller — a small thing but I think it reads a lot better
  5. If the UIStoryboardSegueIdentifier is important to you here, you can access and test it. Also allows you to invoke
    performSegue(withIdentifier:sender:) or prepare(for:sender:) programmatically if you need/want to for your test
  6. Invokes the action on the UIBarButtonItem's target instead of your controller which makes your test more flexible (you can now have that target point to anything, an object in the storyboard, a dataSource, whatever)
  7. Wraps the previous lines from the previous test:
    XCTAssertNotNil(controller.presentedViewController)
    XCTAssertTrue(controller.presentedViewController is InputViewController
    let inputViewController = controller.presentedViewController as! InputViewController

    into a single guard statement

The code looks like this:

Nothin to it

Exactly, there is no code. It’s one click (basically) in the storyboard file instead of an @IBAction which you can forget to hook up to code which you can implement incorrectly.

The Good:

  • Far less code
  • Cleaner code
  • Clearer test naming
  • Less coupling between the UIViewController and the UIStoryboard
  • Tests the identifier. A segue does not need an identifier to work. That said, if it has one it will be easier later to test the relationship between two UIViewControllers without involving the UIBarButtonItem

What I don’t like about this:

  • I don’t love using value(forKey:) in my tests. It seems necessary here but still has a sort of hacky code smell
  • I think it might be cleaner to test that the target has an identifier, and thatperformSegue(withIdentifier:sender:) presents the correct UIViewController when passed that identifier. The whole performSelector rigamarole seems a little dense

Conclusion:

I hope this gives you some inspiration for how you want to approach unit testing your UIStoryboardSegues and gets you thinking more about UIControls.

I also hope you noticed the thing that bothers me the most about both solutions which is that neither one tests that the UIViewController was presented modally. Right now you can change the UIStoryboardSegue from Show (e.g. Push) to Present Modally without the tests breaking. This is an oversight that can be remedied by adding the UIViewController to a UINavigationController as part of your setup but I thought adding this would be out of scope for this particular article. Just wanted to mention it.

Happy coding! Leave some 👏👏👏 if you enjoyed. Please comment if there’s something you think I missed or could make clearer.