前端单元测试---孤勇者级教程

test(‘requestSuccess方法 请求正常测试用例’, () => {
const input = {
data: {
success: true,
code: 200,
message: ‘请求成功’
}
};
const expectOutput = input;
const output = requestSuccess(input);
expect(output).toEqual(expectOutput);
});


在上面的案例中,我们来逐步分解:


首先定义了一个输入:input。


然后将input作为参数,调用了requestSuccess方法,本次调用得到另一个返回值output。


最后就是判定,判定(也就是所谓的期望/断言)得到的输出值output等于期望的输出值expectOutput。


这是一段最基础的,正常输入值的单元测试代码,我们可以总结出大概的步骤:


1、定义输入值


2、将输入值带上,执行代码,得到输出值


3、对输出值进行断言


这个断言就是说你觉得这个输出值应该是什么,也断言这个输出值和你期望输出值匹配。当然,实际输出值和你的期望输出可能不匹配,那就是表明你的这条用例执行失败。失败的原因可能是你的源代码有问题,也可能是你单元测试的用例代码有问题。


OK,我们了解了最基础的单元测试。那么真正意义的单元测试应该怎么写呢?


**无非就是写单元测试用例,定义各种输入值,判断和期望输出是否一致,然后进行分析修改。**


再回归到上面的requestSuccess方法,上面的测试用例仅仅是验证了正常情况下,当然这种情况可能占大多数,但是单元测试一般就是为了兼容小部分的异常场景。


那么接下来,我们就来分析下一般意义上请求失败场景的测试用例:



// utils.test.js
import { requestSuccess } from ‘./utils’;

test(‘requestSuccess方法 请求失败测试用例’, () => {
const input = {
data: {
success: false,
message: ‘请求失败’
}
};
const output = requestSuccess(input); // 没有返回值,output为undefine
expect(output).toBeUndefined();
});


好了,到这里,有的同学说,请求正常、请求异常的情况都覆盖了,单元测试完成,可以提交测试,然后进行愉快的摸鱼了。


等等,事情没有那么简单。


测试同学急急忙忙来找你了,说你的程序又崩了,页面空白了。


你让测试同学给你复现了,一步一步debug。原来发现,调用你requestSuccess方法的response参数,尽然为一个空对象: {} 。


你可能会直呼好家伙,后端不讲武德啊(当然可能性很多,可能并不是后端一个人的锅),因为不可抗拒因素,你又得去改代码,一边改一边骂。


改完之后的源码如下,然后你又得意的告诉测试同学已经改完,没有问题了。



export const requestSuccess = response => {
if (
response.data?.success === true ||
response.data?.code === ‘success’ ||
response.data?.code === ‘1000’ ||
response.data?.code === 200 ||
response.data?.code === ‘200’
) {
return response;
}
message.error(response.data.message || ‘请求失败’);
};


结果不一会,测试同学说,你的程序又崩了,页面空白了。


你慌了,自言自语的说道,没问题啊,劳资都写了兼容了,让测试同学给你复现了,一步一步debug。原来发现,调用你requestSuccess方法的response参数,尽然为undefined。你破口大骂,告诉测试是后端的锅,是另一个前端瞎鸡儿调用,和你无关。掰扯了一段时间,你又改了下你的代码:



// 当然下面的代码还是可以继续优化
export const requestSuccess = response => {
if (
response?.data?.success === true ||
response?.data?.code === ‘success’ ||
response?.data?.code === ‘1000’ ||
response?.data?.code === 200 ||
response?.data?.code === ‘200’
) {
return response;
}
message.error(response.data.message || ‘请求失败’);
};


再回到单元测试的正题上,上面的那些异常情况,在实际项目运行中比比皆是。而除了配合测试同学发现bug然后修改之外,我们在单元测试的时候即可发现,并优化自己的代码。


例如requestSuccess针对这个方法而言,我们先不用去管实际调用时候什么请求成功、请求失败,只去针对这个方法本身,调用requestSuccess方法的参数可能性是非常多的,各种类型的,所以我们可以以每一种类型的输入值作为一条测试用例。



// utils.test.js
import { requestSuccess } from ‘./utils’;

// 这个describe可以不用纠结,理解成几份用例的集合,只是统一为异常输入的描述
describe(‘requestSuccess方法异常输入测试用例’, () => {
test(‘response为空对象测试’, () => {
const input = {};
const output = requestSuccess(input);
expect(output).toBeUndefined();
});

test(‘response为undefined测试’, () => {
const output = requestSuccess();
expect(output).toBeUndefined();
});

test(‘response为Number类型测试’, () => {
const output = requestSuccess(123);
expect(output).toBeUndefined();
});
});


在写了这么多的异常输入的测试用例之后,你会发现你一开始写的**requestSuccess**不够强大,导致单元测试用例执行失败,所以你可以一遍又一遍的修改你的源码,直至测试用例都通过。


**总结: 如何进行有效的单元测试,最简单的做法就是考虑各种异常/边界输入值,编写相应的测试用例,通过单元测试的执行,优化你的代码。**


