单元测试--(二)Jest测试工具介绍

552 阅读7分钟

前言

  • Jest介绍
  • Jest配置
  • Jest例子

一. Jest介绍

Jest 是 Facebook 出品的一个测试框架,相对其他测试框架, 其一大特点就是就是内置了常用的测试工具,比如自带测试执行器、断言、mock、snapshot和测试覆盖率工具,实现了开箱即用。

Jest中文文档地址

1.1 Jest特点:

  • 易用性:基于Jasmine,提供断言库,支持多种测试风格
  • 适应性:Jest是模块化、可扩展和可配置的
  • 沙箱:Jest内置了JSDOM,能够模拟浏览器环境,并且并行执行
  • 快照测试:Jest能够对React组件树进行序列化,生成对应的字符串快照,通过比较字符串提供高性能的UI检测
  • Mock系统:Jest实现了一个强大的Mock系统,支持自动和手动mock
  • 支持异步代码测试:支持Promise和async/await
  • 自动生成静态分析结果:内置Istanbul,测试代码覆盖率,并生成对应的报告

1.2 Jest配置

jest.config.js配置

const path = require('path');
module.exports = {
    // 测试环境,这个字段可以选择node或者是jsdom两个选项
    testEnvironment: 'jsdom',
    // 显示测试用例和时间
    verbose: true,
    // 测试文件的类型 告诉 Jest 需要处理的文件后缀
    moduleFileExtensions: [
        'js',
        'jsx'
    ],
    // 根目录
    rootDir: path.resolve(__dirname, './'),
    // 忽略文件夹
    testPathIgnorePatterns: ['/node_modules/'],
    // 匹配的文件  需要执行哪些目录下的测试用例
    testMatch: [
        '<rootDir>/test/**/*.test.js',
    ],

    // 支持源代码中相同的 `@` -> `src` 别名
    moduleNameMapper: {
        "^@/(.*)$": "<rootDir>/$1"
    },

    // 是否开启代码测试覆盖率【建议开启】
    collectCoverage: false,
    // 列出包含reporter名字的列表,而Jest会用他们来生成覆盖报告
    coverageReporters: ["html", "text-summary"],

    // 覆盖率报告输出地址
    coverageDirectory: '<rootDir>/test/coverage',

    // 添加 collectCoverageFrom 数组来定义需要收集测试覆盖率信息的文件
    // 测试报告想要覆盖那些文件,前面加!是避开这些文件
    collectCoverageFrom: [
        'src/components/**/*.{js, jsx}',
        'test/**/*.{js, jsx}',
        '!src/main.js',
        '!**/node_modules/**'
    ]
};

二. Jest例子

2.1 describe、test与expect

describe 称为"测试套件或测试分组"(test suite)

test (也可以写成 it)称为"测试用例"(test case)

expect 就是判断源码的实际执行结果与预期结果是否一致,如果不一致就抛出一个错误.

测试文件中应包括一个或多个describe, 每个describe中可以有一个或多个it, 每个it中可以有一个或多个expect.

const my = {
	name : "james"
};
describe("my info", ()=>{
	test("my name", ()=>{
		expect(my.name).toBe("james")
	});

	test.skip("my name skip", ()=>{
		expect(my.name).toBe("james")
	});
});

2.2 Jest 匹配器

断言库是Jest自己的,区别于Chai断言库 www.jestjs.cn/docs/expect

注意:

 Jest: toBe(value)
 Chai: to.be.a(value)

常用的:

    expect({a:1}).toBe({a:1}) // 判断两个对象是否相等
    expect(1).not.toBe(2)     // 判断不等
    expect(n).toBeNull();     // 判断是否为null
    expect(n).toBeUndefined(); // 判断是否为undefined
    expect(n).toBeDefined();   // 判断结果与toBeUndefined相反
    expect(n).toBeTruthy();    // 判断结果为true
    expect(n).toBeFalsy();     // 判断结果为false
    expect(value).toBeGreaterThan(3); // 大于3
    expect(value).toBeGreaterThanOrEqual(3.5); // 大于等于3.5
    expect(value).toBeLessThan(5); //  小于5
    expect(value).toBeLessThanOrEqual(4.5); // 小于等于4.5
    expect(value).toBeCloseTo(0.3); // 浮点数判断相等
    expect('Christoph').toMatch(/stop/); // 正则表达式判断
    expect(['one','two']).toContain('one'); // 含有某个元素
    expect(toJson(wrapper)).toMatchSnapshot() // 测试快照
    expect(fn).toHaveBeenCalled() // 判断函数是否被调用
    expect(fn).toHaveBeenCalledTimes(number) // 判断函数被调用次数
    
    注意:
        `toBeNull` 只匹配 `null`
        `toBeUndefined` 只匹配 `undefined`
        `toBeDefined` 与 `toBeUndefined` 相反
        `toBe` 对比的原理是Object.is(value1, value2), 遵循 sameValue算法

