如何做前端单元测试 您所在的位置:网站首页 单元测试常用方法是 如何做前端单元测试

如何做前端单元测试

#如何做前端单元测试| 来源: 网络整理| 查看: 265

作者:边顺

单元测试 什么是单元测试

单元测试(unit testing),是指对软件中的最小可测试单元进行检查和验证

需要访问数据库的测试不是单元测试 需要访问网络的测试不是单元测试 需要访问文件系统的测试不是单元测试 --- 修改代码的艺术 为什么要做单元测试 执行单元测试,就是为了证明这段代码的行为和我们期望的一致 进行充分的单元测试,是提高软件质量,降低开发成本的必由之路 在开发人员做出修改后进行可重复的单元测试可以避免产生那些令人不快的负作用 怎么去设计单元测试 理解这个单元原本要做什么(倒推出一个概要的规格说明(阅读那些程序代码和注释)) 画出流程图 组织对这个概要规格说明的走读(Review),以确保对这个单元的说明没有基本的错误 设计单元测试

在实践工作中,进行了完整计划的单元测试和编写实际的代码所花费的精力大致上是相同的

两个常用的单元测试方法论:

TDD(Test-driven development):测试驱动开发 BDD(Behavior-driven development):行为驱动开发 前端与单元测试 如何对前端代码做单元测试

通常是针对函数、模块、对象进行测试

至少需要三类工具来进行单元测试:

*测试管理工具 *测试框架:就是运行测试的工具。通过它,可以为 JavaScript 应用添加测试,从而保证代码的质量 *断言库 测试浏览器 测试覆盖率统计工具 测试框架选择

Jasmine:Behavior-Drive development(BDD)风格的测试框架,在业内较为流行,功能很全面,自带 asssert、mock 功能

Qunit:该框架诞生之初是为了 jquery 的单元测试,后来独立出来不再依赖于 jquery 本身,但是其身上还是脱离不开 jquery 的影子

Mocha:node 社区大神 tj 的作品,可以在 node 和 browser 端使用,具有很强的灵活性,可以选择自己喜欢的断言库,选择测试结果的 report

Jest:来自于 facebook 出品的通用测试框架,Jest 是一个令人愉快的 JavaScript 测试框架,专注于简洁明快。他适用但不局限于使用以下技术的项目:Babel, TypeScript, Node, React, Angular, Vue

如何编写测试用例(Jest + Enzyme)

通常测试文件名与要测试的文件名相同,后缀为.test.js,所有测试文件默认放在__test__文件夹中

describe块之中,提供测试用例的四个函数:before()、after()、beforeEach()和 afterEach()。它们会在指定时间执行(如果不需要可以不写)

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

describe 称为"测试套件"(test suite),it 块称为"测试用例"(test case)。

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

所有的测试都应该是确定的。 任何时候测试未改变的组件都应该产生相同的结果。 你需要确保你的快照测试与平台和其他不相干数据无关。

基础模板

