🧪 UI-TDD with Xcode Part II

As I wrote about last month, I was able to speak at Cocoaheads on some work we have been doing with Xcode UI Testing. I am speaking again tonight at a different Cocoaheads group, which is exciting for me.

I had created a sample project for my presentation last month, but lost the project when my computer died and had to get the logic board replaced. I have recreated the sample project, and am happy to share it here:

UI Testing Example

And just in case you would like to download the slides for reference, here is a link to the PDF:

UI TDD Presentation (5 MB)

And you can view a recording of the presentation here:


🧪 UI-TDD with Xcode

Last week I had the opportunity to present at our local Cocoaheads meetup. At work, I have been focused on testing more as of late, and have been working to get our UI tests in much better shape.

I adapted an approach that our Test Engineering department has been taking with automated tests for our web apps. Essentially, we create a page object to represent each screen in the app, and include all of the elements, actions, and verifications needed for that screen. Then the actual UI tests just reference the page objects using the exposed API of actions and verifications. As an example, this has changed a call from this:

XCUIApplication().tables.children(matching: .cell).element(boundBy: 0).staticTexts["2018-07-03 20:45:14 +0000"].tap()

to this:

.tapOnCell(at: 0).

I have really enjoyed the productivity boost this has given us, along with the safety and security of good test coverage and wanted to share this approach with the community. Included below is a PDF of my presentation.

UI TDD Presentation (5 MB)

Update: The talk was recorded and can be viewed here:


🧪 The value of iOS test-driven development (TDD)

This will not come as a surprise to most people—writing tests makes your code better. I have come to appreciate this more over the past few months, as I wrote about previously. Also not a surprise—writing tests first makes your code even better. From my experience with iOS developers, while we might know this, most of us do not do it. Even after coming to see the value, it is easy to skip straight to writing the code. I had an experience this week that highlighted the value of this for me again, and wanted to pause and reflect before allowing myself to lose the lesson.

The story

I wrote earlier this week about handling live text reload elegantly, and wanted to share a bit more about the process I went though. It went something like this:

  1. 💡 Think of a great idea to add to the app
  2. 🖥 Write the code to update text and preserve cursor position
  3. ⌨️ Write tests to verify my code worked properly
  4. 🎊 Bask in the illusion of completion
  5. 🙄 Step away from the project and realize I should probably support text selection as well
  6. ⌨️ Write failing tests for all the scenarios I could think of
  7. 🖥 Implement the logic for text selection
  8. 😒 Run the tests and realize I had broken cursor position support
  9. 😅 Repeat previous two steps until everything passed
  10. 🎉 Write an exultant blog post

The realization

After I was done, it hit me how different it was to write the code before the tests instead of vice versa. When the code came first, my tests just verified my code. I had come up with all the scenarios I thought I needed to support, and had coded them, so I just made sure that they worked as I intended.

However, when I wrote the tests first, it was a completely different mental exercise. I was not worried about how I was going to implement the different scenarios—I was just focused on thinking of all possible scenarios. Not only did I think of many different cases for text selection, but I realized that I had omitted a number of cases for cursor position as well.

The takeaway

My adherence to TDD has ebbed and flowed over time. It is easy to want to jump straight into writing the code and seeing something work in the simulator. But taking the time at the beginning to consider the desired behavior, and writing tests for that behavior makes for better code and fewer rewrites and regressions.

Sometimes, it can feel impossible to start with tests. Your tests will not compile if the objects they are testing do not exist yet. One approach that has worked well for me is to start by writing descriptive function names for tests, and then moving on to writing the rest of everything. The process looks something like this:

  1. Create a test case class and write empty test functions
  2. Create objects with properties and stubbed functions
  3. Fill in failing tests to verify behavior
  4. Implement the functions and logic needed for the tests to pass
  5. Adjust tests and code as needed

One of the most exciting aspects to programming is how much you learn while you are working through something. Invariably, you will have to make changes to the tests as you think through the implementation details. The trick is to make sure that your tests remain focused on the behavior, and not the actual implementation details. That way, if you refactor the implementation, your tests can still verify that the behavior remains intact.

The conclusion

A major benefit to using this approach is that it helps you write more reusable code, which has been a focus of mine lately. You still need to learn about good design patterns and best practices. But starting with your tests means that you are exercising the API from the beginning, and that will help you think through having a good API as you build it.

