应用测试

本文共--字 阅读约--分钟 | 浏览: -- Last Updated: 2021-05-21

软件测试术语表

断言

断言是一个条件,它是必须被测试确认的一段代码的行为是否与期望相符,或换句话说是否与要求一致。

测试规范

测试规范(specs)是软件开发人员用来指代测试规范的一个术语。一个测试规范(不要和测试计划混淆)是一个详细的清单,它包含了需要测试的场景,这些场景将如何被测试,它们应该怎样被测试等。

测试用例

测试用例是决定一个程序中的功能是否按照原始期望工作的一些条件,断言是一个条件,而测试用例是一组条件;

测试监视

测试监视(Spy)是某些测试框架提供的功能。它允许我们包裹一个函数,并且记录它的使用情况(输入、输出和被调用的次数)。这个函数的功能并没有被改变。

替身

替身对象是指测试执行时被传入但并没有实际用到的对象。

测试桩

桩(stub)是指测试框架提供的一种功能,测试桩也允许包裹一个方法然后观察它的使用情况。和测试监视不一样的是,当使用测试桩包裹一个函数时,这个函数的功能会被新的行为替换。

模拟

测试模拟和测试桩都能为测试用例提供一些输出,但是信息流差异非常大。

  • 测试桩为被测试的程序提供输入值,可让被测试程序能扮演另外的程序
  • 模拟为测试提供输入来决定测试是否能通过。

测试覆盖率

测试覆盖率是指程序中有多大比例的代码通过自动化测试被测试到。

测试计划和方法

测试驱动开发

测试驱动开发(TDD)是一种测试技术,它鼓励开发者在写程序代码之前写测试,通常情况下,使用TDD编码包含以下基本步骤:

  1. 编写一个不通过的测试
  2. 运行这个测试,并保证它不能通过
  3. 编写应用代码,让测试通过
  4. 运行这个测试,保证它能通过
  5. 运行所有其他测试,保证程序的其他部分没有被破坏
  6. 重复以上步骤。

行为驱动测试

行为驱动开发(BDD)在测试驱动开发后出现,它的使命是提取TDD的精华,以BDD方式测试的重点是描述并且阐明测试应该关注程序的需求而非测试的需求,理想状态下,它会鼓励开发者少思考测试这件事情本身而更多的思考整个程序。

测试计划和测试类型

测试计划是特定被测试区域内的测试规范的集合,一般推荐为具体的测试计划撰写文档,因为一个测试计划包括非常多的步骤、文档和实践。测试计划的一个重要目标是,定义、指出什么类型的测试对于程序中的特定组件是合格的。

测试类型:

  • 单元测试:这被用来测试独立的组件,如果组件不是独立的,或换句话说它包含一些依赖,我们就需要通过设置测试模拟和依赖注入来尽可能地让它在测试中独立。如果没办法操作组件的依赖,则使用测试监视以让测试更加方便。我们的主要目标是在测试的时候达到组件完全独立的程度,一个单元测试也应该需要快速运行,应该避免输入输出、网络请求、或任何可能影响到测试运行速度的操作。

  • 部分集成测试和整体集成测试:这被用来测试一组组件(部分集成测试)或者整个程序(整体集成测试),在集成测试中,我们会正常地使用已知的数据与后端通信,获取到将会显示在前端的数据。随后我们将断言显示的信息是否是正确的。

  • 回归测试:用来确认程序错误是否被修复,如果使用TDD或BDD,当遇到一个程序错误的时,应该新建一个单元测试来重现这个问题,然后改写代码,做完这些就可以运行单元测试尝试重现错误并通过测试,最后确认所有测试都能正常通过。

  • 性能/加载测试:用来确认程序是否达到性能预期,可以使用性能测试确认程序是否能处理大量的用户并发或活跃度剧增。

  • 端对端测试:这种测试和整体集成测试其实并没有什么不同,最主要的是在一个E2E测试活动期间,我们会尝试完全模拟与正式用户一样的环境。

  • 验收测试(UAT):这被用来验证系统是否符合用户的所有需求。

编写测试用例

举例

interface.d.ts

interface MathInterface {
  PI: number;
  pow(base: number, exponent: number);
}

bdd.test.ts

/// <reference path="./interface.d.ts" />

import { MathDemo } from "./math_demo";
var expect = chai.expect;  // 引入断言库

