Test Driven Development Tutorial for iOS: Getting Started

In this Test Driven Development Tutorial, you will learn the basics of TDD and how to be effective at it as an iOS developer. By Christine Abernathy.

Leave a rating/review
Download materials
Save for later
Share
You are currently viewing page 3 of 4 of this article. Click here to view the first page.

Refactoring

Recognizing duplicate code and cleaning it up, also known as refactoring, is an essential step in the TDD cycle.

At the end of the previous section, a pattern emerged in the conversion logic. You’re going to identify this pattern fully.

Exposing the Duplicate Code

Still in Converter.swift, take a look at the conversion method:

func convert(_ number: Int) -> String {
  var result = ""
  var localNumber = number
  while localNumber >= 10 {
    result += "X"
    localNumber = localNumber - 10
  }
  if localNumber >= 9 {
    result += "IX"
    localNumber = localNumber - 9
  }
  if localNumber >= 5 {
    result += "V"
    localNumber = localNumber - 5
  }
  if localNumber >= 4 {
    result += "IV"
    localNumber = localNumber - 4
  }
  result += String(repeating: "I", count: localNumber)
  return result
}

To highlight the code duplication, modify convert(_:) and change every occurrence of if with while.

To make sure you haven’t introduced a regression, run all of your tests. They should still pass:

That’s the beauty of cleaning up your code and refactoring with TDD methodology. You can have the peace of mind that you aren’t breaking existing functionality.

There’s one more change that will fully expose the duplication. Modify convert(_:) and replace:

result += String(repeating: "I", count: localNumber)

With the following:

while localNumber >= 1 {
  result += "I"
  localNumber = localNumber - 1
}

These two pieces of code are equivalent and return a repeating I string.

Run all of your tests. They should all pass:

Optimizing Your Code

Continue refactoring the code in convert(_:) by replacing the while statement that handles 10 with the following:

let numberSymbols: [(number: Int, symbol: String)] // 1
  = [(10, "X")] // 2
    
for item in numberSymbols { // 3
  while localNumber >= item.number { // 4
    result += item.symbol
    localNumber = localNumber - item.number
  }
}

Let’s go through the code step-by-step:

  1. Create an array of tuples representing a number and the corresponding Roman numeral symbol.
  2. Initialize the array with values for 10.
  3. Loop through the array.
  4. Run each item in the array through the pattern you uncovered for handling the conversion for a number.

Run all of your tests. They continue to pass:

You should now be able to take your refactoring to its logical conclusion. Replace convert(_:) with the following:

func convert(_ number: Int) -> String {
  var localNumber = number
  var result = ""

  let numberSymbols: [(number: Int, symbol: String)] =
    [(10, "X"),
     (9, "IX"),
     (5, "V"),
     (4, "IV"),
     (1, "I")]
    
  for item in numberSymbols {
    while localNumber >= item.number {
      result += item.symbol
      localNumber = localNumber - item.number
    }
  }

  return result
}

This initializes numberSymbols with additional numbers and symbols. It then replaces the previous code for each number with the generalized code you added to process 10.

Run all of your tests. They all pass:

Handling Other Edge Cases

Your converter has come a long way, but there are more cases you can cover. You’re now equipped with all the tools you need to make this happen.

Start with the conversion for zero. Keep in mind, however, zero isn’t represented in Roman numerals. That means, you can choose to throw an exception when this is passed or just return an empty string.

In ConverterTests.swift, add the following new test to the end of the class:

func testConverstionForZero() {
  let result = converter.convert(0)
  XCTAssertEqual(result, "", "Conversion for 0 is incorrect")
}

This tests the expected result for zero and expects an empty string.

Run your new test. This works by virtue of how you’ve written your code:

Try testing for the last number that’s supported in Numero: 3999.

In ConverterTests.swift, add the following new test to the end of the class:

func testConverstionFor3999() {
  let result = converter.convert(3999)
  XCTAssertEqual(result, "MMMCMXCIX", "Conversion for 3999 is incorrect")
}

This tests the expected result for 3999.

Run your new test. You’ll see a failure because you haven’t added code to handle this edge case:

In Converter.swift, modify convert(_:) and change the numberSymbols initialization as follows:

let numberSymbols: [(number: Int, symbol: String)] =
  [(1000, "M"),
   (900, "CM"),
   (500, "D"),
   (400, "CD"),
   (100, "C"),
   (90, "XC"),
   (50, "L"),
   (40, "XL"),
   (10, "X"),
   (9, "IX"),
   (5, "V"),
   (4, "IV"),
   (1, "I")]

This code adds mappings for the relevant numbers from 40 through 1,000. This also covers the test for 3,999.

Run all of your tests. They all pass:

If you fully bought into TDD, you likely protested about adding numberSymbols mappings for say 40 and 400 as they’re not covered by any tests. That’s correct! With TDD, you don’t want to add any code unless you’ve first written tests. That’s how you keep your code coverage up. I’ll leave you with the exercise of righting these wrongs in your copious free time.

Note: Special mention goes to Jim Weirich – Roman Numerals Kata for the algorithm behind the app.

Use Your Converter

Congratulations! You now have a fully functioning Roman numeral converter. To try it out in the game, you’ll need to make a few more changes.

In Game.swift, modify generateAnswers(_:number:) and replace the correctAnswer assignment with the following:

let correctAnswer = converter.convert(number)

This switches to using your converter instead of the hard-coded value.

Build and run your app:

Play a few rounds to make sure all the cases are covered.

Other Test Methodologies

As you dive more into TDD, you may hear about other test methodologies, for example:

  • Acceptance Test-Driven Development (ATDD): Similar to TDD, but the customer and developers write the acceptance tests in collaboration. A product manager is an example of a customer, and acceptance tests are sometimes called functional tests. The testing happens at the interface level, generally from a user point of view.
  • Behavior-Driven Development (BDD): Describes how you should write tests including TDD tests. BDD advocates for testing desired behavior rather than implementation details. This shows up in how you structure a unit test. In iOS, you can use the given-when-then format. In this format, you first set up any values you need, then execute the code being tested, before finally checking the result.