How we do iOS apps: Part 2 - 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
- (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 ;-).