describe('BDD test example for MathDemo class', () => { // 定义测试套件
  // before(() => { /* 全局前置hook */})
  // after(() => { /* 全局后置hook */})
  // beforeEach(() => { /* 所有测试用例前置hook */})
  // afterEach(() => { /* 所有测试用例后置hook */})

  it('should return the correct numeric value for PI', () => { // 单元测试
    var math: MathInterface = new MathDemo();
    expect(math.PI).to.equal(3.14159265359);
    expect(math.PI).to.be.a('number');
  })

  it('should return the correct numeric value for pow', () => { // 单元测试
    var math: MathInterface = new MathDemo();
    var result = math.pow(3, 5);
    var expected = 243;
    expect(result).to.be.a('number');
    expect(result).to.equal(expected);
  })
})

现在根据测试用例编写代码math_demo.ts

/// <reference path="./interface.d.ts" />

class MathDemo implements MathInterface {
  public PI: number;
  constructor() {
    this.PI = 3.14159265359;
  }

  public pow(base: number, exponent: number) {
    var result = base;
    for(var i = 1; i < exponent; i++) {
      result = result * base;
    }
    return result;
  }
}

export { MathDemo };

测试异步代码

interface.d.ts

interface MathInterface {
  powAsync(base: number, exponent: number, cb: (result: number) => void);
}

bdd.test.ts

/// <reference path="./interface.d.ts" />

import { MathDemo } from "./math_demo";
var expect = chai.expect;  // 引入断言库

describe('BDD test example for MathDemo class', () => { // 定义测试套件

  it('should return the correct numeric value for pow (async)', (done) => { // 传入一个done函数
    var math: MathInterface = new MathDemo();
    math.powAsync(3, 5, (result: number) => {
      var expected = 243;
      expect(result).to.be.a('number');
      expect(result).to.equal(expected);
      done(); // 调用 done() 来表示测试已经完成
    })
  })
  // 需要注意的是,默认情况下,it方法会等待它的回调返回结果,但是在测试异步代码时,函数可能会在测试执行完成之前返回。
})

现在根据测试用例编写代码math_demo.ts

/// <reference path="./interface.d.ts" />

class MathDemo implements MathInterface {
  constructor() {
    // ...
  }

  public powAsync(base: number, exponent: number, cb: (result: number) => void) {
    var delay = 45;
    setTimeout(() => {
      var result = base;
      for(var i = 1; i < exponent; i++) {
        result = result * base;
      }
      cb(result);
    }, delay);
  }
}

export { MathDemo };

断言异常

interface.d.ts

interface MathInterface {
  bad(foo?: any): void;
}

math_demo.ts

/// <reference path="./interface.d.ts" />

class MathDemo implements MathInterface {
  constructor() {
    // ...
  }

  public bad(foo?: any) {
    if (isNaN(foo)) {
      throw new Error('Error!');
    } else {
      // ...
    }
  }
}

export { MathDemo };

bdd.test.ts

/// <reference path="./interface.d.ts" />

import { MathDemo } from "./math_demo";
var expect = chai.expect;  // 引入断言库

describe('BDD test example for MathDemo class', () => { // 定义测试套件

  it('should throw an exception when no parameters passed', () => {
    var math: MathInterface = new MathDemo();
    var throwsF = () => {
      math.bad(/* missing args */);
    }
    expect(throwsF).to.throw(Error);
  })
})

Mocha 和 Chai 的 TDD 与 BDD 对比

TDD和BDD遵守很多一样的原则,但它们的风格有些许不同,这两种风格提供了同样的功能,BDD更多地考虑到能被软件开发团队的不同角色读懂(不仅仅是开发人员)

下面对比了 TDD 和 BDD 的套件、测试和断言的名字和风格

TDD BDD
suite describe
setup before
teardown after
suiteSetup beforeEach
suiteTeardown afterEach
test it
assert.equal(Math.PI, 3.14159265359) expect(Math.PI).to.equal(3.14159265359)

使用Sinon.JS编写测试监视和测试桩

interface.d.ts

interface CalculatorWidgetInterface {
  render(id: string);
  onSubmit() : void;
}

calculator_widget.ts

///<reference path="./interfaces.d.ts" />
///<reference path="../typings/tsd.d.ts" />

class CalculatorWidget implements CalculatorWidgetInterface{

