Step by Step Walkthrough of iOS Test Driven Development in Swift

The following is an excerpt from my book Effective Unit Testing. It walks you through the TDD process in great details.

Test Driven Development, or TDD, means you write a failing test first, and add as little code as possible to make it pass. As you go, there can be multiple steps you need to take before a test eventually passes. When all your assertions are verified, your feature is complete. Therefore, when all tests pass, your app code will already be covered by unit tests. You don’t need to spend extra time to add tests afterward. There are a lot of resources about the process of TDD on the Internet, so I’m not going to elaborate it here.

The Red-Green-Refactor TDD Cycle

In essence, TDD is performed with the Red-Green-Refactor cycle. The three steps are:

  1. RED – Write a failing test
  2. GREEN – Write the minimum amount of code to make the test pass
  3. REFACTOR – Refactor both the app code and test code

You write a failing test to go into the RED state. Next, you write just enough code to transition to the GREEN state. Finally, you REFACTOR both your app code and test code while staying in GREEN. You then move on to the next test.

The Ordered Steps of TDD

Some people think they should write the assertions to fail first. But I have a different take on this. At the beginning, you don’t even know what the function signature looks like. You just draw a blank. It’s hard to write an assertion. What is the name of the function, its arguments (if any), the return value (if any)? Is it synchronous or asynchronous? Does it return results in a delegate method? Any optionals or nil’s we need to account for? Will it generate errors?

Any argument, return value of possible nil values usually mean you’ll have more inputs, and thus more test methods.

Instead, my preference is to write the function call in the When phase first. Because the function doesn’t exist yet, so we’ll create the function. This’ll help you define the function signature first. Yes, the function signature may very well change as we discover more things. But it’s a good start to get to GREEN.

With a real function that we can actually invoke, it’s much easier to write assertions in the Then phase. These assertions are meant to fail at first, because we haven’t implemented the function to the spec. So, resist the temptation to write a bunch of failing assertions. Otherwise, you’re just making the RED state too red.

Write one failing assertion. Implement the function to make the assertion pass. Add any inputs or test doubles in the Given phase, if necessary.

Pro Tip: Follow this order during test-driven development will make things so much easier: (1) When, (2) Then, (3) Given.

It’s one thing to know about the TDD cycle and the ordered steps. It’s another thing to see how it works in practice. Before we look at how to use TDD to drive features for the ShowOrder scene in the next chapter. Let’s see how you actually enter and exit each state in the TDD cycle by working through a simple example.

A Simple TDD Example

We’re going to use TDD to drive an implementation for the Greeting class’s generate() method. This method returns an appropriate greeting message, depending on whether the first and last names are provided as arguments or not. Our requirements are as follows:

  • If both first name and last name are nil, generate() should return nil.
  • If only first name is present, generate() should return a friendly greeting message such as “Hi Raymond.”
  • If only last name is present, generate() should return a formal greeting message such as “Hello, Mr. Law.”
  • If both first name and last name are present, generate() should return a full greeting message such as “Good to see you, Mr. Raymond Law.”

First question. How many tests should we have? As a general guideline, the number of test methods should be greater than or equal to the number of bullet points in your requirements. The reason is simple. As is often the case, each bullet describes what should happen based on a different set of input combinations. Each set of input combinations represents a unique test case, or user story. Therefore, each bullet warrants its own test method.

But why greater than or equal to? Try to think of the last time you specified the feature requirements, did you over-specify or under-specify? That’s right. Before the feature is worked on, it’s almost impossible to foresee every edge case. We tend to under-specify requirements. As development progresses, we find out more edge cases that we didn’t account for but now are aware of. These extra edge cases should really be their own bullet points, or sub-bullet points, too. That’s why the number of test methods should be greater than or at least equal to the number of bullet points in your requirements.

Pro Tip: As you find out more edge cases, more test methods need to be written to cover these edge cases. For each feature requirement, the number of test methods should be greater than or equal to the number of bullet points.

In this simple example, we’re going to keep things simple so as to keep our focus on the TDD process. We’ll write the following four test methods for each of the bullet points in the requirements above:

  • testGenerateShouldReturnNilWithNilFirstAndLastName()
  • testGenerateShouldReturnFriendlyGreetingWithJustFirstName()
  • testGenerateShouldReturnFormalGreetingWithJustLastName()
  • testGenerateShouldReturnFullGreetingWithBothFirstAndLastName()

You can find this TDD sample project on GitHub if you want to try things out yourself. Let’s begin.

If both firstName and lastName are nil, the greeting message should be nil

The first step is the When phase. So let’s call the generate() method on sut with the firstName and lastName arguments set to nil.

Build now, and you should see the error: Value of type 'Greeting' has no member 'generate'. We’re in our first RED state. Not to panic. This is expected. Let’s get to GREEN by defining the generate(firstName:lastName:) method.

The firstName and lastName arguments need to be of the optional type String? because our use case specifies that they may be nil. Build again to confirm we’re in GREEN before proceeding.

The next step is the Then phase. We’ll write an assertion to complete this step. The resulting greeting should be nil.

We’re now back in the RED with this warning: Constant 'greeting' inferred to have type '()', which may be unexpected. It’s missing a return type. So let’s get to GREEN by making the generate(firstName:lastName:) method return String?.

Yay, GREEN!. Because we have an assertion, a successful build doesn’t necessarily mean we’re in GREEN. We need to run the test to make sure it passes. If it passes, we’re truly in GREEN. If it fails, we’re actually in RED. Hit ⌘U to run the test now, and you should see we’re in GREEN.

In fact, this test is done! The current implementation simply returns nil. But that’s all our requirements ask for in this use case. What about the first and last names? We’ll take care of them in the next few tests.

But wait, what about the REFACTOR state? Let’s look at our app code and test code. You’ll see there’s really nothing to refactor at this point. So let’s move on to the next test.

If you want to read the rest and learn more, check out my Effective Unit Testing book:

  • See the walkthrough of the remaining tests in this example
  • Learn how to prepare your inputs in the GIven phase
  • Learn how to refactor both app code AND test code
  • Write unit tests for not only a single class, but an entire app
  • Understand stubs, mocks, spies and use them effectively
  • How to test asynchronous operations
  • Generate and organize your unit tests automatically
  • And, most of all, learn how to write testable code by getting the Clean Swift Handbook and Effective Unit Testing together for special bundle pricing

If you already purchased one book and now want to get the other book for a special discount, send me an email. And let me know which book you already bought and the email you used for the purchase. I’ll send you a $30 coupon code to get the other book. This is essentially the bundle price.

Email me for a $30 coupon code

I've been developing in iOS since the iPhone debuted, jumped on Swift when it was announced. Writing well-tested apps with a clean architecture has been my goal.

Leave a Comment

Your email address will not be published. Required fields are marked *