什么是单元测试?

在计算机编程中,单元测试(英语:Unit Testing)又称为模块测试, 是针对程序模块(软件设计的最小单位)来进行正确性检验的测试工作。程序单元是应用的最小可测试部件。在过程化编程中,一个单元就是单个程序、函数、过程等;对于面向对象编程,最小单元就是方法,包括基类(超类)、抽象类、或者派生类(子类)中的方法。

举个例子

待测试类

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
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
"use strict";

class Car {

constructor() {
this.status = "stop";
}

// 开车
start() {
this.status = "start";
this._lockDoor();
}

// 停车
stop() {
this.status = "stop";
this._unlockDoor();
}

// 锁车门
_lockDoor() {
console.log( "door locked" );
this._lockFrontDoor();
this._lockBackDoor();
}

// 解锁车门
_unlockDoor() {
console.log( "door unlocked" );
this._unlockFrontDoor();
this._unlockBackDoor();
}

_lockFrontDoor() {
console.log( "front door locked" );
}

_lockBackDoor() {
console.log( "back door locked" );
}

_unlockFrontDoor() {
console.log( "front door unlocked" );
}

_unlockBackDoor() {
console.log( "back door unlocked" );
}

};

module.exports = Car;

以逻辑单元为最小单位的测试

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
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
"use strict";

const assert = require( "assert" );

const Car = require( "./sample" );

describe( "test Car", function() {

it( "start car", function () {
let car = new Car();

assert.equal( car.status, "stopped" );

let locked = false;
// MOCK
car._lockDoor = function () {
locked = true;
}

car.start();

assert.equal( car.status, "running" );
assert.equal( locked, true );

} );

it( "lock door", function () {
let car = new Car();

let frontDoorLocked = false;
let backDoorLocked = false;
car._lockFrontDoor = function () {
frontDoorLocked = true;
};

car._lockBackDoor = function () {
backDoorLocked = true;
};

car._lockDoor();

assert( frontDoorLocked, true );
assert( backDoorLocked, true );

} );

} );

以业务单元为最小单位的测试

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
27
28
29
30
31
32
33
34
35
36
37
38
"use strict";

const assert = require( "assert" );

const Car = require( "./sample" );

describe( "test Car", function() {

it( "start car", function () {
let car = new Car();

assert.equal( car.status, "stop" );

let _lockFrontDoor = car._lockFrontDoor;
let _lockBackDoor = car._lockBackDoor;

let frontDoorLocked = false;
let backDoorLocked = false;

car._lockFrontDoor = function () {
frontDoorLocked = true;
_lockBackDoor();
};

car._lockBackDoor = function () {
backDoorLocked = true;
_lockFrontDoor();
};

car.start();

assert.equal( car.status, "running" );
assert.equal( frontDoorLocked, true );
assert.equal( backDoorLocked, true );

} );

} );

一些单元测试原则

  • 单元测试必须由最熟悉代码的人(程序的作者)来写。
  • 单元测试应该在最低的功能/参数上验证程序的正确性。
  • 单元测试过后,机器状态保持不变。
  • 单元测试要快(一个测试运行时间是几秒钟,而不是几分钟)。
  • 单元测试应该产生可重复、一致的结果。
  • 独立性,单元测试的运行/通过/失败不依赖于别的测试,可以人为构造数据,以保持单元测试的独立性。
  • 单元测试应该覆盖所有代码路径,包括错误处理路径,为了保证单元测试的代码覆盖率,单元测试必须测试公开的和私有的函数/方法。
  • 单元测试应该集成到自动测试的框架中。
  • 单元测试必须和产品代码一起保存和维护。

单元测试的好处

  • 保证现有代码的功能都是正常的。
  • 若不清楚代码而修改了一个逻辑,运行一下单元测试就知道影响到了什么地方。
  • 反向推动代码做到一个单元的功能最小化。
  • 写单元测试的过程中可以重新梳理自己的代码,找出bug。
  • 重构系统而不改变功能时,对照单元测试来写自己的功能就可以,即使之前的代码不是自己写的。

