Exploring the universe of TDD

Dave Poirier
ITNEXT
Published in
7 min readAug 24, 2018

--

I’ve been doing software development for over 20 years now, and somehow in all of that time I got pulled in projects which for some reason or another, Test Driven Development was either unwelcome, or simply impossible (you know, like when writing kernel components in x86 assembly).

But I finally have a chance to explore this universe and at the same time, re-learn how to program as some would say.. “the proper way”.

Throughout this tutorial, I will be re-implementing some of the code from my previous tutorial at https://itnext.io/infinite-grid-using-uicollectionview-155801e4f7f4

I’ve skipped over the basic of creating a project, I assume most of you know how to do this and how to check the little box labelled“unit test”. Let’s dive in!

Rule #1: Write your test first

Yes, you are not allowed to write any actual code, unless you have a failing test that justify you writing code to pass the test. And once you pass the test, stop.

So our tutorial starts by creating a function to instantiate a UICollectionView into a host view. Let’s create the stub of the class then stop until we write the test.

InfiniteGrid.swift — minimal code to compile

And now our first test, which should fail so we can actually implement our function properly:

InfiniteGridTests.swift — our first test

In theory, when we run our test, it should fail so it justifies us adding enough code so it passes. And now…

Xcode screenshot — Menu option to run unit tests

Success! Or rather … Failure! Now that we have a failing test, we can go and implement just enough code so it passes.

Rule #2: Write the minimum for your code to pass

Let’s update our InfiniteGrid.swift as so:

InfiniteGrid.swift — minimum code to pass first unit test

After adding one line of code, our test passes:

Xcode screenshot — passing first unit test

So far so good. Before we go on and write our second test, let’s use the setUp() and tearDown() functions so we do not have to duplicate the grid initialization code for every test.

InfiniteGridTests.swift — using setUp() and tearDown() functions

Using ⌘+U will re-run our tests, and confirm everything still works:

Xcode screenshot — passing our updated first unit test

Feel free to go and comment out our InfiniteGrid.swift change we did earlier and confirm the test fails; then make sure to uncomment it before proceeding to our next step.

Rule #3: Try to break your existing code

Before you even think of writing more code, take a step back and think of ways that we can write a test that would make the current code fail.

In our initializer, we set the frame of our grid to CGRect.zero, ignoring the size of the hostView. But we do not have any test failing! Let’s solve this by writing a second test:

InfiniteGridTests.swift — our second unit test

Re-running our tests (⌘+U) we can confirm it fails:

Xcode screenshot — our second failing unit test

We can then go and fix the code so it passes:

InfiniteGrid.swift — updated to pass second unit test

Then confirm our tests passes (⌘+U):

Xcode screenshot — passing both unit tests

Rule #4: Keep going until you can no longer break it

What else can we test? If you can think of a way to make your existing code fail, then you should write a test for that.

This part can actually be a fun challenge if you work in team, with one person writing the tests and the other writing the code, constantly challenging each other throughout the day as you progress in the code; alternating every few hours to keep things interesting or even after every test / code iteration!

Thinking about our InfiniteGrid, if the hostView is resized, shouldn’t the InfiniteGrid also resize? Let’s write a test:

InfiniteGridTests.swift — third unit test

Confirm it fails (⌘+U):

Xcode screenshot — failing our third unit test

And now to fix it:

InfiniteGrid.swift — code to pass third unit test

Confirm it now passes (⌘+U):

Xcode screenshot — passing our third unit test

Satisfied? Well no, I need my InfiniteGrid to have a clear background… another test:

InfiniteGridTests.swift — our fourth unit test

Confirm it fails … update the code…

InfiniteGrid.swift — updated to pass all four tests, finally matching code from demo

Confirm it passes… Repeat this process until you are satisfied your code is well covered, and respect all the criteria you can think of.

Reviewing our progress

So far, we have about 5 lines of code, and 4 tests to confirm the code is behaving as expected. You can review the Xcode console logs for a summary of the tests performed:

