2年前在实习的时候买了这本书,当时就随便翻了翻,也没看懂太多; 最近比较轻松,碰巧比较关注代码质量,一口气就把这书读完了。 这本书虽然关注点比较窄(重构代码,解开依赖,方便测试),但在这方面就众多情形提出了各自的解决方案,不得不说作者富有相当的代码洞察能力和实践经验。总的来说是本好书。
下面是我自己的一点体会:
PⅠ,概念和原则
1. 测试有关
测试代码并不难写, 它毕竟也只是代码, 胡乱一通总可以得到个结论, 但如果这样做, 也许我们还得需要额外的东西来测试我们的测试代码; 同样由于只是代码, 我们就有大量可以展现其漂亮的方式. 编写测试代码比较困难的一个因素就是测试对象内部与其外部的依赖.
假设我们现在完成了一个典型的3层系统, 如果我们需要测试逻辑层是否正确, 正式产品的运作方式是需要向下访问数据层的, 这里多半需要提供数据库的连接, 但由于层次的明确, 如果我们测试时获取数据的方式同样采取访问产品的数据层, 那么一方面, 设置数据层环境并不那么容易, 我们需要付出额外的工作代价; 另一方面, 如果测试失败, 由于我们引入了另外的层次, 错误将难定位.
测试就是给定语义上下文合理的输入, 执行测试方法, 比较实际输出和语义逻辑输出.
方便的测试就是除去所有复杂无意义的依赖, 实例化测试类(产品类的去依赖版本), 运行其上复写的简易方法, 然后实际结果与意义上的逻辑结果比较.
2. 本书有关
接缝: 指程序中的一些特殊点, 在这些点上你无需作任何修改就可以达到改变程序行为的目的. 面象对象方面, 虚函数就是一种接缝.
3. 解决方案
本书就是围绕这类主题, 其提出的各种解决方案本质都是寻找适当的接缝, 引入中间层(通常是接口), 以解除测试对象有关的各种依赖.
PⅡ, 具体技巧
1. 接口提取
原始代码: TargetFun是我们要测试的方法, 它依赖了DependenceObj的Fun函数.
1
Class TargetCls
2

{
3
public void TargetFun()
4
{
5

6
7
DependenceObj obj = new DependenceObj();
8
obj.Fun();
9
10

11
}
12
}
重构: TestFun是我们与TargetFun配套的测试方法, 其实现基本一样, 区别仅仅是IObj指向对象不同.

Code
1
interface IObj
2

{
3
void Fun();
4
}
5
6
class DependenceObj : IObj
7

{
8
public void Fun()
9
{
10
//Complicated things
11
}
12
}
13
14
class FakeObj : IObj
15

{
16
public void Fun()
17
{
18
//Easy things
19
}
20
}
21
22
Class TargetCls
23

{
24
public void TargetFun()
25
{
26

27
28
IObj obj = new DependenceObj();
29
obj.Fun();
30
31

32
}
33
}
34
35
Class TestCls
36

{
37
public void TestFun()
38
{
39

40
41
IObj obj = new FakeObj();
42
obj.Fun();
43
44

45
}
46
}
47
48
这个例子存在代码重复, 下面的方法可以避免这个问题.
2. 参数化
原始代码:
1
Class TargetCls
2

{
3
public void TargetFun()
4
{
5

6
7
DependenceObj obj = new DependenceObj();
8
obj.Fun();
9
10
11
}
12
}
重构:

Code
1
interface IObj
2

{
3
void Fun();
4
}
5
6
class DependanceObj : IObj
7

{
8
public void Fun()
9
{
10

11
}
12
}
13
14
class FakeObj : IObj
15

{
16
public void Fun()
17
{
18

19
}
20
}
21
22
Class TargetCls
23

{
24
25
public void TargetFun()
26
{
27
DelegateTargetFun(new DependanceObj());
28
}
29
30
public void DelegateTargetFun(IObj obj)
31
{
32

33
34
obj.Fun();
35
36
37
}
38
}
39
40
Class TestCls
41

{
42
public void TestFun()
43
{
44
IObj fakeObj = new FakeObj();
45
TargetCls target = new TargetCls();
46
target.DelegateTargetFun(fakeObj);
47
}
48
}
对于由于依赖难以执行的方法都可以尝试此技巧. 比如某构造函数内部依赖严重难以创建此类型对象, 我们可以将依赖对象由外部传入, 测试时只需要传入简单对象即可. 为了保持一致性, 带参数方法多作为原方法委托实现.
3. 子类化并重写
原始代码:

Code
1
class TargetCls
2

{
3
public void TargetFun()
4
{
5

6
7
DependanceFun();
8
9

10
}
11
12
private void DependanceFun()
13
{
14
//Complicated things
15
}
16
}
重构:

Code
1
class TargetCls
2

{
3
public void TargetFun()
4
{
5

6
7
DependanceFun();
8
9

10
}
11
12
protected virtual void DependanceFun()
13
{
14
//Complicated things
15
}
16
}
17
18
class TestCls : TargetCls
19

{
20
protected override void DependanceFun()
21
{
22
//Easy things
23
}
24
}
25
为了可以override依赖方法, 此方法必须声明virtual, 并提升访问权限.
4. 方法分解, 子类化
原始代码:

Code
1
class TargetCls
2

{
3
public void TargetFun()
4
{
5

6
//Complicated part 1
7
//Complicated part 2
8

9
//Complicated part N
10

11
}
12
}
重构:

Code
1
class TargetCls
2

{
3
public virtual void InnerPartOne()
4
{
5

6
}
7
8
public virtual void InnerPartTwo()
9
{
10

11
}
12
13
public virtual void InnerPartN()
14
{
15

16
}
17
18
public void TargetFun()
19
{
20

21
InnerPartOne(); //Complicated part 1
22
InnerPartTwo(); //Complicated part 2
23

24
InnerPartN(); //Complicated part N
25

26
}
27
}
28
29
class TestCls : TargetCls
30

{
31
public override void InnerPartOne()
32
{
33

34
}
35
36
public override void InnerPartTwo()
37
{
38

39
}
40
41
42
public override void InnerPartN()
43
{
44

45
}
46
47
}
分解巨型方法是关键.
5. 剥离并外覆
子类化是一个重要的解依赖手段, 但有时候由于特殊原因, 被依赖的类不能被继承(sealed).
原始代码:
1
sealed class UninheritCls
2

{
3
4
}
5
6
class TargetCls
7

{
8
public void TargetFun(UninheritCls obj)
9
{
10

11
obj.Fun();
12

13
}
14
}
重构:

Code
1
sealed class UninheritCls
2

{
3
public void Fun()
4
{}
5
}
6
7
interface IObj
8

{
9
void Fun();
10
}
11
12
class UninheritWrapper : IObj
13

{
14
private UninheritCls innerObj = new UninheritCls();
15
16
public UninheritWrapper(UninheritCls param)
17
{
18
innerObj = param;
19
}
20
21
public void Fun()
22
{
23
innerObj.Fun();
24
}
25
}
26
27
class FakeUninheritCls : IObj
28

{
29
public void Fun()
30
{}
31
}
32
33
class TargetCls
34

{
35
public void TargetFun(IObj obj) //Uses UninheritWrapper object as parameter when at production, FakeUninheritCls object at testing
36
{
37

38
obj.Fun();
39

40
}
41
}
42
43
class TestCls
44

{
45
public void TestFun()
46
{
47
IObj fakeObj = new FakeUninheritCls();
48
TargetCls target = new TargetCls();
49
target.TargetFun(fakeObj);
50
}
51
}