Part 6: Joining the dots with View Controllers — the UI testing part (with bonus Enums)

Yvette
11 min readDec 28, 2015

--

This is Part 6 of the Getting Started with TDD in Swift tutorial. If you haven’t done the first 5 parts of this tutorial, now might be a good time to go and take a look.

It’s been a long road through Parts 1–5, but you’ll be overjoyed to hear that by the end of this page, you will have a functioning FizzBuzz game.

  1. Without further ado, lets get going. As usual, delete the sample FizzBuzzUITests.swift file, and create a new UI Test Case Class. This time be sure to save it in the FizzBuzzUITests folder. Call it ViewControllerUITests.swift

2. Delete the example test, and start a new one:

3. Click on the empty line inside that new test, and then click the red record button.

Wait for the simulator to load, and then click on the number button.

Stop the recording.

4. Ok, it’s not much, but let’s see what our recording has generated:

It’s short and sweet, but we can see that it’s creating an instance of our application with XCUIApplication(), and then finding a button with the title “0” , and tapping it.

We add our expectation to the generated test:

And running our test we see that, as expected, it fails:

5. In order to make our test pass, we’ll need to do two things:

  • Trigger the play() method when the NumberButton is tapped.
  • Update the NumberButton text with the response from the Game.

First things first — to communicate an interaction with the View to the ViewController we need an IBAction.

Go to the Main.storyboard, and open the Assistant Editor, so that you have the storyboard and the ViewController.swift file side-by-side.

CTRL+Click+Drag from the number button to end the ViewController class, and in the pop up that appears change the following fields:

  • Connection: Action
  • Name: buttonTapped
  • Type: UIButton

This has created a new IBAction function in our ViewController.

In this new function, add a call to the play() function:

We’ll also need to add an IBOutlet for the NumberButton so we can update it’s title:

CTRL+Click+Drag from the number button to end the ViewController class, and in the pop up that appears change:

  • Connection: Outlet
  • Name: numberButton

Remember — the IBAction is triggered by an action on the button, the IBOutlet means the viewController can make changes to the button.

6. Next, we need to update the title of the NumberButton when it is updated. To do this we’ll use another cool Swift feature: Property Observers.

The Swift Documentation defines them as:

Property observers observe and respond to changes in a property’s value. Property observers are called every time a property’s value is set, even if the new value is the same as the property’s current value.

In practice it looks like this:

This means that whenever the value of gameScore is set, it’s didSet function will be called. Pretty cool!

7. Let’s run our tests.

Still failing! Let’s go check out what the error is. If it’s not shown inline in the tests, we can see the error in the report navigator

Reading the error, what do you think the problem is?

Why is our test able to find the button on the first line of the test, and then unable to find a match on the second??

The problem is that our code is working! The didSet is successfully changing the title of the button, and now is no longer a button on screen with the title “0”.

The quick fix is to change what we look for on the second line. The button now should have the title of “1”, so we’ll look for that

This works, and our test is now passing!

However, this code is screaming out for refactoring — we need a better way to identify our button.

Fortunately we have that with Accessibility Identifiers. To give the button an accessibility identifier head over to the Main.storyboard

Select the button, and go to the Identity Inspector. Scroll down to the Accessibility section, and make sure Accessibility is enabled.

In the Identifier field, type “numberButton”.

Now, we can neaten up our test code:

As always, don’t forget to run the tests and see them pass

8. Time for a challenge!

CHALLENGE 1: Write a test to check that after tapping the number button twice, the title of the button is now 2.

Answer here.

9. To make this test pass we need to update the buttonTapped() method to play the score, plus 1:

However, this give us an error:

Why are we unable to add two Ints together?

The problem is that gameScore is currently an Optional. As a reminder: Optionals are Swift’s way of saying of showing that a variable might be something, or it might be nothing.

As before, we’ll use a nifty guard statement to safely unwrap the optional. We’ll also use string interpolation (as before) to turn the Int into a string.

Try running your UI Tests again.

Still failing — but why?