当然做好单元测试,并不仅仅只是说考虑各种异常输入即可,实际还会涉及到开发时候 的考虑(比如常说的测试驱动开发之类的)以及非常多的实现细节,这个可能就需要你慢慢的理解了。


### Jest


### 介绍


[官方链接]( )


Jest is a delightful JavaScript Testing Framework with a focus on simplicity.


It works with projects using: [Babel]( ), [TypeScript]( ), [Node]( ), [React]( ), [Angular]( ), [Vue]( ) and more!


官方的介绍就是上面2段话,就是说jest是一个让人愉悦的js测试框架,专注于简单性。可以配合babel、ts、node、react、angular、vue等其他库 一起使用。


我们前文提及的什么describe、test、expect方法等等在Jest中都有相应的api。


### 一、基础教程


### 安装


可以使用yarn或者npm进行安装



yarn add jest -D | npm i jest -D


### 源码开发


这里举了一个简单的例子,实际组件开发需要使用ts以及其他UI测试框架。


例如开发一个基础方法,返回2个参数的和。文件名为sum.ts



// sum.js
function sum(a, b) {
return a + b;
}
export default sum;


### 测试用例编写


首先我们根据上面的目标文件(sum.js)创建一个测试用例文件-- sum.test.js, 测试用例文件名称统一为\*.test.js(后缀根据实际场景区分为.js或者.ts或者.tsx)



// sum.test.js
import sum from ‘./sum’;

test(‘adds 1 + 2 to equal 3’, () => {
expect(sum(1, 2)).toBe(3);
});


### 开始测试


添加下面的部分到你的package.json中



{
“scripts”: {
“test”: “jest”
}
}


最后,执行yarn testornpm run test命令,Jest将会打印如下信息:



PASS ./sum.test.js
✓ adds 1 + 2 to equal 3 (5ms)


就这样,基于jest的一个基础单元测试流程走好了,Jest的单元测试核心就是在test方法的第二个参数里面,expect方法返回一个期望对象,通过匹配器(例如toBe)进行断言,期望是否和你预期的一致,和预期一致则单元测试通过,不一致则测试无法通过,需要排除问题然后继续进行单元测试。


更多的配置以及命令行参数请参考[官方文档]( )下面开始讲解一些核心API。


### 二、核心API


### 全局方法


在你的测试文件中,Jest将下面这些方法和对象放置全局环境,所以你无需再显式的去require或者import。当然,如果你更喜欢显式的import,也可以使用例如import { describe, expect, it } from '@jest/globals’的方式。


### 1、test(name, fn, timeout)


test有别名it,两个方法是一样的。


第一个参数是你想要描述的测试用例名称; 第二个参数是包含测试期望的函数,也是测试用例的核心。第三个参数(可选)是超时时间,也就是超过多久将会取消测试(默认是5秒钟)



> 
> Note: 如果fn返回的是个promise,Jest在完成测试前将会等待Promise达到resolved状态。具体情况本文下面也会讲到如何对异步代码进行测试。
> 
> 
> 


### 匹配器


Jest使用匹配器可以让你使用各种方式测试你的代码,Jest中的匹配器实际就是expect方法返回的期望对象中包含的相关方法。官方提供了非常多的匹配器,完善的学习请查看[官方文档。]( )


下面摘选了几个最常见的匹配器方法。


### 1、.toBe(value)


toBe是最简单最基础的匹配器,就是判定是否精确匹配,toBe方法使用了Object.is方法来测试精确相等。


Object.is方法的判定准则可以[参考这里]( )



test(‘two plus two is four’, () => {
expect(2 + 2).toBe(4);
});


在测试中,你有时需要区分 undefined、 null,和 false,但有时你又不需要区分。 Jest 让你明确你想要什么。


* toBeNull 只匹配 null
* toBeUndefined 只匹配 undefined
* toBeDefined 与 toBeUndefined 相反
* toBeTruthy 匹配任何 if 语句为真
* toBeFalsy 匹配任何 if 语句为假


### 2、.not


非常容易理解,一般就是反向测试



test(‘two plus two is four’, () => {
expect(2 + 2).not.toBe(4);
});


### 3、.toEqual


递归检查对象或数组的每个字段


和上面的toBe进行对比,toBe对比俩对象对比的是内存地址,toEqual比的是属性值。



test(‘object assignment’, () => {
const data1 = { one: 1, two: 2 };
const data2 = { one: 1, two: 2 };
expect(data1).toBe(data2); // 测试失败
expect(data1).toEqual(data2);// 测试通过
});


### 4、expect.assertions


expect.assertions(number) 验证一定数量的断言在某个测试用例中被调用。通常在异步代码测试中非常有用,目的就是为了确保所有的断言被真实的调用了。


比如下面这个例子,如果去掉了expect.assertions(2), 那么测试用例会通过测试,但实际的需求应该是失败的,因为我们最初的期望是catch中的断言也会被调用。而有了expect.assertions(2),Jest会判断断言实际调用的数量和我们预期是否一致,如果不一致则说明测试失败。



