这篇文章上次修改于 268 天前,可能其部分内容已经发生变化,如有疑问可询问作者。

单元测试

一、概念

测试基础单元(组件、类,函数),保证单元是否正常运行。

目的:单元测试能够让开发者明确知道代码结果

特点

  • 正确性:验证代码的正确性
  • 自动化:开发中,用到的console就是一种手工测试,但效率低下。单元测试通过编写测试用例,只需编写一次,做到多次运行
  • 解释性:测试用例用于测试接口、模块的重要性,其他开发者阅读这些测试用例会比看文档更清晰
  • 驱动开发,指导设计:开发者在设计API的同时,还需保证代码的可测试性
  • 保证重构:迭代项目,我们可以根据测试用例,去进行重构

二、单元测试的各种事

测试工具

组织和运行整个测试的工具,例如Karma

Karma:一个基于 Node.js 的 JavaScript 测试执行过程管理工具(Test Runner)。可用于测试所有的主流浏览器,在使用的过程中,能监控(watch)文件的变化并自行执行,通过console.log显示测试结果。

测试框架

提供单元测试的各种API,是单元测试的核心,例如JasmineMacacaMocha

---Jasmine:一种JavaScript行为驱动(BDD)单元测试框架,它不依赖任何其他JS框架,也不需要对DOM操作,具有灵巧而明确的语法可以让你轻松的编写测试代码。可运行于Node.js,浏览器端或移动端。

---Mocha:既可以在浏览器环境下运行,也可以在Node.js环境下运行。使用mocha,我们就只需要专注于编写单元测试本身,然后,让mocha去自动运行所有的测试,并给出测试结果。可以看看阮老师教测试框架 Mocha 实例教程

---Macaca:一套面向用户端软件的测试解决方案,提供了自动化驱动,环境配套,周边工具,集成方案,旨在解决终端上的测试、自动化、性能等方面的问题。

断言库

断言库提供了用于描述你的具体测试的 API,有了它们你的测试代码便能简单直接,也更为语义化,理想状态下你甚至可以让非开发人员来撰写单元测试,例如expectchaishould

测试风格

①测试驱动开发(TDD:Test-Driven Development)

关注所有的功能是否被实现,每一个功能都有对应的测试用例。

在开发功能代码之前,先编写测试用例,再通过测试来编写开发的功能代码。

TDD的具体步骤

  • 根据需要编写一个功能测试用例
  • 编写功能代码,以让刚才的测试用例通过
  • 逐步补充测试用例
  • 修改功能代码使新增的测试用例和原来的都通过
  • 重构,包括功能代码和测试用例

TDD运行流程

  • setup --> 单个测试套件里的所有测试用例开始之前执行,只执行一次
  • suiteSetup --> 每一个测试用例开始之前
  • test --> 定义测试用例,并使用断言
  • suiteTeardown --> 每个测试用例结束之后
  • teardown --> 单个测试套件里的所有测试用例结束之后执行,只执行一次
suite('Array', function() {
  setup(function() {
    // 测试用例开始之前
  });
  suite('#indexOf', function() {
    test('should return -1 when not present', function() {
      assert.equal(-1, [1, 2, 3].indexOf(4));
    });
  });
  teardown(function() {
    // 测试用例结束之后
  });
});

②行为驱动开发(BDD:Behavior Driven Development)

关注整体行为是否符合预期,编写的每一行代码都有目的提供一个全面的测试用例集

描述了软件的行为过程,可以直接作为软件的需求文档

BDD运行流程

  • before --> 单个测试套件里的所有测试用例开始之前执行,只执行一次
  • beforeEach --> 每一个测试用例开始之前
  • it --> 定义测试用例,并用断言库进行设置
  • afterEach --> 每一个测试用例结束之后
  • after --> 单个测试套件里的所有测试用例结束之后执行,只执行一次
// Jasmine为例
describe("A suite with some shared setup", function() {
    var foo = 0;

    beforeAll(function() { // 只调用一次
        foo = 1;
    });
    beforeEach(function() {// 每个it测试用例执行前,被调用一次
        foo += 1;
    });

    it('测试用例', function(){
        expect(foo).toBe(2);
    })

    it('测试用例', function(){
        expect(foo).toBe(1);
    })

    afterEach(function() {// 每个it测试用例执行后,被调用一次
        foo = 0;
    });

    afterAll(function() {// 只调用一次
        foo = 0;
    });
});

一个测试脚本由一个或多个describe测试套件组成,一个测试套件由一个或多个it测试用例组成,describe块称为"测试套件"(test suite),表示一组相关的测试。它是一个函数,第一个参数是测试套件的名称("add"),第二个参数是一个实际执行的函数。it块称为"测试用例"(test case),表示一个单独的测试,是测试的最小单位。