2.3 Jest中的[钩子函数]

beforeAll(fn) 在测试用例执行之前运行 多用于做初始化工作

afterAll(fn) 在所有测试用例执行结束之后运行 多用于做测试完毕后的清理工作

beforeEach(fn) 在每一个测试用例执行之前运行

afterEach(fn) 在每一个测试用例执行之后运行

2.4 工具库测试

// common.js
export const add = (a, b) => {
    return a + b
}


export const sum = (list) => {
    return list.reduce((acc, cur) => acc + cur, 0)
}


export const addItem = (list, item) => {
    let ret = [...list]
    ret.push(item)
    return ret
}
import { add, sum, addItem } from '@/src/utils/common'

describe("test utils", () => {
    it("add", () => {
        expect(add(1, 2)).toBe(3)
    })

    it("sum", () => {
        expect(sum([1, 2, 3, 4])).toBe(10)
    })

    it("addItem", () => {
        expect(addItem([1, 2, 3], 4)).toHaveLength(4)
    })
})

2.5 testEnvironment之jsdom

jsdom 是一个基于 JavaScript 的无头浏览器,可用于创建真实的测试环境。 有了它才可以在node环境下操作DOM

jsdom的简单使用:

const jsdom = require("jsdom");
const { JSDOM } = jsdom;
 
const dom = new JSDOM(`<!DOCTYPE html><p>Hello world</p>`);
// console.log(dom.window)

// node环境下不能直接通过document来获取DOM节点
console.log(dom.window.document.querySelector("p").textContent); // "Hello world"
 
const $ = require('jquery')(dom.window);
 
console.log($('p').text());

有了jsdom测试环境,才使得在jest中可以通过jQuery来获取dom

// 可以通过jquery来获取DOM元素
const $ = require('jquery');

let container;

// 初始化工作
beforeEach(() => {
  container = document.createElement('div');
  container.className = 'con'
  container.innerHTML = 'value'
  document.body.appendChild(container);
});

// 测试完成之后的清理工作
afterEach(() => {
  document.body.removeChild(container);
  container = null;
});

it('container content', () => {
  expect($('.con').html()).toBe('value');
});

2.6 支持Promise和async/await

// mock.js
const users = {
    4: {
      name: 'hehe',
    },
    5: {
      name: 'haha',
    },
};
  
export default function getUserName(userID) {
    return new Promise((resolve, reject) => {
        process.nextTick(() => {
            users[userID] ?
                resolve(users[userID].name) :
                reject({
                    error: `User with ${userID} not found.`,
                });
        });
    });
}
import getUserName from './mock'


describe('test Promise', () => {
    // 使用'.resolves'来测试promise成功时返回的值
    it('works with resolves', () => {
        expect(getUserName(5)).resolves.toEqual('haha')
    });
    
    // 使用'.rejects'来测试promise失败时返回的值
    it('works with rejects', () => {
        expect(getUserName(3)).rejects.toEqual({
            error: 'User with 3 not found.',
        });
    });
    
    // 使用promise的返回值来进行测试
    it('test resolve with promise', async (done) => {
        let data = await getUserName(4);
        expect(data).toEqual('hehe');

        // 我们必须加入一个done方法,保证我们的回调已经完成了,这时候我们表示测试完成
        // 否则jest不知道什么时候停止
        done();
    });
    
    it('test error with promise', async (done) => {
        try {
            await getUserName(2);
        } catch(e){
            expect(e).toEqual({
                error: 'User with 2 not found.',
            });
        }
        done();
    });
});

三. Mock Funtions

mock-funtions-Api

3.1 Jest Mock 的匹配器

    Jest 匹配器中还有一类匹配器专门用来检查 jest mock() 的,比如:

    名字
        mockFn.mockName(value)
        mockFn.getMockName()
    运行情况
        mockFn.mock.calls:传的参数
        mockFn.mock.results:得到的返回值
        mockFn.mock.instances:mock 包装器实例
    模拟函数
        mockFn.mockImplementation(fn):重新声明被 mock 的函数
        mockFn.mockImplementationOnce(fn)
    模拟结果
        mockFn.mockReturnThis()
        mockFn.mockReturnValue(value)
        mockFn.mockReturnValueOnce(value)
        mockFn.mockResolvedValue(value)
        mockFn.mockResolvedValueOnce(value)
        mockFn.mockRejectedValue(value)
        mockFn.mockRejectedValueOnce(value)

