前端开发工具集(九):单元测试(jest)

本文是前端开发工具系列之单元测试,主要会讨论利用常见的单元测试工具的原理和使用,保证前端开发时的代码正确性,该系列其他部分请参考这里。

本文后期会随着对自动化测试的探索不定时更新,最终会完成全套的自动化测试。

自动化测试分为三种类型

  • unit test 单元测试是对一个模块、一个函数或者一个类来进行正确性检验的测试工作。
  • integration test 集成测试是对多个模块作为一个整体进行测试。
  • end-to-end 即E2E 端到端测试模拟真实用户的黑盒测试。

关于测试更多,可参考vue和react官方的描述

  • vue test
  • react test

1 单元测试

单元测试和我们手动测试的逻辑是一样的,即执行某一个单元的逻辑,然后将执行结果和预期结果对比,如果一致则通过测试,否则失败。
一个测试框架通常包含两部分

  • Test Runner 测试容器,自动执行内部的测试代码(包括断言库)
  • Assertion Library 断言库,其中包含很多断言,即判断执行结果和预期是否一致。

比如

test('the best flavor is grapefruit', () => {//测试容器
  expect(bestLaCroixFlavor()).toBe('grapefruit');//断言库
});
复制代码

执行结束后会自动将测试结果输出。

每个测试文件被称为test suite,每个具体的单元测试被称为test,即测试用例。
除了对常规的算法进行测试,还可以通过Snapshots测试ui。

前端测试框架这边推荐jest,也是react和vue官方推荐的测试工具

另外可参考

  • Jest 自动化测试
  • 深入浅出前端单元测试框架的实现原理

2 jest

jest和其他前端工具差不多,比如webpack,都可以在命令行或者npm script执行,可选的配置文件,各种钩子函数,用于各种具体实现的api。想来应该可以很容易上手,下面来了解一下。

2.1 基本使用

2.1.1 配置文件

配置文件可选,可通过jest --init进行交互式创建,文件名默认为jest.config.js或者通过--config参数指定。
具体配置选项查看这里。

配置完了可以在npm scripts中添加"test": "jest",,使用--watch--watchAll可以在修改文件后自动测试。

2.1.2 代码组织

每次执行jest时,jest会根据testMatch和testRegex的配置会查找相关测试代码,我们这里选择建立一个名为__tests__的文件夹,其中的.js, .jsx, .ts and .tsx文件就会被当作测试文件。

比如我们有一个待测试函数,路径为src/index.js

export const sum=(a,b)=>a+b
复制代码

测试文件__tests__/sum.test.js,其中使用的一些jest方法为全局方法,因此不需要显式引入。

import {sum} from '../src/index'
test('adds 1 + 2 to equal 3', () => {
    expect(sum(1, 2)).toBe(3);
  });
复制代码

