Test-driven using Objective-C

对 TDD 不了解的同学可参考 Test-driven development

本文使用的 Objective-C 单元测试框架是 OCUnit ,最新的 Xcode 已经包含。

TDD 的步骤如下:

  1. 写一个测试某个功能的单元测试用例;
  2. 运行,测试失败;
  3. 编码实现功能;
  4. 运行单元测试,通过修改代码,直到测试成功;
  5. 重构代码;
  6. 重构单元测试用例;
  7. 重复 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
// _SavingAccountTest.h
#import < foundation /Foundation.h >
#import < sentestingkit /SenTestingKit.h >

@interface _SavingAccountTest : SenTestCase {

}
@end

// _SavingAccountTest.m
#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
// SavingAccount.h
#import < foundation /Foundation.h >

@interface SavingAccount : NSObject {

}

- (void)deposit:(int)money;
- (int)balance;

@end

// SavingAccount.m
#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
// _SavingAccountTest.m
- (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
// SavingAccount.m
- (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
// _SavingAccountTest.m
- (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
// _SavingAccountTest.m
- (void)setUp {
account = [[SavingAccount alloc] init];
}

- (void)tearDown {
[account release];
}

运行,测试成功

继续 ...

UT 和 TDD

  1. 人月神话很早以前就说过 No, silver bullet,TDD 也是
  2. UT 是需要时间成本的,所以要考虑 ROI (Return on Investment), 有些场景比如 UI 交互单元测试成本很高,就可以不去做,但大多数场景下,只要做 UT,总是会有很好的 ROI 的
  3. 切记切记不要追求覆盖率,但至少每个 bug 都要用 UT 覆盖