3.2 jest.fn()

返回一个全新没有使用过的mock function,这个function在被调用的时候会记录很多和函数调用有关的信息

在实际项目的单元测试中,jest.fn()常被用来进行某些有回调函数的测试;

如果没有定义函数内部的实现,jest.fn()会返回undefined作为返回值。

const runCallback = (callback) => {
    return callback();
};
test('测试 runCallback', () => {
    const mockFn = jest.fn()
    runCallback(mockFn)
    expect(mockFn).toBeCalled()
    expect(mockFn()).toBeUndefined()
    
    // console.log(func.mock)
})

test('测试jest.fn()返回固定值', () => {
  let mockFn = jest.fn().mockReturnValue('default');
  // 断言mockFn执行后返回值为default
  expect(mockFn()).toBe('default');
})

test('测试jest.fn()内部实现', () => {
  let mockFn = jest.fn((num1, num2) => {
    return num1 * num2;
  })
  // 断言mockFn执行后返回100
  expect(mockFn(10, 10)).toBe(100);
})

test('测试 mock', () => {
  const func = jest.fn((x) => {
    return x;
  });
  func('a');
  func('b');
  expect(func.mock.calls.length).toBe(2);
  expect(func.mock.calls[0][0]).toEqual('a');
  expect(func.mock.calls[0][1]).toEqual('b');
  
  // console.log(func.mock);
})

test('测试jest.fn()返回Promise', async () => {
  let mockFn = jest.fn().mockResolvedValue('hello promise');
  let result = await mockFn();
  // 断言mockFn通过await关键字执行后返回值为 hello promise
  expect(result).toBe('hello promise');
  // 断言mockFn调用后返回的是Promise对象
  expect(Object.prototype.toString.call(mockFn())).toBe("[object Promise]");
})

测试axios请求

// fetch
import axios from 'axios';

export default {
    async fetchPostsList(callback){
      return axios.get('https://sijwywb.ecej.com/jwywb/city/getOpenCityList').then(res => {
        return callback(res.data);
      })
    }
} 

import fetch from './src/fetch'


test('测试axios请求', async (done) => {
  let mockFn = jest.fn();
  await fetch.fetchPostsList(mockFn);

  // 断言mockFn被调用
  expect(mockFn).toBeCalled();
  expect(mockFn.mock.calls[0][0]['resultCode']).toBe(200);
  // console.log('22', mockFn.mock.calls[0][0]);
  
  // done的目的是告诉Jest异步请求完成,否则会一直等待
  done();
})

3.3 jest.mock(moduleName)

使用jest.mock()去mock整个模块 在jest中如果想捕获函数的调用情况,则该函数必须被mock或者spy

// events.js
import fetch from './fetch';

export default {
  async getPostList() {
    return fetch.fetchPostsList(data => {
      console.log('fetchPostsList be called!');
    });
  }
}
import events from './src/events';
import fetch from './src/fetch';

jest.mock('./src/fetch.js');

test('mock 整个 fetch.js模块', async (done) => {
  await events.getPostList();
  expect(fetch.fetchPostsList).toHaveBeenCalled();
  expect(fetch.fetchPostsList).toHaveBeenCalledTimes(1);

  await events.getPostList();
  expect(fetch.fetchPostsList).toHaveBeenCalledTimes(2);
  done();
});

3.4 jest.spyOn(object, methodName)

jest.spyOn()方法同样创建一个mock函数,但是该mock函数不仅能够捕获函数的调用情况,还可以正常的执行被spy的函数。

返回一个mock function,和jest.fn相似,但是能够追踪object[methodName]的调用信息,类似Sinon

        当需要测试某些必须被完整执行的方法时,常常需要使用jest.spyOn()。
import events from './src/events';
import fetch from './src/fetch';


test('使用jest.spyOn()监控fetch.fetchPostsList被正常调用', async(done) => {
  const spyFn = jest.spyOn(fetch, 'fetchPostsList');
  await events.getPostList();
  expect(spyFn).toHaveBeenCalled();
  expect(spyFn).toHaveBeenCalledTimes(1);

  done();
});

image.png

系列文章

单元测试--(一)前端测试的简单介绍

单元测试--(二)Jest测试工具介绍

单元测试--(三)React + Enzyme

单元测试--(四)周边生态库介绍