TDD on Swift

Long long ago, I wrote a post about how to do TDD using Objective-C, since Apple WWDC 2014, Swift is really eye-catching, I think I should write a new one to follow the trend.

XCTest is used as the unit test framework, and Xcode 6 is needed.

TDD Work-flow

  1. Add a test for a user case or a user story
  2. Run all tests and see if the new one fails
  3. Write some code that causes the test to pass
  4. Run tests, change production code until all test cases pass
  5. Refactor the production code
  6. Refactor the test code
  7. Return to 1, and repeat

The 5 and 6 are optional, do them only if needed, but be sure that DO NOT do them at the same time. That is, when you refactor production code, you can't change the test code, until all the test cases are passed, then you are confident that your production code refactoring is perfect, then, you can refactor the test code, and this time, you can't change the production code.

A Simple Example

We are about to implement a super simple bank account management tool.

Create a Project

Use Xcode to create a project BankAccount (iOS Single View Application)

Add a Test Case

Create a Swift file named SavingAccountTest, and choose BankAccountTests as target.

"People can deposit money to a saving account", it's our first user story.

1
2
3
4
5
6
7
8
9
10
11
import Foundation
import XCTest

class SavingAccountTest: XCTestCase {

func testDeposit() {
var account = SavingAccount()
account.deposit(100)
XCTAssertEqual(100, account.balance)
}
}

Run All Tests

Run all the unit tests, it fails as we expected.

Write Code to Pass the Test

Create a Swift file named SavingAccount, and choose both BankAccount and BankAccountTests as targets.

Make it simple, just to pass the test.

1
2
3
4
5
6
7
8
9
import Foundation

class SavingAccount {
var balance:Int = 100

func deposit(money:Int) {

}
}

Run All Tests

It passes.

Next User Story?

"People could withdraw some money"

Let's change the testDeposit test case.

1
2
3
4
5
6
7
8
9
10
11
12
import Foundation
import XCTest

class SavingAccountTest: XCTestCase {

func testDepositAndWithdraw() {
var account = SavingAccount()
account.deposit(100)
account.withdraw(50)
XCTAssertEqual(50, account.balance)
}
}

Also, add an empty withdraw method to SavingAccount to satisfy the compiler. Do not add any other code until we see it fails.

Run All Tests

The test fails, because the account balance was not updated after people withdrew some money.

Write Code to Support Withdraw

1
2
3
4
5
6
7
8
9
10
11
12
13
import Foundation

class SavingAccount {
var balance:Int = 0

func deposit(money:Int) {
balance += money
}

func withdraw(money:Int) {
balance -= money
}
}

Run All Tests

All the user stories are satisfied.

Any Other New User Story?

"People can't withdraw money beyond their account balance"

We add a new test case testNegativeBalanceIsNotFine

1
2
3
4
5
6
func testNegativeBalanceIsNotFine() {
var account = SavingAccount()
account.deposit(50)
account.withdraw(100)
XCTAssertEqual(0, account.balance)
}

Run All Tests

It fails, we have to fix it.

Write Code

Change the withdraw method, set account balance to 0 if it is less than 0.

1
2
3
4
5
6
func withdraw(money:Int) {
balance -= money
if balance < 0 {
balance = 0
}
}

Run All Tests

All right, all the test cases are succeeded.

Refactoring

Until now, we haven't do any refactoring on our code base.

I think the production code is fine, so we skip the step 5, and refactor the test code.

We can see that both test cases create an instance of SavingAccount, the duplicated code can be removed by using only one SavingAccount instance.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class SavingAccountTest: XCTestCase {
var account = SavingAccount()

func testDepositAndWithdraw() {
account.deposit(100)
account.withdraw(50)
XCTAssertEqual(50, account.balance)
}

func testNegativeBalanceIsNotFine() {
account.deposit(50)
account.withdraw(100)
XCTAssertEqual(0, account.balance)
}
}

Don't forget to run all the tests, make sure it is succeeded.

Why no setup and tearDown

People coming from objc may doubt that why the account instance is not put into setUp method, the way we use might cause different test cases sharing one instance variable, as we know, test cases should be independent with each other.

Yes, I had this doubt, too. So I did a test, by adding a "account balance should be 0" check before each test cases.

1
2
3
4
5
6
7
8
9
10
11
12
13
func testDepositAndWithdraw() {
XCTAssertEqual(0, account.balance)
account.deposit(100)
account.withdraw(50)
XCTAssertEqual(50, account.balance)
}

func testNegativeBalanceIsNotFine() {
XCTAssertEqual(0, account.balance)
account.deposit(50)
account.withdraw(100)
XCTAssertEqual(0, account.balance)
}

The result shows that the XCTest framework avoids instance variable sharing between test cases by instantiating a brand new XCTestCase object for each test case. That is, it instantiated two SavingAccountTest objects as our tests run.

To TDD Haters

If you hate TDD, and may think this blog post is garbage.

Sorry for that, you can remove your browser history of this address, if it makes you feel better.

Also, I strongly recommend you to watch the "TDD dead" discussions by Martin Fowler, Kent Beck and David Heinemeier Hansson.