Jest+Enzyme前端自动化测试

jest测试

笔者接触组件库开发已经几月了,目前组件库越做越大,越做越好,但随之而来接入的业务方提的bug也越来越多,这显得前端自动化测试格外重要,此文主要是讲测试入门以及一些思想,具体怎么配置笔者觉得没有必要花心思去整理,网上一搜一大堆。

测试类型

  • 单元测试:又称模块测试,对一个模块、类、函数等进行正确性检验工作。目标是隔离程序部件并证明这些单个部件是正确的。一个单元测试提供了代码片断需要满足的严密的书面规约。因此,单元测试带来了一些益处。 单元测试在软件开发过程的早期就能发现问题。
  • 功能测试:又称黑盒测试,是通过测试来检测每个功能是否都能正常使用。在测试中,把程序看作一个不能打开的黑盒子,在完全不考虑程序内部结构和内部特性的情况下,在程序接口进行测试,它只检查程序功能是否按照需求规格说明书的规定正常使用,程序是否能适当地接收输入数据而产生正确的输出信息。黑盒测试着眼于程序外部结构,不考虑内部逻辑结构,主要针对软件界面和软件功能进行测试。
  • 集成测试:在单元测试的基础上,将所有模块按照设计要求组装成子系统或者系统,进行测试
  • 冒烟测试: 在正式全面的测试之前,对主要功能进行的与测试,确认主要功能是否满足需要,软件是否能正常运行

jest匹配器

  • toBe (===)
  • toEqual
  • toBeNull
  • toBeTrusty
  • toBeFalsy
  • toBeGreaterThan(number)
  • toBeGreaterThanOrEqual(number)
  • toBeLessThan(number)
  • toBeLessThanOrEqual(number)
  • toBeCloseTo(number); // 近似
  • toContainer
  • toMatch可以检查字符串是否与正则表达式匹配

++jest –watchAll(类似热加载)++

1
2
3
//toBe 失败
const a = {one: 1}
expect(a).toBe({one: 1})

异步函数的测试

1
2
3
4
5
6
export const fecthData = (fn) => {
axios.get("http://www.dell-lee.com/react/api/demo.json")
.then(res => {
fn(res)
})
}
1
2
3
4
5
6
7
8
9
10
//回调类型
import fetchData from './index.js'
test('fetchData 返回结果为{ success:true }',(done) => {
fetchData((data) => {
expect(data).toEqual({
success: true
});
done();
})
})

expect.assertions(1); //至少要执行一个expect语法,expect.assertions,来测试一定数量的断言被调用,否则fulfilled态promise不会让测试失败

jest globals API

  • discribe(name,func): 块,一个测试单元的所用果能测试用例可组合为一个块
  • it(name,func,timeout): 测试用例,一般一个测试用例测试一个单一功能

beforeEach和afterEach

需要为多次测试重复设置的工作,可以使用breforeEach和afterEach。

1
2
3
4
5
6
7
beforeEach(()=>{
initializeCityDatabase(); //每次测试前都要初始化citybase
})

afterEach(()=>{
clearCityDatabase();// 每次测试之后要清空citybase
})

beforeAll和afterAll

例如,如果 initializeCityDatabase 和 clearCityDatabase 都返回了 promise ,城市数据库可以在测试中重用,我们就能把我们的测试代码改成这样

1
2
3
4
5
6
7
beforeAll(()=>{
return initializeCityDatabase();
})

afterAll(()=>{
return clearCityDatabase();
})

全局和describe都拥有上面四个生命周期。

  • 但describe的after函数优先级高于全局after函数
  • describe的before函数优先级要低于全局的before函数

mock函数

1
2
3
const func = jest.fn();   // mock函数,捕获函数的调用
func.mockReturnValueOnce('Dell'); // mock函数第一次调用的返回值
// func.mock.instance mock函数每次被调用this指向

mock函数中的作用

  • 捕获函数的调用和返回结果,以及this和调用顺序(func.mock)
  • 自由设置返回结 func.mockReturnValueOnce();
  • 改变内部函数的实现

通过mock函数来测试异步函数

1
2
3
4
5
6
7
8
9
10
11
12
13
export getData = ()=>{
axios.get("/api").then(res=>res.data);
}

// 测试
jest.mock("axios"); // 用jest来模拟ajax请求

test("test getData", async()=>{
axios.get.mockReturnValue("hello"); // 把axios的get请求都返回hello
await getData().then((data)=>{
expect(data).toBe("hello")
})
})
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
const func = jest.fn();
func.mockReturnValue("hello");

// 等同于
const func = jest.fn(()=>{
return "hello"
})

// 等同于
func.mockImplementation(()=>{
return "hello"
})


func.mockReturnThis(); //undefined;
expect(func.mock.calls[0]).toEqual(['abc']); // 第一次调用这个函数的参数是abc
expect(func.toBeCalledWidth('abc')); // 每次调用这个函数的参数都是abc
1
2
3
jest.mock('./demo');
import { fetchData } from './demo'; // 这个是从__mocks__中引入的模拟的函数
const { getNumber } = jest.requireActual('./demo'); // 这个是从真实文件夹中引入的函数。

spyOn

jest.spuOn()同样创建一个mock函数,但他比jest.fn()或jest.mock();更厉害的是,他还会执行被mock的原函数。例如

1
2
3
4
5
6
const fn = function(){
console.log("1234567");
}

const jestfn = jest.mock(fn); // 如果执行,不会打印任何东西
const jestspy = jest.spyOn(fn); // 如果执行,会打印出1234567

snapshot 快照测试

就是看你生成的快照测试跟原来一不一样,如果原来文件更改,测试会失败,也就是让你确定你是不是要更改原来的文件。