describe('加法函数测试', () => { before(() => { // 在本区块的所有测试用例之前执行 }); after(() => { // 在本区块的所有测试用例之后执行 }); beforeEach(() => { // 在本区块的每个测试用例之前执行 }); afterEach(() => { // 在本区块的每个测试用例之后执行 }); it('1加1应该等于2', () => { expect(add(1, 1)).toBe(2); }); it('2加2应该等于4', () => { expect(add(2, 2)).toBe(42); }); });

常用的测试

组件中的方法测试

it('changeCardType', () => { let component = shallow(); expect(component.instance().cardType).toBe('initCard'); component.instance().changeCardType('testCard'); expect(component.instance().cardType).toBe('testCard'); });

模拟事件测试

通过 Enzyme 可以在这个返回的 dom 对象上调用类似 jquery 的 api 进行一些查找操作,还可以调用 setProps 和 setState 来设置 props 和 state,也可以用 simulate 来模拟事件,触发事件后,去判断 props 上特定函数是否被调用,传参是否正确;组件状态是否发生预料之中的修改;某个 dom 节点是否存在是否符合期望

it('can save value and cancel', () => { const value = 'edit'; const { wrapper, props } = setup({ editable: true, }); wrapper.find('input').simulate('change', { target: { value } }); wrapper.setProps({ status: 'save' }); expect(props.onChange).toBeCalledWith(value); });

使用 snapshot 进行 UI 测试

it('App -- snapshot', () => { const renderedValue = renderer.create().toJSON(); expect(renderedValue).toMatchSnapshot(); }); 真实用例分析(组件)

写一个单元测试你需要这样做

看代码,熟悉待测试模块的功能和作用 设计测试用例必须覆盖到组件的各种情况 对错误情况的测试

通常测试文件名与要测试的文件名相同,后缀为.test.js,所有测试文件默认放在test文件夹中,一般测试文件包含下列内容:

全局设置:一些前置配置,mock 的全局或第三方方法、进行一些重复的组件初始化工作,,当多个测试用例有相同的初始化组件行为时,可以在这里进行挂载和销毁 UI 测试:为组件打快照,第一次运行测试命令会在目录下生成一个组件的 DOM 节点快照,在之后的测试命令中会与快照文件进行 diff 对照,避免在后面对组件进行了非期望的 UI 更改 关键行为:验证组件的基本行为(如:Checkbox 组件的勾选行为) 事件:测试各种事件的触发 属性:测试传入不同属性值是否得到与期望一致的结果

accordion 组件

// accordion.test.tsx import { afterEach, beforeEach, describe, expect, jest, test } from '@jest/globals'; import Enzyme, { mount } from 'enzyme'; import Adapter from 'enzyme-adapter-react-16'; import toJSON from 'enzyme-to-json'; import JestMock from 'jest-mock'; import React from 'react'; import { Accordion } from '..'; Enzyme.configure({ adapter: new Adapter() }); // 需要根据项目的react版本来配置适配 describe('Accordion', () => { // 测试套件,通过 describe 块来将测试分组 let onChange: JestMock.Mock; // Jest 提供的mock 函数,擦除函数的实际实现、捕获对函数的调用 let wrapper: Enzyme.ReactWrapper; beforeEach(() => { // 在运行测试前做的一些准备工作 onChange = jest.fn(); wrapper = mount( two two three four ); }); afterEach(() => { // 在运行测试后进行的一些整理工作 wrapper.unmount(); }); // UI快照测试,确保你的UI不会因意外改变 test('Test snapshot', () => { // 测试用例,需要提供详细的测试用例描述 expect(toJSON(wrapper)).toMatchSnapshot(); }); // 事件测试 test('should trigger onChange', () => { wrapper.find('.qtc-accordion-item-header').first().simulate('click'); expect(onChange.mock.calls.length).toBe(1); expect(onChange.mock.calls[0][0]).toBe('one'); }); // 关键逻辑测试 //点击头部触发展开收起 test('should expand and collapse', () => { wrapper.find('.qtc-accordion-item-header').at(2).simulate('click'); expect(wrapper.find('.qtc-accordion-item').at(2).hasClass('active')).toBeTruthy(); }); // 配置disabled时不可展开 test('should not trigger onChange when disabled', () => { wrapper.find('.qtc-accordion-item-header').at(1).simulate('click'); expect(onChange.mock.calls.length).toBe(0); }); // 对所有的属性配置进行测试 // 是否展示头部左侧图标 test('hide icon', () => { expect(wrapper.find('.qtc-accordion-item-header').at(2).children().length).toBe(2); }); // 自定义图标 test('custom icon', () => { const customIcon = wrapper.find('.qtc-accordion-item-header').at(3).children().first(); expect(customIcon.getDOMNode().innerHTML).toBe('custom'); }); // 是否可展开多项 test('single expand', () => { onChange = jest.fn(); wrapper = mount( 1 2 ); wrapper.find('.qtc-accordion-item-header').at(0).simulate('click'); wrapper.find('.qtc-accordion-item-header').at(1).simulate('click'); expect(wrapper.find(Accordion).state().activeNames).toEqual(new Set(['2'])); }); test('mutiple expand', () => { onChange = jest.fn(); wrapper = mount( 1 2 ); wrapper.find('.qtc-accordion-item-header').at(0).simulate('click'); wrapper.find('.qtc-accordion-item-header').at(1).simulate('click'); expect(wrapper.find(Accordion).state().activeNames).toEqual(new Set(['1', '2'])); }); }); 难点记录

对一些异步和延时的处理

使用单个参数调用 done,而不是将测试放在一个空参数的函数,Jest 会等 done 回调函数执行结束后,结束测试

test('the data is peanut butter', done => { function callback(data) { try { expect(data).toBe('peanut butter'); done(); } catch (error) { done(error); } } fetchData(callback); });

模拟 setTimeout

// 提取utils方法,封装一个sleep export const sleep = async (timeout = 0) => { await act(async () => { await new Promise((resolve) => globalTimeout(resolve, timeout)); }); }; // 测试用例中调用 it('测试用例', async () => { doSomething(); await sleep(1000); doSomething(); });

mock 组件内系统函数的返回结果

对于组件内调用了 document 上的方法,可以通过 mock 指定方法的返回值,来保证一致性

const getBoundingClientRectMock = jest.spyOn( HTMLHeadingElement.prototype, 'getBoundingClientRect', ); beforeAll(() => { getBoundingClientRectMock.mockReturnValue({ width: 100, height: 100, top: 1000, } as DOMRect); }); afterAll(() => { getBoundingClientRectMock.mockRestore(); });

直接调用组件方法

通过 wrapper.instance()获取组件实例,再调用组件内方法,如:wrapper.instance().handleScroll() 测试系统方法的调用

const scrollToSpy = jest.spyOn(window, 'scrollTo'); const calls = scrollToSpy.mock.calls.length; expect(scrollToSpy.mock.calls.length).toBeGreaterThan(calls);

使用属性匹配器代替时间

当快照有时间时,通过属性匹配器可以在快照写入或者测试前只检查这些匹配器是否通过,而不是具体的值

it('will check the matchers and pass', () => { const user = { createdAt: new Date(), id: Math.floor(Math.random() * 20), name: 'LeBron James', }; expect(user).toMatchSnapshot({ createdAt: expect.any(Date), id: expect.any(Number), }); }); 附录 JEST 语法 匹配器

expect:返回一个'期望‘的对象

toBe:使用 object.is 去判断相等

toEqual:递归检测对象或数组的每个字段

not:测试相反的匹配

真值

toBeNull:只匹配 null

toBeUndefined:只匹配 undefined

toBeDefined:与 toBeUndefined 相反

toBeTruthy:匹配任何 if 语句为真

toBeFalsy:匹配任务 if 语句为假

数字

toBeGreaterThan:大于

toBeGreaterThanOrEqual:大于等于

toBeLessThan:小于

toBeLessThanOrEqual:小于等于

toBeCloseTo:比较浮点数相等

字符串

toMatch:匹配字符串

Array

toContain:检测一个数组或可迭代对象是否包含某个特定项

例外

toThrow:测试某函数在调用时是否抛出了错误

自定义匹配器 // The mock function was called at least once expect(mockFunc).toHaveBeenCalled(); // The mock function was called at least once with the specified args expect(mockFunc).toHaveBeenCalledWith(arg1, arg2); // The last call to the mock function was called with the specified args expect(mockFunc).toHaveBeenLastCalledWith(arg1, arg2); // All calls and the name of the mock is written as a snapshot expect(mockFunc).toMatchSnapshot(); 测试异步代码 回调

默认情况下,一旦到达运行上下文底部 Jest 测试立即结束,使用单个参数调用 done,而不是将测试放在一个空参数的函数,Jest 会等 done 回调函数执行结束后,结束测试。

test('the data is peanut butter', (done) => { function callback(data) { try { expect(data).toBe('peanut butter'); done(); } catch (error) { done(error); } } fetchData(callback); }); Promises

为你的测试返回一个 Promise,Jest 会等待 Promise 的 resove 状态,如果 Promist 被拒绝,则测试将自动失败

test('the data is peanut butter', () => { return fetchData().then((data) => { expect(data).toBe('peanut butter'); }); });

如果期望 Promise 被 Reject,则需要使用 .catch 方法。 请确保添加 expect.assertions 来验证一定数量的断言被调用。 否则,一个 fulfilled 状态的 Promise 不会让测试用例失败

test('the fetch fails with an error', () => { expect.assertions(1); return fetchData().catch(e => expect(e).toMatch('error')); }); .resolves/.rejects test('the data is peanut butter', () => { return expect(fetchData()).resolves.toBe('peanut butter'); }); test('the fetch fails with an error', () => { return expect(fetchData()).rejects.toMatch('error'); }); Async/Await

写异步测试用例时,可以再传递给 test 的函数前面加上 async。

安装和移除

为多次测试重复设置:beforeEach、afterEach 来为多次测试重复设置的工作

一次性设置:beforeAll、afterAll 在文件的开头做一次设置

作用域:可以通过 describe 块将测试分组,before 和 after 的块在 describe 块内部时,则只适用于该 describe 块内的测试

模拟函数

Mock 函数允许你测试代码之间的连接——实现方式包括:擦除函数的实际实现、捕获对函数的调用(以及在这些调用中传递的参数)、在使用 new 实例化时捕获构造函数的实例、允许测试时配置返回值。

两种方法可以模拟函数:1.在测试代码中创建一个 mock 函数,2.编写一个手动 mock 来覆盖模块依赖

mock 函数 const mockCallback = jest.fn((x) => 42 + x); forEach([0, 1], mockCallback); // 此 mock 函数被调用了两次 expect(mockCallback.mock.calls.length).toBe(2); // 第一次调用函数时的第一个参数是 0 expect(mockCallback.mock.calls[0][0]).toBe(0); // 第二次调用函数时的第一个参数是 1 expect(mockCallback.mock.calls[1][0]).toBe(1); // 第一次函数调用的返回值是 42 expect(mockCallback.mock.results[0].value).toBe(42); .mock 属性

所有的 mokc 函数都有这个特殊的.mock 属性,它保存了关于此函数如何被调用、调用时的返回值的信息。.mock 属性还追踪每次调用时的 this 的值,所以我们同样可以检查 this

// 这个函数被实例化两次 expect(someMockFunction.mock.instances.length).toBe(2); // 这个函数被第一次实例化返回的对象中,有一个 name 属性,且被设置为了 'test’ expect(someMockFunction.mock.instances[0].name).toEqual('test'); Mock 的返回值 const myMock = jest.fn(); console.log(myMock()); // > undefined myMock.mockReturnValueOnce(10).mockReturnValueOnce('x').mockReturnValue(true); console.log(myMock(), myMock(), myMock(), myMock()); // > 10, 'x', true, true 模拟模块

可以 用 jest.mock(...)函数自动模拟 axios 模块,一旦模拟模块,我们可为.get 提供一个 mockResolveValue,它会返回假数据用于测试

// users.test.js import axios from 'axios'; import Users from './users'; jest.mock('axios'); test('should fetch users', () => { const users = [{ name: 'Bob' }]; const resp = { data: users }; axios.get.mockResolvedValue(resp); // or you could use the following depending on your use case: // axios.get.mockImplementation(() => Promise.resolve(resp)) return Users.all().then((data) => expect(data).toEqual(users)); }); Mock 实现

用 mock 函数替换指定返回值:jest.fn(cb => cb(null, true))

用 mockImplementation 根据别的模块定义默认的 mock 函数实现:jest.mock('../foo'); const foo = require('../foo');foo.mockImplementation(() => 42);

当你需要模拟某个函数调用返回不同结果时,请使用 mockImplementationOnce 方法

.mockReturnThis()函数来支持链式调用

Mock 名称

可以为你的 Mock 函数命名,该名字会替代 jest.fn() 在单元测试的错误输出中出现。 用这个方法你就可以在单元测试输出日志中快速找到你定义的 Mock 函数

const myMockFn = jest .fn() .mockReturnValue('default') .mockImplementation((scalar) => 42 + scalar) .mockName('add42'); 快照测试

当要确保你的 UI 不会又意外的改变时,快照测试是非常有用的工具;典型的做法是在渲染了 UI 组件之后,保存一个快照文件, 检测他是否与保存在单元测试旁的快照文件相匹配。 若两个快照不匹配,测试将失败:有可能做了意外的更改,或者 UI 组件已经更新到了新版本。

快照文件应该和项目代码一起提交并做代码评审

更新快照

jest --updateSnapshot/jest -u,这将为所有失败的快照测试重新生成快照文件。 如果我们无意间产生了 Bug 导致快照测试失败,应该先修复这些 Bug,再生成快照文件;只重新生成一部分的快照文件,你可以使用--testNamePattern 来正则匹配想要生成的快照名字

属性匹配器

项目中常常会有不定值字段生成(例如 IDs 和 Dates),针对这些情况,Jest 允许为任何属性提供匹配器(非对称匹配器)。 在快照写入或者测试前只检查这些匹配器是否通过,而不是具体的值

it('will check the matchers and pass', () => { const user = { createdAt: new Date(), id: Math.floor(Math.random() * 20), name: 'LeBron James', }; expect(user).toMatchSnapshot({ createdAt: expect.any(Date), id: expect.any(Number), }); }); 覆盖率

Jest 还提供了生成测试覆盖率报告的命令,只需要添加上 --coverage 这个参数即可生成,再加上--colors 可根据覆盖率生成不同颜色的报告(



【本文地址】

公司简介

联系我们

今日新闻

    推荐新闻

    专题文章
      CopyRight 2018-2019 实验室设备网 版权所有