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
- Add a test for a user case or a user story
- Run all tests and see if the new one fails
- Write some code that causes the test to pass
- Run tests, change production code until all test cases pass
- Refactor the production code
- Refactor the test code
- 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 | import Foundation |
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 | import Foundation |
Run All Tests
It passes.
Next User Story?
"People could withdraw some money"
Let's change the testDeposit
test case.
1 | import Foundation |
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 | import Foundation |
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 | func testNegativeBalanceIsNotFine() { |
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 | func withdraw(money:Int) { |
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 | class SavingAccountTest: XCTestCase { |
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 | func testDepositAndWithdraw() { |
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.