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.
- Use a
UIControl
with an@IBAction
to aUIViewController
and present theUIViewController
in code — this is the one that the book recommends - Use a
UIControl
with aUIStoryboard
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 tests look like this:
Breaking it down:
- In the setup, instantiate the view controller from the storyboard and load the view so the
UINavigationItem
is available - In the first test, make sure that the
UIBarButtonItem
exists and has aUIViewController
as its target. - Makes sure the current controller is not presenting another
UIViewController
- 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
- ‘Manually’ perform the action that’s attached to your
UIBarButtonItem
- Assert that your custom
UIViewController
subclass was presented - Get a reference to the presented view controller
- 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 onUIControl
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
andUIKit
'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
The Second Way
In my setup I have a UIStoryboard
segue with a present modally option that links the two UIViewController
s.
The tests look like this:
Breaking it down:
All of the setup is the same so I’ll just focus on the differences here.
- The test no longer cares about the target of
UIBarButtonItem
. Also, the previous iteration of this test was calledtestHasAddBarButtonWithSelfAsTarget
but only ever tested the second part of that title. - 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.
- The name reflects the behavior you want to see without mentioning an internal method of your class
- 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
- If the
UIStoryboardSegueIdentifier
is important to you here, you can access and test it. Also allows you to invokeperformSegue(withIdentifier:sender:)
orprepare(for:sender:)
programmatically if you need/want to for your test - 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) - Wraps the previous lines from the previous test:
XCTAssertNotNil(controller.presentedViewController)
XCTAssertTrue(controller.presentedViewController is InputViewController
let inputViewController = controller.presentedViewController as! InputViewController
into a singleguard
statement
The code looks like this:
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 theUIStoryboard
- 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
UIViewController
s without involving theUIBarButtonItem
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 that
performSegue(withIdentifier:sender:)
presents the correctUIViewController
when passed that identifier. The wholeperformSelector
rigamarole seems a little dense
Conclusion:
I hope this gives you some inspiration for how you want to approach unit testing your UIStoryboardSegue
s and gets you thinking more about UIControl
s.
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.