When your tests are properly setup, changing the behaviour of your class should cause one of the tests to fail. For example, if we were to update InfiniteGrid.swift to replace .flexibleHeight with .flexibleTopMargin, one of the test should fail.

If you can change the behaviour of your code and no tests are failing, then you missed some test cases. Immediately stop changing your code, and go write a test that fails then and only then resume development. Never forget Rule #1: Write your test first.

Moving on to ViewController

Satisfied with our work on InfiniteGrid, let’s move on to the code we created in ViewController.swift — in viewDidLoad we instantiate a InfiniteGrid.

Rule #1… test first. Let’s create a new test file for this new class.

ViewControllerTests.swift — setUp(), tearDown() and first unit test

Confirm it fails (⌘+U):

Xcode screenshot — failing our first ViewController unit test

Update the ViewController.swift to pass..

ViewController.swift — code to pass first unit test

Confirm it passes (⌘+U):

Xcode screenshot — passing our ViewController first unit test

Finally! We have the first part of the tutorial fully covered with Unit Tests.

What we’ve learned so far:

  1. Always write your tests first
  2. Write only the minimum code to pass your tests
  3. Try to break your existing code before writing more code
  4. Keep going until you can no longer break it

As you develop your code and your tests, you will eventually need to re-factor or re-organize your classes to keep your code clean and readable. You should strive to do this as early as possible as changing your code will mean having to rewrite your tests too! If you wait too long to re-factor you will only cause yourself more pain as you will have to rewrite more tests.

Refactor early, refactor often.

XCAssert?

You’ve probably noticed that we used XCAssertNotNil and XCAssertEqual functions during our testing. There are a number of functions available for you to test:

  • XCAssert()
  • XCAssertEqual()
  • XCAssertNotEqual()

While you can use XCAssert to perform most validation, its easier to confirm the intended test when using one of the more explicit function. For example:

XCAssertEqual(x,y)

is more explicit then:

XCAssert(x == y)

While both are equivalent, it is clear that the intent of the first test is to verify equality. When using only XCAssert and the test fails, you are left to wonder if the error is in the test or in the code you are testing. Was the intent of the test to use != instead of == or maybe it should have been >= ?

Similarly, using XCAssertTrue, XCAssertFalse, XCAssertNil() or XCAssertNotNil() clearly defines the intent. You should seldomly use the base XCAssert() function, if ever.

You can find a complete list of XCTest functions at: https://developer.apple.com/documentation/xctest

What about asynchronous methods?

You will want to use “Expectations” to manage your asynchronous testing. Once again, the docs at XCTest (see link in previous section) will provide you with all the different tests you need. XCTWaiter allows you to wait for a group of asynchronous expectations to complete, you can even setup Key-Value-Observer expectations. Most importantly, expectations can be time bounded so if the asynchronous operation does not completes within the specified time limit the test will be considered failed.

Want to learn more?

Great! There are many resources to learn more about TDD best practices and how to specifically to TDD using Xcode and Swift.

One lovely (albeit slightly outdated) tutorial from Yvette is particularly interesting to go through: https://medium.com/@ynzc/getting-started-with-tdd-in-swift-2fab3e07204b — Its a fun project, contains little challenges sprinkled throughout, it’s easy to follow and most content should be useable as is in Swift 4.

More formal? Lynda.com has a nice 2-hour tutorial available at https://www.lynda.com/iOS-tutorials/iOS-App-Development-Test-Driven-Development/672254-2.html

More hands-on? Check this great GitHub repository full of TDD love: https://github.com/luontola/tdd-tetris-tutorial

About the author

Dave Poirier is a senior software developer, currently working on creating some really interesting iOS applications at ID Fusion Software Inc.

Need help with your mobile app software development? Visit our website at http://idfusion.com

--

--

Senior iOS Developer | Mobile Security And Reliability Specialist