对 TDD 不了解的同学可参考 Test-driven
development
本文使用的 Objective-C 单元测试框架是 OCUnit ,最新的 Xcode
已经包含。
TDD 的步骤如下:
- 写一个测试某个功能的单元测试用例;
- 运行,测试失败;
- 编码实现功能;
- 运行单元测试,通过修改代码,直到测试成功;
- 重构代码;
- 重构单元测试用例;
- 重复 1。
其中 5、6
是可选步骤,有必要了才会进行,但是必须保证产品代码和单元测试用例不能同时被更改。
简单的例子
实现一个很简单的储蓄账户管理。
创建项目
TddDemo (iOS Window-based Application).
Xcode 模版会缺省生成一个 TddDemo 的 target,这个是在 simulator
上跑的,我们需要添加新的 target Test,菜单 project -> new target
-> Cocoa -> Unit Test Bundle。具体设置可参考这篇博文。
测试 Case
创建类 _SavingAccountTest, target 选择 Test。

使用 OCUnit 需要 import 头文件 SenTestingKit.h, 并继承
SenTestCase,测试方法名必须以 test 开头。
代码如下: 我们需要可以存钱。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| #import < foundation /Foundation.h > #import < sentestingkit /SenTestingKit.h >
@interface _SavingAccountTest : SenTestCase {
} @end
#import "_SavingAccountTest.h"
@implementation _SavingAccountTest
- (void)testDeposit { SavingAccount *account = [[SavingAccount alloc] init]; [account deposit:100]; STAssertEquals(100, [account balance], @"bad balance 100 != %d", [account balance]); [account release]; }
@end
|
运行,测试失败

功能实现
最简单的方式让测试通过。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
| #import < foundation /Foundation.h >
@interface SavingAccount : NSObject {
}
- (void)deposit:(int)money; - (int)balance;
@end
#import "SavingAccount.h"
@implementation SavingAccount
- (void)deposit:(int)money {
}
- (int)balance { return 100; }
@end
|
运行,测试成功

下一个 Case
那么如果取钱会怎样?
修改 testDeposit 函数为如下:
1 2 3 4 5 6 7 8 9
| - (void)testDepositAndWithdraw { SavingAccount *account = [[SavingAccount alloc] init]; [account deposit:100]; [account withdraw:50]; STAssertEquals(50, [account balance], @"bad balance 50 != %d", [account balance]); [account release]; }
|
然后在 SavingAccount 添加空方法 withdraw 使编译通过。
运行,测试失败

功能实现
SavingAccount interface 添加属性 balance,更改实现如下
1 2 3 4 5 6 7 8 9 10 11 12
| - (void)deposit:(int)money { balance += money; }
- (void)withdraw:(int)money { balance -= money; }
- (int)balance { return balance; }
|
运行,测试成功

新 Case
银行存款账户不能透支, 添加 testNegativeBalanceIsNotFine:
1 2 3 4 5 6 7 8 9
| - (void)testNegativeBalanceIsNotFine { SavingAccount *account = [[SavingAccount alloc] init]; [account deposit:50]; [account withdraw:100]; STAssertEquals(0, [account balance], @"balance can't be negative 0 > %d", [account balance]); [account release]; }
|
运行,测试失败

更改实现
1 2 3 4 5 6
| - (void)withdraw:(int)money { balance -= money; if (balance < 0) { balance = 0; } }
|
运行,测试成功

重构
这时我们会发现测试的两个 case 里面都要实例化一个 SavingAccount,
是重复代码,可以提取出来,放入 setUp 和 tearDown
中,这两个方法分别在每一个 test 的最早和最后执行。
1 2 3 4 5 6 7 8
| - (void)setUp { account = [[SavingAccount alloc] init]; }
- (void)tearDown { [account release]; }
|
运行,测试成功
继续 ...
UT 和 TDD
- 人月神话很早以前就说过 No, silver bullet,TDD 也是
- UT 是需要时间成本的,所以要考虑 ROI (Return on Investment),
有些场景比如 UI
交互单元测试成本很高,就可以不去做,但大多数场景下,只要做
UT,总是会有很好的 ROI 的
- 切记切记不要追求覆盖率,但至少每个 bug 都要用 UT 覆盖