How we do iOS apps: Part 2 – Test Driven Development

Nov, 12, 2015 • Joris Dubois

Categories: iOS, TDD, Test Driven Development

This is the second post in a series where we describe how we build iOS apps at AppFoundry.

In this post we’ll explain Test Driven Development.

TDD

In a nutshell TDD (Test Driven Development) can be explained in a single line: write a test before you code. However a common mistake is that TDD is not about writing a full test suite, but about writing your code in small cycles. These small cycles all start and end with unit tests, so let’s dive into that.

A unit test

A unit test is a simple test that verifies an individual unit of code, usually methods, to behave exactly as you expect. Let’s first look at some unit-tested code before we dive into the details of writing those tests.

In this piece of code we are checking if the method incrementNumber on the NumberIncrementer object is returning the given integer parameter plus 1.

First the unit test:

import XCTest
@testable import TDD

class NumberIncrementerTest: XCTestCase {
    private let numberIncrementer:NumberIncrementer = NumberIncrementer()

    func testIncrementNumberShouldReturnGivenNumberPlusOne() {
        let givenNumber:Int = 10
        let expectedResult:Int = 11
        XCTAssert(numberIncrementer.incrementNumber(givenNumber) == expectedResult)
    }
}

And the production code:

class NumberIncrementer {
    func incrementNumber(numberToIncrement: Int) -> Int {
        return numberToIncrement + 1
    }
}

Now remember that while we were developing this specific class and method we used small cycles, ultimately ending to this result.

The TDD Cycle

Step one, make a test (red phase)

Making a test is the first step in the TDD cycle. We are going to produce a failing test without touching or writing any production code. Why? Because you have to know this test fails in some circumstances and that after this step your production code solves that problem. Note that writing a test with a compilation error is also a failing test.

Now it’s time to continue to the next step.

Step two, write production code (green phase)

Now that we are sure the test is failing, we can make it pass by writing some production code. However, your only goal in this phase is to make your test work. Do not touch any other code but try to make it green (non failing) the easiest way possible. By following these rules you are not writing any other non-tested code. Surely you could argue that you know some piece of code is necessary to make the function complete. But you won’t need it at this time, what you do need is a green test!

Once finished, continue to the last step.

Step three, refactor if needed

Now it’s time to look at the code you created. Are you happy with it? If the answer is ‘no’ it’s time to do some refactoring and cheer up your mind. Don’t forget to run your tests after, to ensure your refactor didn’t break any tests.

Hooray, cycle completed. Job well done! Return to step one and finish up that code you are working on.

Useful iOS libraries

Libraries can really help and speed up your development process. Luckily for us, iOS developers, the community provides some great tools:

SwiftHamcrest/OCHamcrest

The Hamcrest libraries provide matchers to give fluent api and improved error messages from your tests. It originated from the Java world but is also very common in other programming languages.

Here’s an example of how easily these matchers are used and read:

func testAvailableLanguagesContainsExpectedLanguages() {
	let hamcrestSupportedLanguages = ["Java", "Python", "Ruby", "Objective-C", "PHP", "Erlang", "Swift"]
	assertThat(array, containsInAnyOrder("Objective-C", "Swift", "Java", "Python", "Ruby", "PHP", "Erlang"))
}

OCMockito

Another great example of a test library is OCMockito. Unfortunately it is only available for Objective-C due to the lack of Swift runtime access, but never lose hope!

For those not yet familiar with the concept of mocking I’ll provide a few words. Mocking is mainly used when your object-under-test has dependencies on other objects. Meaning these mocked dependencies will simulate the behavior of real objects. You are simply making sure your object-under-test does not rely on a dependencies’ implementation.

An other example, but this time in good old Objective-C:

@protocol Greeter <NSObject>
- (NSString *)sayHelloTo:(NSString *)helloText;
@end
@interface Person() {
	id<Greeter> _greeter;
}
@end

@implementation Person

- (instancetype)initWithGreeter:(id<Greeter>)greeter {
	self = [super init];
	if (self) {
		_greeter = greeter;
	}
	return self;
}

- (NSString *)sayHello {
	return [_greeter sayHelloTo:@"OCMockito"];
}

@end
@interface PersonTest : XCTestCase {
	Person *_objectUnderTest;
	id<Greeter> _mockedGreeter;
}
@end

@implementation PersonTest

- (void)setUp {
	_mockedGreeter = mockProtocol(@protocol(Greeter));
	_objectUnderTest = [[Person alloc] initWithGreeter:_mockedGreeter];
}

- (void)testPersonGreeterSaysExpectedString {
	NSString *expectedGreeting = @"expectedGreeting";
	[given([_mockedGreeter sayHelloTo:@"OCMockito"]) willReturn:expectedGreeting];
	XCTAssertEqualObjects([_objectUnderTest sayHello], expectedGreeting);
}

@end

Code coverage

Now that you understand the way of implementing unit tests the TDD-way we can talk a bit about code coverage. Code coverage provides a great estimate on how much code has been unit-tested. Next if you follow the trend of your coverage you can detect untested lines and see if you’re introducing legacy code.

What we usually do is run code analysis on our CI to know if the coverage percentage is decreasing. You can even make your builds fail or unstable when they didn’t meet the required percentage of covered code.

My last advice about coverage is: always try to be pragmatic about it. Not everything is testable in an easy way and sometimes you have to consider if a test is absolutely needed for a specific piece of code. Do not lose too much time with it. These metrics do not specifically tell something about the quality of the code. You can still write crappy code with 100% coverage.

Conclusion

Working the TDD way is for you and your fellow developers, not for anybody else. It’s helpful in writing good maintainable code but it’s definitely not a religion. Don’t exaggerate, but try to look for a sweet spot. Once you hit that spot you will feel more confident about the code and fellow members of your team will immediately know if something breaks when they’re working on your code.

If you want to get started with TDD yourselves we highly recommend checking out unclebob’s Bowling Kata Game. It’s not written in fancy Swift, but I’m sure you will understand ;-).

Bowling Kata Game by UncleBob

Continue reading

View all articles

DRYUtilities 1.5.0 - iOS utility library

We’re proud to announce DRYUtilites 1.5.0. DRYUtilities is our iOS library of useful utility classes that we need in most of our projects. It includes categories on Foundation and UIKit to make it easier to:[...]

Devoxx Scheduler v5.0 released

Since 2011 we’ve been delivering the official Devoxx scheduler app. The app is your go to conference companion before, during and after the conference. It allows you to check the schedule, plan your devoxx, take notes, get info about the speakers, etc. This year we’ve added a[...]