单元测试在前端中应用-jest

598 阅读5分钟

测试分为三个种类

  • 单元测试
    在计算机编程中,单元测试(英语:Unit Testing)又称为模块测试, 是针对程序模块(软件设计的最小单位)来进行正确性检验的测试工作。程序单元是应用的最小可测试部件。在过程化编程中,一个单元就是单个程序、函数、过程等;对于面向对象编程,最小单元就是方法,包括基类(超类)、抽象类、或者派生类(子类)中的方法
  • 集成测试
    集成测试,也叫组装测试或联合测试。在单元测试的基础上,将所有模块按照设计要求(如根据结构图)组装成为子系统或系统,进行集成测试。
  • 功能测试
    功能测试就是对产品的各功能进行验证,根据功能测试用例,逐项测试,检查产品是否达到用户要求的功能。

测试的好处

  • 减少bug
  • 提高代码质量
  • 快速定位问题,减少调试时间
  • 放心重构

缺点

  • 单元测试的学习成本比较高
  • 编写单元测试会增加程序员工作量
  • 推广和运用单元测试需要比较大的投入

前端框架的种类

mocha, jasmine, ava, testcafe, jest

为何选用jest

  • 方便的异步测试
  • snapshot功能
  • 集成断言库,不许要引用其他第三方库
  • 对React天生支持

jest

快速开发

首先在项目中安装 npm install --save-dev jest

添加被测试文件

// 被测试文件 sum.js
function sum(a, b) {
  return a + b;
}
module.exports = sum;

书写测试文件

