近年来,随着前端工程化的发展,前端发生了翻天覆地的变化。jQuery已经慢慢淡出了我们的视野,React、Vue和anglur三驾马车急速驶来。从此,前端进入了数据驱动的时代,也有了清晰的模块化开发的方式。随之而来的就是如何去保证自己的代码的正确性。

为什么需要前端自动化测试

编写测试代码要在正是写代码前进行的,它就相当于具体明确的需求文档。之后我们写的代码如果能通过测试代码就证明是符合预期的。

除此之外,由于一个项目需要多人维护,也许别人不小心改动了你的代码就会导致新的问题。所以提交代码前需要跑一遍测试用例,确保自己没有改动别人的逻辑。如果有改动别人的代码,一定要弄清楚这样改动会不会产生新的问题,最后记得把测试用例代码也要改下。

前端测试工具也和前端的框架一样纷繁复杂,其中常见的测试工具,大致可分为测试框架、断言库、测试覆盖率工具等几类。在正式开始本文之前,我们先来大致了解下它们:

测试框架

测试框架的作用是提供一些方便的语法来描述测试用例,以及对用例进行分组。

测试框架可分为两种: TDD (测试驱动开发)和 BDD (行为驱动开发),我理解两者间的区别主要是一些语法上的不同,其中 BDD 提供了提供了可读性更好的用例语法,至于详细的区别可参见 The Difference Between TDD and BDD 一文。

常见的测试框架有 Jasmine, Mocha 以及本文要介绍的 Jest

断言库

断言库主要提供语义化方法,用于对参与测试的值做各种各样的判断。这些语义化方法会返回测试的结果,要么成功、要么失败。常见的断言库有 Should.js, Chai.js 等。

测试覆盖率工具

用于统计测试用例对代码的测试情况,生成相应的报表,比如 istanbul

Jest

为什么选择Jest?

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

而作为一个面向前端的测试框架, Jest 可以利用其特有的快照测试功能,通过比对 UI 代码生成的快照文件,实现对 React 等常见框架的自动测试。

此外, Jest 的测试用例是并行执行的,而且只执行发生改变的文件所对应的测试,提升了测试速度。目前在 Github 上其 star 数已经破两万;而除了 Facebook 外,业内其他公司也开始从其它测试框架转向 Jest ,比如 Airbnb 的尝试 ,相信未来 Jest 的发展趋势仍会比较迅猛。

安装

Jest 可以通过 npm 或 yarn 进行安装。以 npm 为例,既可用npm install -g jest进行全局安装;也可以只局部安装、并在 package.json 中指定 test 脚本:

{
"scripts": {
"test": "jest"
}
}

Jest 的测试脚本名形如*.test.js,不论 Jest 是全局运行还是通过npm run test运行,它都会执行当前目录下所有的*.test.js*.spec.js 文件、完成测试。

用法

具体用法参考JEST官网,我们这里只是简单介绍几个常规用法。

用例的表示

表示测试用例是一个测试框架提供的最基本的 API , Jest 内部使用了 Jasmine 2 来进行测试,故其用例语法与 Jasmine 相同。test()函数来描述一个测试用例,举个简单的例子:

// hello.js
module.exports = () => 'Hello world'
// hello.test.js
let hello = require('hello.js') test('should get "Hello world"', () => {
expect(hello()).toBe('Hello world') // 测试成功
// expect(hello()).toBe('Hello') // 测试失败
})

其中toBe('Hello world')便是一句断言( Jest 管它叫 “matcher” ,想了解更多 matcher 请参考文档)。写完了用例,运行在项目目录下执行npm test,即可看到测试结果。

用例的预处理或后处理

有时我们想在测试开始之前进行下环境的检查、或者在测试结束之后作一些清理操作,这就需要对用例进行预处理或后处理。对测试文件中所有的用例进行统一的预处理,可以使用 beforeAll() 函数;而如果想在每个用例开始前进行都预处理,则可使用 beforeEach() 函数。至于后处理,也有对应的 afterAll()afterEach() 函数。

如果只是想对某几个用例进行同样的预处理或后处理,可以将先将这几个用例归为一组。使用 describe() 函数即可表示一组用例,再将上面提到的四个处理函数置于 describe() 的处理回调内,就实现了对一组用例的预处理或后处理:

describe('test testObject', () => {
beforeAll(() => {
// 预处理操作
}) test('is foo', () => {
expect(testObject.foo).toBeTruthy()
}) test('is not bar', () => {
expect(testObject.bar).toBeFalsy()
}) afterAll(() => {
// 后处理操作
})
})
测试异步代码

异步代码的测试,关键点在于告知测试框架测试何时完成,让其在恰当的时机进行断言。随着Babel的盛行,前端的异步写法很多都是用 Promise 的形式了,这使得我们可以用 async/await 类似同步的方式写异步。下面看下如何针对这种写法测试:

// promiseHello.js
module.exports = (name) => {
return new Promise((resolve) => {
setTimeout(() => resolve(`Hello ${name}`), 1000)
})
}
// promiseHello.test.js
let promiseHello = require('promiseHello.js') test('should get "Hello World"', async () => {
const data = await promiseHello('World');
expect(data).toBe('Hello World');
}); test('the fetch fails with an error', async () => {
expect.assertions(1);
try {
const data = await promiseHello('World');
expect(data).toBe('Hello World');
} catch (e) {
expect(e).toMatch('error');
}
});
Mock Functions

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

使用 mock 函数

假设我们要测试函数 forEach 的内部实现,这个函数为传入的数组中的每个元素调用一次回调函数。

function forEach(items, callback) {
for (let index = 0; index < items.length; index++) {
callback(items[index]);
}
}

为了测试此函数,我们可以使用一个 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属性

所有的 mock 函数都有这个特殊的 .mock属性,它保存了关于此函数如何被调用、调用时的返回值的信息。

// The function was called exactly once
expect(someMockFunction.mock.calls.length).toBe(1); // The first arg of the first call to the function was 'first arg'
expect(someMockFunction.mock.calls[0][0]).toBe('first arg'); // The second arg of the first call to the function was 'second arg'
expect(someMockFunction.mock.calls[0][1]).toBe('second arg'); // The return value of the first call to the function was 'return value'
expect(someMockFunction.mock.results[0].value).toBe('return value'); // This function was instantiated exactly twice
expect(someMockFunction.mock.instances.length).toBe(2); // The object returned by the first instantiation of this function
// had a `name` property whose value was set to 'test'
expect(someMockFunction.mock.instances[0].name).toEqual('test');

Mock 的返回值

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

在函数连续传递风格(functional continuation-passing style)的代码中时,Mock 函数也非常有效。 以这种代码风格有助于避免复杂的中间操作,便于直观表现组件的真实意图,这有利于在它们被调用之前,将值直接注入到测试中。

const filterTestFn = jest.fn();

// Make the mock return `true` for the first call,
// and `false` for the second call
filterTestFn.mockReturnValueOnce(true).mockReturnValueOnce(false); const result = [11, 12].filter(num => filterTestFn(num)); console.log(result);
// > [11]
console.log(filterTestFn.mock.calls);
// > [ [11], [12] ]

大多数现实世界例子中,实际是在依赖的组件上配一个模拟函数并配置它,但手法是相同的。 在这些情况下,尽量避免在非真正想要进行测试的任何函数内实现逻辑。

Mock模拟模块

假定有个从 API 获取用户的类。 该类用 axios 调用 API 然后返回 data,其中包含所有用户的属性:

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

现在,为测试该方法而不调用实际 API (使测试变的缓慢与不稳定),我们可以用 jest.mock(...) 函数自动模拟 axios 模块。

一旦模拟axios模块,axios的返回结果就可以被我们随意模拟值。我们可为 .get 提供一个 mockResolvedValue ,它会返回假数据用于测试。 实际上,我们想让 axios.get('/users.json') 有个假的 response。

// users.test.js
import axios from 'axios';
import Users from './users'; jest.mock('axios'); // mock模拟模块 test('should fetch users', () => {
const users = [{name: 'Bob'}];
const resp = {data: users};
axios.get.mockResolvedValue(resp); // 模拟实际调用axios后的返回值 // 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));
});

前端自动化测试的价值

近几年前端工程化的发展风起云涌,但是前端自动化测试这块内容大家却似乎不太重视。虽然项目迭代过程中会有专门的测试人员进行测试,但等他们来进行测试时,代码已经开发完成的状态。与之相比,如果我们在开发过程中就进行了测试,会有如下的好处:

  • 保障代码质量和功能的实现的完整度
  • 提升开发效率,在开发过程中进行测试能让我们提前发现 bug ,此时进行问题定位和修复的速度自然比开发完再被叫去修 bug 要快许多
  • 便于项目维护,后续任何代码更新也必须跑通测试用例,即使进行重构或开发人员发生变化也能保障预期功能的实现

当然,凡事都有两面性,好处虽然明显,却并不是所有的项目都值得引入测试框架,毕竟维护测试用例也是需要成本的。对于一些需求频繁变更、复用性较低的内容,比如活动页面,让开发专门抽出人力来写测试用例确实得不偿失。

  • 需要长期维护的项目。它们需要测试来保障代码可维护性、功能的稳定性

  • 较为稳定的项目、或项目中较为稳定的部分。给它们写测试用例,维护成本低

  • 被多次复用的部分,比如一些通用组件和库函数。因为多处复用,更要保障质量

参考:

前端测试框架 Jest

