什么是单元测试?

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

举个例子

待测试类

1
"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
"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
"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
"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

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