The assertion is telling us that the score is still 0, which means it’s never incrementing. It could be that our guard statement is failing, and so returning before incrementing the score. We can test this hypothesis with breakpoints. A breakpoint will stop your app running when it hits a particular line of code, and pause it so we can inspect the state of the variables at that point. They can also helpfully tell us if a particular line of code is being hit!

Go to your ViewController, and add a breakpoint inside the else clause of the guard statement in the buttonTapped method by clicking on the line number, and see a blue arrow marker appear. Right where the ‘print’ statement is.

Now run the UITests again.

As they run, you’ll see the breakpoint being hit, and the app is paused. Our else statement from our guard is being run — which means that when we’re tapping on our button, gameScore is nil — it hasn’t yet been initialised.

So, let’s fix it!

CHALLENGE 2: At the end of the viewDidLoad() function use a guard statement to safely unwrap the game, and then assign the game.score to the ViewController’s gameScore.

Answer here.

Don’t forget to remove the breakpoint when you’re done, by clicking and dragging the blue marker away from the tramlines.

10. Progress! The tests run, and because we’re not seeing “Game score is nil” in the console, it means that it’s being assigned.

The last step for this test is updating the gameScore’s didSet function.

Using string interpolation, we’re setting the button’s title to whatever the value of the gameScore is.

You know the drill by now: Run the tests!

If it’s not clear from the error messages what’s going wrong, try firing up the simulator, and running the tests manually by clicking through the game yourself. Using the iPad Air 2, we see this:

Does it make sense now? Our string interpolation is printing out the verbatim value of gameScore — optional and all.

CHALLENGE 3: Using a guard statement again, safely unwrap the game score at the beginning of the didSet function, and use the unwrapped value in the setTitle function.

Answer here.

And with that, our tests should pass!

11. However, not to be a downer, but I think the buttonTapped() function could be refactored further.

Most of the code in the method is dealing with working out what the next number will be, but does this actually need to be done? When our users are playing the game they’re only tapping on the number button — not saying or typing the actually number that comes next. So, why are we going to all the faff to figure it out?

It’s because that’s the way we wrote the original Game and Brain. But as we’ve learnt before, there’s nothing wrong with continuing to change and improve our code.

Instead of passing around strings of “1”, “2”, “4”, “16”, “29” “Fizz”, “Buzz”, “FizzBuzz” (etc.), instead we only actually need to pass around 4 different alternatives:

  • Number
  • Fizz
  • Buzz
  • FizzBuzz.

In Swift we use Enumerations (Enums for short)for this situation — when we need to group together related values, and work with them across our code.

The official documentation is worth a read, and one which I won’t try and improve on!

In our app we’ll jump straight into it. Create a new file called Move.swift, and in it we’ll define our Move enum:

Now we have our enum, with all our different possible cases in it, let’s update our BrainTests. It’s only the testSayXXX() tests that need updating, so that instead of expecting a String, they expect a Move.

So, for example:

CHALLENGE 4: Update testSayBuzz(), testSayFizzBuzz() and testSayNumber() to expect a Move.

Answer here.

12. To get rid of the complier errors this has create we need to update the check() function. Instead of returning a String we need it to return a Move:

CHALLENGE 5: Update the return cases in the check() function to return Moves

Answer here.

If you try and run your tests now, you’ll get a complier error from the Game’s play() function. To silence the error for now we’ll comment out the troublesome lines in the method — commenting out means that the complier (and therefore, our app) will ignore these lines for now. Be sure to leave one return line uncommented!

Running our tests shows us that the updated to the Brain have worked, and all BrainTests are passing.

13. Now, let’s fix the Game.

CHALLENGE 6: In the GameTests do the same as we did with the BrainTests. Replace any game.play(move: “String”) instances with game.play(move: .something).

Answer here.

14. In the Game.swift file, uncomment any commented-out parts of the play() function.

The only change needed here is to update the move argument in play() from being a String to a Move.

Once again, we’ll need to comment out a few lines in our ViewController so we can run our tests.

Run your tests, and the GameTests should be back in the green!

15. Lastly the ViewController

CHALLENGE 7: Update ViewControllerUnitTests to use Moves.

Answer here.