Part of my purpose in writing this post is to help myself recommit to TDD in all of my projects. It is too easy to get lazy and skip the tests altogether. That is part of why I have put build, coverage and version badges on all of my open-source project pages on this site. I am hoping that the shame of not having tests and builds and coverage will help motivate me to get this done quickly. My future self will thank me later.


Leveling up with automated testing

As I approached the end of 2015, I was thinking of my goals for 2016, and how I wanted to grow and improve as a developer. One of the main areas in which I felt like I was lacking was in automated testing. That is a topic that many developers talk about, usually with a slight sense of shame, or a tinge of regret. There is almost always a tacit acknowledgement that it is important, and we should be doing it, but for many reasons, it is not happening.

As an iOS development community, we seem to largely have not yet embraced testing. There are many notable developers who are huge proponents of testing, but there seem to be even more who are not. I was talking with a Ruby developer about why that is. He commented that without tests, he has no way to know if his code is working—there is nothing he can easily pull up to see the code in action. With iOS development, we launch the simulator, and immediately conduct a manual test. So writing automated tests feels unnecessary.

One of the most common refrains that I hear is that writing automated tests feels like writing the app a second time, but with no obvious value to the end user. The conclusion to that line of thinking is that the time would be better spent building out more features that do benefit customers.

One experience that I had in late 2015 started to change my thinking about this. I participated in a Startup Weekend event in Salt Lake City, and was on a team that built a bowling app for the then-yet-to-be-released Apple TV. I had never built a game before, and I was tasked with creating the actual game play—the bowling lane, pin, balls, and logic of the game. After diving into SceneKit and figuring out how to get the ball to go down the lane and knock over pins, I had to implement scoring logic, and reset the pins correctly based on the results of the frame. The logic was fairly simple, up until the tenth frame. I realized that it was going to be extremely difficult to manually test all of the different options. So I wrote a suite of automated tests to make sure that everything worked properly. That suite saved me a number of times as I would make a tweak for something like a strike or a spare, only to find out that I had broken part of the tenth frame logic.

At the beginning of 2016, I started on a new team at my day job, and began working on a new project, Align. We started with a quick prototype that had no automated testing. But as we began work on the real app, I took the time to set up a continuous integration build with Jenkins and Fastlane that would run all of my tests and push to iTunes Connect for Test Flight. Then I began writing tests for every single thing that I implemented.

As I worked to maintain high test coverage, I found that writing automated tests, and thinking about writing automated tests did at least three things for me:

  1. Knowing that I was writing tests made me write my code to be more testable, which helped it to be more modular and self-contained
  2. Actually writing the tests helped me think of edge cases and error handling that I had not yet considered
  3. Having tests already written helped me find small regressions as I made changes in the app

I worked hard to make sure that all non-UI code was as completely covered with unit tests as possible. And then I wrote UI tests both to test the UI code, and also to serve as integration tests for the whole app. There were a number of configuration things that I had to set up, and I hope to discuss those in future posts. Having a comprehensive suite of unit and UI tests has already saved me from some major bugs. I have made significant changes to the architecture and organization of the project, and have been able to verify within seconds where I had introduced problems in the app, or verify with confidence that everything was still working.

One practice that I have been pushing myself to adopt more is test-driven development or TDD. This has been most useful when discovering or hearing about a bug in the app. I write a failing test that highlights where the error is, and then write the code to fix the behavior. That way, I know when it is fixed, and that it will not break again in the future. When writing new code, TDD is often more difficult. I will create the basic objects that I need, and then write test methods whose names describe the behavior I want to have in the app. I will then add the property and functions necessary to define the API and write failing tests that illustrate what I expect to happen. Finally, I actually fill our the functions so that the tests pass. Invariably, there is some back and forth as I tweak the tests and tweak the code before everything is working properly.

Having a project with a robust test suite is such a comfort that I never want to go back. When I work on other projects that do not have tests in them, I start to get anxious and worry that I am breaking things without knowing about it. I have found that starting by adding some simple tests around any new behavior, or to illustrate bugs that are found is a great way to get started. You do not have to have the perfect project with complete test coverage in order to start reaping the benefits of automated tests. I look forward to learning more about testing and sharing more of what I am learning along the way.