执行`yarn test

yarn run v1.17.3
$ jest
 PASS  __tests__3/sum.test.js
  √ adds 1 + 2 to equal 3 (2 ms)

Test Suites: 1 passed, 1 total
Tests:       1 passed, 1 total
Snapshots:   0 total
Time:        1.352 s, estimated 2 s
Ran all test suites.
Done in 2.39s.
复制代码

以上就是一个完整的单元测试,是我们测试系统的一个最小单元。

2.2 expect和mathers

当我们写一个具体的测试时,需要两部分进行对比,即使用expect将待测试结果与表示期望结果的matchers进行匹配。
最常用的expect语法是expect(value),后面添加.not表示取反。
这里主要讲一下常用matchers,除了以下之外的场景可查看expect这里的文档。

其实matcher本身就是expect对象的一个方法。

  1. Common Matchers

最简单的方式是精确匹配,其中toBe使用Object.is,如果想检查对象可以使用toEqual,后者递归处理。

test('object assignment', () => {
  const data = {one: 1};
  data['two'] = 2;
  expect(data).toEqual({one: 1, two: 2});
});
复制代码
  1. Truthiness

这里的matchers用来检查各种和布尔有关的值

  • toBeNull 匹配null
  • toBeUndefined 匹配undefined
  • toBeDefined 非undefined
  • toBeTruthy if语句作为true处理的场景
  • toBeFalsy 和上面相反
  1. Numbers

包括用于比较的matchers

test('two plus two', () => {
  const value = 2 + 2;
  expect(value).toBeGreaterThan(3);
  expect(value).toBeGreaterThanOrEqual(3.5);
  expect(value).toBeLessThan(5);
  expect(value).toBeLessThanOrEqual(4.5);

  // toBe and toEqual are equivalent for numbers
  expect(value).toBe(4);
  expect(value).toEqual(4);
});
复制代码

对于浮点类型的,由于误差,要使用

test('adding floating point numbers', () => {
  const value = 0.1 + 0.2;
  //expect(value).toBe(0.3);           This won't work because of rounding error
  expect(value).toBeCloseTo(0.3); // This works.
});
复制代码
  1. Strings

可以对字符串匹配正则

test('there is no I in team', () => {
  expect('team').not.toMatch(/I/);
});

复制代码
  1. Arrays and iterables

使用toContain检查是否包含某元素

  1. Exceptions

使用toThrow

2.3 测试异步代码

对于异步执行的代码,jest需要知道合适结束,以便去执行下一个测试。

  1. Callbacks

对于以回调执行的异步,不能直接在回调中检查,因为jest执行结束当前代码即表示完成,而回调还未执行。
可在test的回调中添加参数done,当done被调用才算完成

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

  fetchData(callback);
});
复制代码
  1. Promises

如果以promise处理异步,jest会等resolved后才处理,如果rejectd直接失败,注意此时要使用return,否则会在promise的数据返回之前结束。

test('the data is peanut butter', () => {
  return fetchData().then(data => {
    expect(data).toBe('peanut butter');
  });
});
复制代码
test('the fetch fails with an error', () => {
  expect.assertions(1);
  return fetchData().catch(e => expect(e).toMatch('error'));
});
复制代码

如果test的回调是async函数,也可以结合await处理异步。

2.4 钩子函数

钩子中的异步和其他

  1. 每个测试都执行

使用beforeEach 或 afterEach

beforeEach(() => {
  initializeCityDatabase();
});

afterEach(() => {
  clearCityDatabase();
});

test('city database has Vienna', () => {
  expect(isCity('Vienna')).toBeTruthy();
});

test('city database has San Juan', () => {
  expect(isCity('San Juan')).toBeTruthy();
});
复制代码
  1. 在本文件所有测试开始之前和全都结束后执行

使用beforeAll and afterAll
3. 作用域
除了上面的全局作用域,还可以使用describe语句创建块作用域,describe语句会比其他全局作用域的test更早执行

2.5 Mock函数

mock函数可以用于清除函数的原来实现、捕获函数调用、捕获构造函数的new调用等。
有两种mock的方法,要么使用test代码创建mock函数,要么使用manual mock覆盖模块依赖。

2.5.1 创建mock函数

可以使用jest.fn()包装一个函数,然后在返回的包装后的函数的mock属性会包含它被调用的各种状态信息。

const mockCallback = jest.fn(x => 42 + x);
forEach([0, 1], mockCallback);

// The mock function is called twice
expect(mockCallback.mock.calls.length).toBe(2);

// The first argument of the first call to the function was 0
expect(mockCallback.mock.calls[0][0]).toBe(0);

// The first argument of the second call to the function was 1
expect(mockCallback.mock.calls[1][0]).toBe(1);

// The return value of the first call to the function was 42
expect(mockCallback.mock.results[0].value).toBe(42);
复制代码

设置包装后函数的返回值

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
复制代码

mock模块

用于mock模块时,比如我们要验证一个axios请求

// users.js
import axios from 'axios';

class Users {
  static all() {
    return axios.get('/users.json').then(resp => resp.data);
  }
}

export default Users;
复制代码

我们没必要真的调用axios,而是使用jest.mock()对该模块的实现进行改写,比如使用mockResolvedValue来mock resolve的值

// 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全部实现

// foo.js
module.exports = function () {
  // some implementation;
};

// test.js
jest.mock('../foo'); // this happens automatically with automocking
const foo = require('../foo');

// foo is a mock function
foo.mockImplementation(() => 42);
foo();
// > 42
复制代码

自定义matcher

前面通过jest.fn()返回的mockfunc可以进行自定义

2.5.2 Manual Mocks

主要用于模拟数据存取,比如读取本地文件。具体参考这里

2.6 Snapshot

快照用于判断ui是否有变化,比如用于测试react组件,这个方式不会渲染整个app而是会将react tree序列化,(话说对比不同不应该git干的事么)。
比如有一个组件

import React from 'react'

export const List=()=>{
    return 5555
}
复制代码

对应测试文件

import React from 'react';
import renderer from 'react-test-renderer';
import {List} from '../src/index';

it('renders correctly', () => {
  const tree = renderer
    .create()
    .toJSON();
  expect(tree).toMatchSnapshot();
});
复制代码

执行测试后会生成snap文件表示当前一个快照,下次执行时进行对比,如果不同则会报错。如果要更新快照要使用jest --updateSnapshot

2.7 dom操作

jest在node中polyfill了一套浏览器api,可以直接操作dom。

2.8 jest对象

jest对象也是在全局作用域,其中挂载了一些属性和方法,主要包括以下几类

  • Mock Modules
  • Mock functions
  • Mock timers
  • Misc

最后:【可能给予你帮助】然后下面分享一些我的自学资料,希望可以帮到大家。

å¨è¿éæå¥å¾çæè¿°

这份资料整体是围绕着【软件测试】来进行整理的,主体内容包含:python自动化测试专属视频、Python自动化详细资料、全套面试题等知识内容。对于软件测试的的朋友来说应该是最全面和完整的备战仓库了,这个仓库也陪伴我走过了很多坎坷的路,希望也能帮助到你。

关注我的微信公众号:【 程序员小濠】免费获取~

加群:175317069,也可以获取,群里有测试大牛分享经验。

最后感谢相遇,感谢缘分,感谢支持,感谢选择,感谢信任。也祝大家可以顺利找到心仪的工作,成功转行软件测试工程师!拿下高薪!

如果我的博客对你有帮助、如果你喜欢我的博客内容,请 “点赞” “评论” “收藏” 一键三连哦!

你可能感兴趣的:(测试,单元测试,测试工具,软件测试,自动化测试,技术图文)