CHALLENGE 8: Update the ViewController so your tests all pass

Answer here.

And now, we can delete all those the unnecessary lines from the buttonTapped function.

So teeny tiny!

And run the tests!

12. With that epic refactoring done, now we can make the rest of our buttons work:

CHALLENGE 9: Record and/or write a UI test for the FizzButton

Answer here.

CHALLENGE 10: Give the FizzButton an accessibility identifier so that your test runs.

13. To make this test pass, we need to hook the FizzButton up to the buttonTapped method.

When creating IBActions and IBOutlets you can either CTRL + Click + Drag from the element on the storyboard, or from the element’s listing on the View Hierarchy — you can use whichever you prefer.

NOTE: There appears to be a bug in Xcode 8 preventing you from hooking up more than one element to an IBAction with the sender as Any type. To fix this, make sure your IBAction func takes a UIButton as the sender type.

You can check that the connection has been made in the Connections Inspector.

Under the list of Sent Events, we can see that the Touch Up Inside event is connected to the buttonTapped: function in View Controller. Which is exactly what we want.

Now we need the buttonTapped function to do different things depending on which button is pressed.

Run the tests, and admire the sea of green ticks.

14. Final few steps to go!

CHALLENGE 11: Write the UI tests, and make the tests pass for the Buzz button

Answer: Buzz test here and ViewController code here.

15. Next, we’ll write the FizzBuzz test. Unfortunately in order to get the score to 14 we have to press the buttons in the right order. This is a little tiresome, but another benefit of writing tests is that once we’ve written the test it once, it’s just one click to run it again. Much better than having to tap through the game ourselves everytime!

To keep our test clean we can write a helper-function in our ViewControllerUITests:

Then, write your FizzBuzz test:

CHALLENGE 12: Make your FizzBuzz button test pass.

Answer here.

16. One final piece of refactoring to cap off this rather epic-length post.

We can tidy up the buttonTapped() function by using a switch statement rather than an if/else if/else if/else statement:

First add an IBOutlet for the fizzBuzz button.

Then you can replace the body of the buttonTapped() function:

Switch statements like this are clearer that many-branched if/else statements, and easier to extend.

Run your tests again, and admire your 30 passing tests.

Maybe even fire up the game on the simulator, and have a go playing it! Most importantly, give yourself a massive pat of the back!

Where to go from here?

Enjoyed this taster of iOS development, and wondering what you can do to learn more? Here’s some ideas!

Self learning

The best thing you can do to cement your learning is delete everything you’ve just written (yes, really!). After you’ve done that try (and taken a deep breathe) doing it all again, this time trying it first without looking at the tutorial the steps. It’ll be tough, but you will learn so much by challenging yourself to think on your feet (with the knowledge you can always look at the tutorial if you need to).

You could also add some features to the app! Challenge yourself to:

  • Stop the game when the user makes a mistake
  • Change to UI when the user loses
  • Add a timer (using a CocoaPod)
  • Store the player’s high score
  • Be able to change the Fizz and Buzz numbers

After that, go on and try and build something of your own!

More resources:

  • Developing iOS 1- Apps with Swift by Stanford University: Hands down (in my opinion) the best Swift & iOS course out there. It’s free on iTunes U, and now — with the release of the iOS 10 version — is bang-up-to-date. The only catch is that it does assume prior programming knowledge of another object-oriented language, so isn’t great for complete beginners.
  • The Swift Programming Language: The authoritative reference for Swift from Apple, offering a guided tour, a comprehensive guide, and a formal reference of the language.
  • Start Developing iOS Apps (Swift): Another official resource from Apple, this is a tutorial which offers guided introduction to building your first app.
  • Learn to Code iOS Apps with Swift Tutorial 1: Welcome to Programming: Finally, if you found some of the concepts covered here too advanced, try this tutorial from Ray Wenderlich. It doesn’t assume any prior knowledge of programming, and will help teach you the fundamentals alongside learning Swift and iOS.

If you found this tutorial helpful, please 💚 it below and follow me!

--

--

Yvette

iOS Developer | @makersacademy Alumni | Runner, Triathlete, ex-Rower | Feminist