Jest测试框架入门的更多相关文章

  1. 使用Jest测试JavaScript (入门篇)

    1 什么是 Jest? Jest是 Facebook 的一套开源的 JavaScript 测试框架, 它自动集成了断言.JSDom.覆盖率报告等开发者所需要的所有测试工具,是一款几乎零配置的测试框架. ...

  2. Pytest测试框架入门到精通(一)

    Python测试框架之前一直用的是unittest+HTMLTestRunner,听到有人说Pytest很好用,所以这边给大家介绍一下Pytest的使用 pytest是一个非常成熟的全功能的Pytho ...

  3. pytest测试框架入门

    安装pytest 命令行输入: pip install -U pytest 检查是否安装了正确的版本: λ pytest --version This is pytest version 5.3.5, ...

  4. 顶级测试框架Jest指南:跑通一个完美的程序,就是教出一群像样的学生

    facebook三大项目:yarn jest metro,有横扫宇宙之势. 而jest项目的宗旨为:减少测试一个项目所花费的时间成本和认知成本. --其实,它在让你当一个好老师. jest文档非常简略 ...

  5. hapi lab测试框架简单使用

    1. 依赖安装 yarn init yarn add lab code 2. 基本模式 const Lab = require('lab'); const Code = require('code') ...

  6. Google C++测试框架系列:入门

    Google C++测试框架系列:入门 原始链接:V1_6_Primer 注 GTest或者Google Test: Google的C++测试框架. Test Fixtures: 这个词实在找不到对应 ...

  7. 前端测试框架Jest系列教程 -- 简介

    写在前面: 随着互联网日新月异的发展,用户对于页面的美观度,流畅度以及各方面的体验有了更高的要求,我们的网页不再是简单的承载文字,图片等简单的信息传递给用户,我们需要的是更加美观的页面展示,更快的浏览 ...

  8. 前端测试框架Jest系列教程 -- Asynchronous(测试异步代码)

    写在前面: 在JavaScript代码中,异步运行是很常见的.当你有异步运行的代码时,Jest需要知道它测试的代码何时完成,然后才能继续进行另一个测试.Jest提供了几种方法来处理这个问题. 测试异步 ...

  9. 前端测试框架Jest系列教程 -- Matchers(匹配器)

    写在前面: 匹配器(Matchers)是Jest中非常重要的一个概念,它可以提供很多种方式来让你去验证你所测试的返回值,本文重点介绍几种常用的Matcher,其他的可以通过官网api文档查看. 常用的 ...

随机推荐

  1. Scrapy爬虫day2——简单运行爬虫

    设置setting.py 修改机器人协议 ROBOTSTXT_OBEY = False 设置User-Agent DEFAULT_REQUEST_HEADERS = { 'Accept': 'text ...

  2. SROP的一个实例

    以前一直只是大概看过这种技术,没实践过,今天刚好遇到一道题,实践了一波,确实很方便 unmoxiao@cat ~/s/pd_ubuntu> r2 -A smallest 00:54:15 War ...

  3. 前端与算法 leetcode 283. 移动零

    目录 # 前端与算法 leetcode 283. 移动零 题目描述 概要 提示 解析 解法一:暴力法 解法二:双指针法 算法 传入[0,1,0,3,12]的运行结果 执行结果 GitHub仓库 # 前 ...

  4. windows下离线安装mysql8.0服务(支持多个安装,端口不同就可以)

      1.官网下载 mysql文件.官网下载链接:https://dev.mysql.com/downloads/mysql/ 选择mysql下载的系统版本. 此处可以下载MSI安装包,图简单的朋友可以 ...

  5. python机器学习——感知器

    最近在看机器学习相关的书籍,顺便把每天阅读的部分写出来和大家分享,共同学习探讨一起进步!作为机器学习的第一篇博客,我准备从感知器开始,之后会慢慢更新其他内容. 在实现感知器算法前,我们需要先了解一下神 ...

  6. java.io.StreamCorruptedException: invalid stream header: 00000000

    Caused by: java.io.StreamCorruptedException: invalid stream header: 00000000 at java.io.ObjectInputS ...

  7. 使用requests实现人人网登录,并做cookie维持

    import requests import re,time s = requests.Session() def doLogin(): login_url = 'http://www.renren. ...

  8. TRANK和VTP

    需求:因为公司规模逐渐扩大,出现相同部门不同办公室的情况,老板提出新的要求:相同部门可以通信,不同部门不能通信. 利用vlan: 缺点:浪费材料,应用技术手段把两条交叉线变成一条. 因此,引进trun ...

  9. on duplicate key update 的使用(数据库有就修改,没有就添加数据)

    on duplicate key update 使用:当数据库中有该数据就修改,没有就添加 MySQL语句如下: # id 不存在则添加数据,id存在就更新数据 INSERT INTO t_user( ...

  10. Oracle基础:数据库操作_数据库事务_表的锁定

    数据库操作语句: INSERT INTO 表名[(字段列表)] VALUES ( 表达式列表); 例子:INSERT INTO emp(empno,ename,job,hiredate) VALUES ...