TDD & GUI in Swift

TDD with a user interface (UI) is a challenge: you have to decide which parts to test.

Recently, Ted Young has been using the Yacht dice game (the root of the commercial game Yahtzee) as a testbed for TDD with UI in Java. He and Mike Hill inspired me to try the same with Swift for iOS. I’m experienced in TDD, much less so in Swift.

My Approach

I set out to do incremental development: one story at a time. Most stories were focused on rules for the various scoring categories.

User stories for the Yacht game: gameplay and categories

I used TDD this way:

  • Use TDD for the game rules and “business logic”
  • Limit behavior on the user interface side:
    • Reflect the state (e.g., the Roll button is only active when it’s legal to roll)
    • Let the user interface trigger behavior on the “back end”
  • Don’t make assertions about appearance or visual relationships

TDD for Business Logic – The Easy Part

Test-Driven Development for business logic (the turn rules and scoring categories) was straightforward – red-green-refactor etc.

Two parts came out especially nice:

  1. A Hand tracks the dice that are rolled, and understands the statistics of them: frequencies, sum, count of the most frequent, and so on.
  2. In the various scoring categories, I ended up with little Strategy objects that just know whether a hand qualifies, and how to score it.

SwiftUI

SwiftUI lets you create screens using textual specifications. (The earlier UIKit approach uses a mix of visual tools and programming.) It is similar in approach to React or The Elm Architecture, where you make the UI reflect a state, and the system worries about what parts to redraw or move around.

What I found confusing is how to get objects bound so they’ll automatically update (especially when passing things between views). I didn’t time it, but this felt like about half the work. I ran into the classic failure: you change something, but the UI doesn’t notice the change, so it doesn’t update.

My takeaway assignment is to get clear on this, as it was obviously a bottleneck for me.

TDD and GUI together

For my first story, I used TDD to create a reasonable interface, then hook things up.

The Yacht game GUI

This wasn’t great: My API ended up needing changes to work smoothly. I don’t know if this is because the SwiftUI approach is different than what I’ve used, or if I’m not as good at anticipating as I thought.

From there, I moved to a more UI-driven interface: define objects used by the UI, and update the UI to reflect them. Then use TDD to make those objects have the right value at the right time. I would definitely adopt this approach more next time.

Figure out the state the UI needs, then use TDD to make the state change properly.

Complexity in the GUI

I used a ViewModel approach, defining a single object that the GUI interacts with. For me, that was the Game object, holding all state the view needs, and all methods it can call for behaviors.

I started with a more distributed approach where the UI would talk to Category directly to select it, but I found it better to move that back to the Game so that updates could be more contained and controlled.

I have a few rules of thumb I took away for working with SwiftUI:

  • For collections, make contents Identifiable. (This lets the screen update work more easily.)
  • Favor properties (possibly published or calculated) over “get” methods.
  • Test notification of contained objects.

Testing notifications: somewhere along the way, I had assumed that making objects “published” meant they would synchronize “transitively”. I hadn’t bothered testing the update behavior because it was built in. And I paid the price of fumbling around, not realizing that things weren’t updating because they weren’t properly published.

The test quickly revealed my misunderstanding, and in the future I’ll make that test much earlier:)

Conclusion

For Test-Driven Development working with a User Interface, it’s not new advice to say “pull as much behavior as possible out of the UI so you can test-drive it.” But that philosophy fits well with SwiftUI.

I learned that it was easier to start from the UI state (even spending time to get the UI to look right in the various states) before test-driving the changes to the state. And I re-learned the importance of testing the update mechanism even when it’s “automatic”.