  private _math : MathInterface;
  private $base: JQuery;
  private $exponent: JQuery;
  private $result: JQuery;
  private $btn: JQuery;

  // 依赖反转原则,依赖一个接口而不是依赖一个实例
  constructor(math : MathInterface) {
    if(math == null) throw new Error("Argument null exception!");
    this._math = math;
  }

  public render(id : string) {
    $(id).html(template);
    this.$base = $("#base");
    this.$exponent = $("#exponent");
    this.$result = $("#result");
    this.$btn = $("#submit");
    this.$btn.on("click", (e) => {
      this.onSubmit();
    });
  }

  public onSubmit() {
    var base = parseInt(this.$base.val());
    var exponent = parseInt(this.$exponent.val());

    if(isNaN(base) || isNaN(exponent)) {
      alert("Base and exponent must be a number!");
    }
    else {
      this.$result.val(this._math.pow(base, exponent));
    }
  }
}

var template = '<div id="widget">' +
  '<input type="text"  id="base">' +
  '<input type="text"  id="exponent" >' +
  '<input type="text"  id="result" placeholder="1">' +
  '<button id="submit" type="submit">Submit</button>' +
'</div>';

export { CalculatorWidget };

当被测试的组件被强关联到另一个组件时,编写单元测试就会变成一项非常复杂的任务,有时候,即使遵循了优秀的实践,依然需要处理高度关联的代码。

测试监视、测试模拟和测试桩会解决一些高度耦合模块带来的痛苦,这些功能帮助我们扎到一些错误,如果将依赖的组件全部替换为测试桩,测试仍然无法通过,我们就知道问题出在了这个组件的内部而不是外部依赖的组件。

比如 CalculatorWidget类的计算展示依赖 MathDemo类,如果这个计算器的组件出了问题,我们无法得知问题的根源在 CalculatorWidget 还是 MathDemo类,然而,如果为 CalculatorWidget 写一写独立的单元测试(将 MathDemo依赖替换为测试桩),其中的一些测试失败了,我们就知道问题出在了 CalculatorWidget类 而不是 MathDemo类;

bdd.test.js

///<reference path="./interfaces.d.ts" />

import { MathDemo } from "./math_demo";
import { CalculatorWidget } from "./calculator_widget";

describe('BDD test example for CalculatorWidget class \n', () => {

  before(function() {
    $("body").append('<div id="widget"/>');
  });

  // 重置 HTML 容器,保证每一个测试的组件都是全新的
  beforeEach(function() {
    $("#widget").empty();
  });

  // 实践: 测试监视
  it('should invoke onSubmit when #submit.click is triggered \n', () => {
    var math : MathInterface = new MathDemo();
    var calculator = new CalculatorWidget(math);

    calculator.render("#widget");

    // 测试监视 onSubmit
    var onSubmitSpy = sinon.spy(calculator, "onSubmit");

    $('#base').val("2");
    $('#exponent').val("3");
    $("#submit").trigger("click");

    // 断言 calculator.onSubmit 会被调用
    expect(onSubmitSpy.called).to.equal(true);
    expect(onSubmitSpy.callCount).to.equal(1);
    expect($("#result").val()).to.equal('8');
  });

  // 测试监视允许许多操作,从检查一个函数被调用多少次到检查到它是否被 new 操作符调用
  // 或调用它的时候是否传入一组特定的参数


  // 实践:测试桩,将 CalculatorWidget 从它的依赖 MathDemo中独立出来
  it('onSubmit should set #result value when #submit.click is triggered \n', (done) => {
    var math : MathInterface = new MathDemo();

    // 替换 math.pow方法 
    sinon.stub(math, "pow", function(a, b) {
      expect(a).to.equal(2);
      expect(b).to.equal(3);
      done();
    });

    // 并不是传入一个普通的的 MathDemo实例作为它的唯一参数,而是将这个实例注入了测试桩,这样做之后,我们就不再测试MathDemo类了
    // 而是在独立的环境里测试 CalculatorWidget类
    // 如果不是通过构造函数参数进行依赖注入的话,事情就会变得复杂得多
    var calculator = new CalculatorWidget(math);

    calculator.render("#widget");
    // 当参数传入的顺序不正确时。可以百分百确定问题的根源在onSubmit函数了
    $('#base').val("2");
    $('#exponent').val("3");

    $("#submit").trigger("click");
    expect($("#result").val()).to.equal('8');
  });
});