例如对一个React组件而言,传入相同的props,希望得到相同的输出。这样就可以通过快照测试来完成。

u可以 upadte
i 可以交互的更新快照

1
2
3
4
5
6
7
8
9
10
11
12
13
14
expect const config = ()=>{
return{
url: "http://localhost",
port: "8000",
time: new Date() // time是时间类型,会一直改变,所以快照是一直通不过的
}
}

// 测试config
test("测试config",()=>{
expect(config()).toMatchSnapshot({
time: expect.any(Date) // 这是说希望time只要是时间类型就好
})
})

行内snapshot和普通snapshot的区别

  • 普通: toMacthSnapshot()会把生成的快照存到一个新文件里
  • 行内: toMatchInlineSnapshot()会把生成的快照作为toMatchInlineSnapshot()的参数

mock timer

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
export const timer = (callback)=>{
setTimerout(()=>{
callback();
},3000)
}


// test
test("timer 测试",(done)=>{
timer(()=>{
expect(1).toBe(1); // 等3秒后触发
done();
})
})

// 用假的timer模拟
jest.useFakeTimers();

test("timer 测试",(done)=>{
const fn = jest.fn();
timer(fn);

jest.runOnlyPendingTimers(); //

只执行已经在队列中的timers,不会执行还未被创建的timers

jest.runAllTimers(); // 避免所有的等待时间(所有的时间函数都会被触发)

jest.advanceTimersByTime(6000); // 快进时间

expect(fn).toHaveBeenCalledTimers(1);
})


// 为了避免advanceTimersByTime()对其他测试用例的影响,可以用
beforeEach(()=>{
jest.useFakeTimers(); // 每次测试前都初始化一个实力对象。
})

mock 类

  • 使用jest.mock(moduleName,factory,options)自动mock模块,jest会自动帮我们mock指定模块中的函数,factoryoption参数是可选的。factory

    jest来测试ES6中的类

    如果引入的文件是一个类的话,jest会默认把这个类里面的构造方法和类方法用jest.fn()代替。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 源文件
class Util{
init() {
// ... 异常复杂
}
a() {
// ... 异常复杂
}
c() {
// ... 异常复杂
}
}
export default Util;

jest.mock("./util"); // 引入类

// 相当于
const Util = jest.fn();
Util.a = jest.fn();
Util.c = jest.fn();

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

  1. 编写测试用例
  2. 运行测试,测试用例无法通过测试
  3. 编写代码,使测试用例通过测试
  4. 优化代码完成开发
  5. 新功能则重复上述步骤

TDD优势

  1. 长期减少回归bug:当改变项目代码,所有测试用例会重新运行,一旦测试用例出错,就会发现原来的代码因为回归产生了bug
  2. 代码质量更高(在写测试的时候对代码有了比较完善的思考)
  3. 测试覆盖率高

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

通过与利益相关者的讨论取得对预期的软件行为的清醒认识。它通过用自然语言书写非程序员可读的测试用例扩展了测试驱动开发方法。行为驱动开发人员使用混合了领域中统一的语言的母语语言来描述他们的代码的目的。这让开发者得以把精力集中在代码应该怎么写,而不是技术细节上,而且也最大程度的减少了将代码编写者的技术语言与商业客户、用户、利益相关者、项目管理者等的领域语言之间来回翻译的代价。

DDD(Domain Drive Design) 领域驱动开发

核心:

  • 统一语言(软件的开发人员/使用人员都使用同一套语言,即对某个概念,名词的认知是统一的)
  • 面向领域(以领域去思考问题,而不是模块)

DDD优势

  1. 使用统一的一套通用语言,沟通成本会大大减小。
  2. 对使用产品的用户有好处,他能在产品不断更新过程中,有一套统一流畅的体验。用户不用在每次软件更新时都要抱怨为什么之前的一个数据保存后没有用到了
  3. 面向领域去开发产品有助于我们深入分析产品的内在逻辑,专注于解决当前产品的核心问题,而不是冗余的做很多功能模块。

Enzyme

对ReactDOM.render进行包装

三种渲染方式

  • shallow 浅渲染(浅复制),仅对当前jsx结构内的顶级组件进行渲染,而不对这些组件内部嵌套的子组件进行渲染,因此行能很快
  • mount 会进行完全渲染,而且完全依赖DOM API, 也就是说mount渲染结果跟浏览器结果一样, 结合jsdom这个工具,可以对上面提到的有内部子组件实现复杂交互功能的组件进行测试
  • render也会进行完整渲染,但不依赖DOM API, 而是渲染成HTML结构。

常用方法

  • simulate(event,mock): 用来触发事件,但一定注意在原组件要注册这个事件
1
2
3
const mockChange = jest.fn()
wrapper = mount(<input onChange={mockChange}/>)
wrapper.simulate('change',{target: {value: '1'}})
  • instance() => ReactComponent|DOMComponent :返回组件的实例。在React 16,无状态组件会返回null(即不是以class创建的组件)
  • find(selector): 查找节点,参数可以是css选择器、组件内部构造函数、组件类名等
  • at(index): 返回一个数组对象的第几个元素
  • contains(nodeOrNodes):当前对象是否包含参数重点 node,参数类型为react对象或对象数组
  • text():返回当前组件的文本内容
  • html():返回当前组件的HTML代码形式
  • props():返回根组件所有属性
  • prop(key):返回根组件指定属性
  • state():返回根组件的状态
  • setState(nextState):设置根组件的状态
  • setProps(nextProps):设置根组件的属性
  • debug(): 会打印出测试实际渲染出的内容字符串
  • first():返回节点数组的第一个元素
  • last():返回节点数组的最后一个节点

好文分享

-------------本文结束感谢您的阅读-------------