test(‘doAsync calls both callbacks’, () => {
expect.assertions(2);
return Promise.resolve(123).then((data: number) => {
expect(data).toBe(123);
return; // 例如手抖写了return
// 因为某些原因下面的代码没有执行
throw new Error(‘报错了’);
}).catch(err => {
expect(err).toBe(‘报错了’);
});
});


### 异步代码测试


在JavaScript中执行异步代码是很常见的。 当你有以异步方式运行的代码时,Jest 需要知道当前它测试的代码是否已执行完成,然后它可以转移到另一个测试。 Jest有若干方法处理这种情况。


### 回调


最常见的异步模式就是回调函数,例如下面的setTimeout方法,下面的测试用例无法通过,原因是Jest无法知道callback具体的调用时间,所以会造成测试已经结束,但是setTimeout的callback还没有执行。



test(‘the data is peanut butter’, () => {
function callback(data: string) {
expect(data).toBe(‘peanut butter’);
}
setTimeout(() => {
callback(‘peanut butter’);
}, 2000);
});


想要解决上面的问题,非常简单,很容易就会联想到消息通知机制,也就是在callback调用的时候通知Jest,表示当前测试用例通过,可以跑下一个测试。


test方法的第二个参数fn,可以添加一个done参数,done是一个方法,调用了done,就是通知Jest测试完成,当然如果你的测试用例中的done方法始终没有执行,那么你的测试也会失败(超时),所以最好的方式就是加上try catch。



test(‘the data is peanut butter’, done => {
function callback(data: string) {
try {
expect(data).toBe(‘peanut butter’);
done();
} catch (err) {
done(err);
}
}
setTimeout(() => {
callback(‘peanut butter’);
}, 2000);
});


### Promise


如果你的代码使用了Promise, Jest提供了一种更加直接的方式去处理异步测试。在test第二个参数fn中直接返回一个promise,Jest就会等待这个promise达到resolved状态,如果达到了fulfilled状态,测试将会自动失败。


例如这个案例,此测试用例能够正常的通过



test(‘promise resolved’, () => {
return new Promise(resolve => {
setTimeout(() => {
resolve(‘resolved’);
}, 2000);
}).then((data: string) => {
expect(data).toBe(‘resolved’);
});
});


如果promise fulfilled如下,则测试用例会跑失败



test(‘promise fulfilled’, () => {
return Promise.reject(‘fulfilled’).then((data: string) => {
expect(data).toBe(‘fulfilled’);
})
});


当然我们也可以使用catch方法,例如下面这个例子,测试用例就能够正常的通过。



test(‘promise fulfilled’, () => {
expect.assertions(1);
return Promise.reject(‘fulfilled’).catch(err => {
expect(err).toMatch(‘fulfilled’);
});
});


promise代码可以配合匹配器.resolves和rejects一起使用,使用案例如下:



test(‘promise resolved’, () => {
return expect(Promise.resolve(‘resolved’)).resolves.toBe(‘resolved’);
});
test(‘promise fulfilled’, () => {
return expect(Promise.reject(‘fulfilled’)).rejects.toMatch(‘fulfilled’);
});


### Async/Await


如果你的代码使用了Promise, Jest提供了一种更加直接的方式去处理异步测试。在test第二个参数fn中直接返回一个promise,Jest就会等待这个promise达到resolved状态,如果达到了fulfilled状态,测试将会自动失败。



const TEN = 10;
const BASE = 5;
function fetchData () {
return new Promise((resolve, reject) => {
const random = Math.random() * TEN;
random > BASE ? resolve(random) : reject(random);
});
}

test(‘the random promise’, async () => {
expect.assertions(1);
try {
const random = await fetchData();
expect(random).toBeGreaterThan(BASE);
} catch (e) {
expect(e).toBeLessThanOrEqual(BASE);
}
});


### Mock Functions


Mock 函数简单的说就是模拟一个函数,这个功能很强大,例如nodejs中没有DOM/BOM,及时是jsdom也会缺少一些api,那么我们可以使用mock函数来进行一些测试,具体暂不详细说明。


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


[Mock Functions Doc]( )


### 使用 mock 函数


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



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


为了测试此函数,我们可以使用一个 mock 函数,然后检查 mock 函数的状态来确保回调函数如期调用。


![img](https://img-blog.csdnimg.cn/img_convert/c24c0f0f9fcf796d26727c86bfe57501.png)
![img](https://img-blog.csdnimg.cn/img_convert/18a04ed264af13e7ca6df66fee405b53.png)
![img](https://img-blog.csdnimg.cn/img_convert/1e6ebdaf8dfdada91c3d19e5b2dd56a3.png)

**既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,涵盖了95%以上软件测试知识点,真正体系化!**

细说明。


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


[Mock Functions Doc]( )


### 使用 mock 函数


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



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


为了测试此函数,我们可以使用一个 mock 函数,然后检查 mock 函数的状态来确保回调函数如期调用。


[外链图片转存中...(img-FHIjQOt1-1719255335253)]
[外链图片转存中...(img-dQUL502o-1719255335253)]
[外链图片转存中...(img-3FpFkMUP-1719255335254)]

**既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,涵盖了95%以上软件测试知识点,真正体系化!**

  • 39
    点赞
  • 39
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值