单元测试不好的地方

  • 时间成本,直接导致人力成本上升。不过也有可能前提代码质量上的提升可以减少后期很多修BUG的时间。
  • 有些外部依赖较多并且逻辑相对简单的时候,初始化外部依赖或者mock的精力复杂度可能已经超过了业务代码本身。
  • 设计影响。有时候,为了方便测试,甚至已经影响到你的业务代码的逻辑。
  • 需求快速变化期间。有时候需求变化非常快速,单元测试会降低业务变化的敏捷度。

MOCK

MOCK 简单来说就是纸老虎。

业务代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
"use strict";

const DB = require( 'database' );

class Mock {

getList() {
const result = DB.query( "select * from table" );
for( let i = 0; i < result.length; i ++ ) {
// do somting
};

return result;
}

};

module.exports = Mock;

测试代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

const Mock = require( './mock' );

const DB = require( 'db' );

describe( "test Mock", function () {

it( "get list", function () {
DB.query = function ( sql ) {
return [ 1, 2, 3 ];
};

const car = new Car();
const result = car.getList();

assert.equal( result, [ 1, 2, 3 ] );
} );

} );

为什么我的代码需要mock?

优点

  • 可以不用去关注一些底层支撑系统,例如DB,如果不MOCK,那么还需要初始化一个DB,使用前使用后都要去清理环境,给自己擦屁股。
  • 可以更加关注于自己业务代码的业务逻辑,例如去初始化一个DB,需要很多配置信息。
  • 不会因为数据库的故障导致自己的单元测试失败。
  • 隔绝别的模块,别的模块即使只申明了接口,也可以正常跑完自己的单元测试。

缺点

  • 如果mock对象很复杂,属性很多,方法也很多,那就需要自己去录制相当多的工作去实现这个mock.
  • 有些语言难以mock。一些强类型语言和编译型语言对于mock还是比较难的。
  • mock会掩盖某些环境。比如各种输出的时间格式会根据用户的环境而定,不同的地域也会有不同的格式,这个时候用mock返回自己所在地区或者自己所在环境的格式,看似单元测试全部通过,实则埋下了隐患。
  • 在不清楚被mock对象的所有边界行为时,可能会导致程序没有处理一些边界问题而超出预期。

TDD

TDD(Test Drive Development),单元测试带来的一个非常好的开发方式。TDD思想让你在写代码之前,就先写单元测试,这个时候可以让你把整个代码从整体层面上运行起来,然后再去实现每个方法具体的功能。

优点

  • 在设计阶段,划分方法,划分类的时候,无需关注某个类或者方法的具体的实现,只要达到功能即可。
  • 在实现某个类或者某个方法的时候,无需关注这么设计是否合理,直接按照方法名实现方法即可。

BDD

BDD指的是Behavior Drive Development,也就是行为驱动开发。这里的B并非指的是Business,实际上BDD可以看作是对TDD的一种补充,当然你也可以把它看作TDD的一个分支。因为在TDD中,我们并不能完全保证根据设计所编写的测试就是用户所期望的功能。BDD将这一部分简单和自然化,用自然语言来描述,让开发、测试、BA以及客户都能在这个基础上达成一致。因为测试优先的概念并不是每个人都能接受的,可能有人觉得系统太复杂而难以测试,有人认为不存在的东西无法测试。所以,我们在这里试图转换一种观念,那便是考虑它的行为,也就是说它应该如何运行,然后抽象出能达成共识的规范。如果你用过JBehave之类的BDD框架,你将会更好的理解其中具体的流程。这里我推荐一篇具体阐述的文章。

DDD

Deadline Drive Development.

软件开发进度表

时间 汇报进度 真实进度
第一天 20% 5%
第二天 50% 10%
第三天 70% 10%
第四天 80% 10%
第五天 90% 15%
第六天 100% 100%

id2