// 测试文件 sum.test.js
const sum = require(‘./sum');
test('adds 1 + 2 to equal 3', () => {
  expect(sum(1, 2)).toBe(3);
});

添加测试命令在package.json(当然如果是全局安装可以直接在项目目录中执行 jest)

// 添加测试命令
{
  "scripts": {
    "test": "jest"
  }
}

控制台执行npm test

内建丰富的断言库(列出的只是一些常用的)

.toBe(value)
.toHaveBeenCalled()
.toBeFalsy()
.toEqual(value)
.toBeGreaterThan(number)
.toBeGreaterThanOrEqual(number)

方便的钩子

  • beforeEach(fn)
  • afterEach(fn)
  • beforeAll(fn)
  • afterAll(fn)

举例,被测试文件:

export default class Hook {

    constructor() {
        this.init();
    }

    init() {
        this.a = 1;
        this.b = 1;
    }

    sum() {
        return this.a  + this.b;
    }
}

测试用例

import Hook from '../src/hook';


describe('hook', () => {

    const hook = new Hook;

    // 每个测试用例执行前都会还原数据,所以下面两个测试可以通过。
    beforeEach( () => {
        hook.init();
    })


    test('test hook 1', () => {
        hook.a = 2;
        hook.b = 2;
        expect(hook.sum()).toBe(4);
    })

    test('test hook 2', () => {

        expect(hook.sum()).toBe(2);// 测试通过
    })


})

其他钩子类似可以看文档

mock(两种方式)

一. jest.fn() 方式

被测试代码every.js

function every(array, predicate) {
  let index = -1
  const length = array == null ? 0 : array.length

  while (++index < length) {
    if (!predicate(array[index], index, array)) {
      return false
    }
  }
  return true
}

export default every

foreach.js

function foreach(arr, fn) {
    for(let i = 0, len = arr.length;  i < len; i++) {
        fn(arr[i]);
    }
}

module.exports = foreach;

测试用例 mock.test.js

import foreach from '../src/foreach';
import every from '../src/every';

jest.mock('../src/sum');
import sum from '../src/sum';

describe('mock test', () => {
    it('test foreach use mock', () => {
        
        // 通过jest.fn() 生成一个mock函数
        const fn = jest.fn();

        foreach([1, 2, 3], fn);
        // 测试mock函数被调用了3次
        expect(fn.mock.calls.length).toBe(3);
       // 测试第二次调用的函数第一个参数是3
        expect(fn.mock.calls[2][0]).toBe(3);
    })

    it('test every use mock return value', () => {
        const fn = jest.fn();
        
        // 可以设置返回值
        fn
          .mockReturnValueOnce(true)
          .mockReturnValueOnce(false);


        const res = every([1, 2, 3, 4], fn);
        expect(fn.mock.calls.length).toBe(2);
        expect(fn.mock.calls[1][1]).toBe(1);
    })

    it('test every use mock mockImplementationOnce', () =>{
       // 快速定义mock的函数体,方便测试
        const fn = jest.fn((val, index) => {
            if(index == 2) {
                return false;
            }
            return true;
        });

        const res = every([1, 2, 3, 4], fn);
        expect(fn.mock.calls.length).toBe(3);
        expect(fn.mock.calls[1][1]).toBe(1);
    })

    it('test mockImplementation', () => {
       // mock函数返回值
        sum.mockImplementation(() => 15);

        expect(sum(1, 2)).toBe(15);
    })
})

二. 手动mock假如我的测试文件sum2.js

function sum2(a, b) {
    if (a > 10) return a * b;
    return a + b;
}


export default sum2;

现在如果我们要mock sum2.js 文件的话,需要在sum2.js 同级目录下新建文件夹__mock__,
然后在此文件下新建文件同名 sum2.js, 只是单纯的返回100

export default function sum2(a, b) {
    return 100;
}

测试用例mock_file.test.js


jest.mock('../src/sum2');
import sum2 from '../src/sum2';


it('test mock sum2', () => {
    // 因为此时访问的是__mock__文件夹下的sum2.js 所以测试通过
    expect(sum2(1, 11111)).toBe(100);
})

手动mock的好处是测试和模拟分离。可以很方便的修改测试用例。如果是复杂的mock建议使用手动新建文件方式

异步测试, 支持三种方式

  • done函数
  • return promise
  • async/await

直接上测试文件,因为superagent库支持 promise和async/await方式,所以用superagent举例了.
文档地址

import superagent from 'superagent';


const target = 'http://www.baidu.com';

describe('test promise async', () => {

    it('test done', done => {
        superagent.get(target).end((err, res) => {
            expect(res).toBeTruthy();
            done();
        });
    });

    it('test promise', () => {
        return superagent.get(target).then((res) => {
            expect(res).toBeTruthy();
        });
    });

    it('test async/await', async () => {
        const res = await superagent.get(target);
        expect(res).toBeTruthy();
    });
});

Snapshot Testing

快照测试第一次运行的时候会将被测试ui组件在不同情况下的渲染结果保存一份快照文件。后面每次再运行快照测试时,都会和第一次的比较。

举例拿react组件开发测试,react-comp.js

import React from 'react';

export default class RC extends React.Component {

    render() {
        return (
            <div>我是react组件 </div>
        )
    }
}

测试用例:

import React from 'react';
import renderer from 'react-test-renderer';

import RC from '../src/react-comp';

test('react-comp snapshot test', () => {
    const component = renderer.create(<RC />);
    //
    let tree = component.toJSON();
    expect(tree).toMatchSnapshot();
})

test('react-comp snapshot test2', () => {
    const component = renderer.create(<RC />);
    
    let tree = component.toJSON();
    expect(tree).toMatchSnapshot();
})

执行测试命令,会在test目录下生成一个__snapshots__目录,在此目录下会与一个文件叫snapshot.test.js.snap的快照文件

// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`react-comp snapshot test 1`] = `
<div>
  我是react组件 
</div>
`;

exports[`react-comp snapshot test2 1`] = `
<div>
  我是react组件 
</div>
`;

如果被测试代码有正常更新可以使用命令jest --updateSnapshot 重新更新缓存文件。


END, 感谢阅读。博客地址