测试用例

测试用例(Test Case)是测试功能的代码,给一个固定的输入和输出,用来判断一个代码功能是否满足特定需求。

①普通例子

describe("测试函数", function () {
    var add = function(a, b) {
        return a + b;
    };
    it("常规输入", function () {
        expect(add(2,3)).toBe(5);
    });
});

②异步的测试用例

describe("测试Promise函数", function () {
    var testPromise = function() {
        return new Promise(function(resolve) {
            setTimeout(function() {
                resolve('ok');
            }, 1000);
        })
    }
    it("常规输入", function () {
        testPromise().then(function(ret) {
            expect(ret).toBe('ok');
        })
    });
});

测试覆盖率

测试覆盖率用来判断单元测试对代码的覆盖情况。原理是通过向源代码中注入统计代码,用于监听每一行代码的执行情况。可以统计到该行代码是否执行和执行次数。测试覆盖率包括以下几个方面:

  • 行覆盖率(line coverage):是否每一行都执行了?
  • 函数覆盖率(function coverage):是否每个函数都调用了?
  • 分支覆盖率(branch coverage):是否每个 if 代码块都执行了?
  • 语句覆盖率(statement coverage):是否每个语句都执行了?

E2E测试

E2E(End To End)即端对端测试,属于黑盒测试,通过编写测试用例,自动化模拟用户操作,确保组件间通信正常,程序流数据传递如预期。

框架:例如:nightwatch cypress testcafe (github上得链接)

使用Karma测试工具+Jasmine测试框架写个demo

先安装karma

npm install karma --save-dev

全局安装karma-cli

npm install karma-cli -g

初始化karma,生成karma.conf.js

karma init

初始化过程: 选择Jasmine --> 双模 no PhantomJS --> 无头浏览器 no

修改配置文件karma.conf.js

module.exports = function(config) {
  config.set({
    basePath: '', // 设置路径
    frameworks: ['jasmine'], // 断言库
    files: [  // 合成测试文件和测试脚本添加进来
        "src/**/*.js",
        "test/**/*.spec.js"
    ],

    exclude: [ ],
    preprocessors: {}, // 哪些东西会做覆盖率的代码检测
    reporters: ['progress'], //生成测试报表的过程
    port: 9876,
    colors: true,
    logLevel: config.LOG_INFO,
    autoWatch: false,
    browsers: ['PhantomJS'], // 测试用到的浏览器,这里用PhantomJS无头浏览器
    singleRun: true, // 独立运行
    concurrency: Infinity
  })
}

继续安装依赖

npm install karma-jasmine jasmine-core --save-dev
npm install phantom --save-dev

在src目录下写index.js用来被测试

function add(num) {
    if (num == 1) {
        return 1;
    } else {
        return num + 1;
    }
}

在根目录创建unit文件夹用来存放测试文件,index.spec.js

describe("单元测试", function() {
    it("基础测试用例",function () {
        expect(add(2)).toBe(3);
    })
})

两者通过karma.conf.js配置文件中的 files 进行连接

执行 karma start

karma代码覆盖率检测

安装覆盖率依赖

npm install karma karma-coverage --save-dev

修改karma.conf.js配置文件

module.exports = function(config) {
  config.set({
    ...
    // preprocess matching files before serving them to the browser
    // available preprocessors: https://npmjs.org/browse/keyword/karma-preprocessor
    // 添加代码覆盖率测试
    preprocessors: {
      "src/**/*.js": ['coverage']
    },

    // 在reporter里添加代码覆盖率
    reporters: ['progress', 'coverage'],
    // 生成覆盖率报告文件,指定到docs目录下
    coverageReporter:{
      type: 'html',
      dir: './docs/coverage/'
    },
    ...
  })
}

再次执行 karma start

会在根目录生成一个docs文件夹 打开里面的页面会显示测试结果 结果显示覆盖率75%,说明测试用例没有完全覆盖代码 。

调整测试用例,将所有分支都测全

describe("单元测试", function() {
    it("基础测试用例",function () {
        expect(add(1)).toBe(1);
        expect(add(2)).toBe(3);
    })
})

再次运行,打开报告 覆盖率100%,ok了!

注意:若使用es6语法规则,则需要将es6转为es5,这时需要安装如下包,webpack必须也得安

npm i webpack karma-webpack --save-dev

在对webpack进行配置,最后在karma.conf.js引入,并加上配置,然后就爽了......

总结

实际开中不会这么繁琐,只需要一个jest测试框架就够了,开箱即用,不需要我们去额外的安装一些测试工具。