jest 自動(dòng)測(cè)試
安裝
npm install jest@24.8.0 -D
jest使用需要模塊化機(jī)制。
配置npm script命令,package.json文件配置:
"scripts": {
"test": "jest --watch"
},
單元測(cè)試:?jiǎn)蝹€(gè)模塊進(jìn)行測(cè)試
集成測(cè)試:多個(gè)模塊進(jìn)行測(cè)試
jest配置: npx jest --init
jest配置文件:jest.config.js 參數(shù):coverageDirectory:生成的覆蓋率文件夾存檔地址
代碼測(cè)試覆蓋率:使用jest測(cè)試過的代碼占所需測(cè)試代碼百分比 npx jest --coverage 生成coverage文件夾內(nèi)部包含測(cè)試文件覆蓋率文件
如何使用es6 module ?
使用babel :npm install @babel/core@7.4.5 @babel/preset-env@7.4.5 -D
.babelrc配置
{
"presets": [
[
"@babel/preset-env",
{
"targets": {
"node": "current"
}
}
]
]
}
jest默認(rèn)使用commonjs規(guī)范,使用es6module運(yùn)行過程為:
- npm run jest
- jest(babel-jest)
- 檢測(cè)是否有安裝babel模塊
- 在運(yùn)行測(cè)試之前結(jié)合babel,先把代碼進(jìn)行一次轉(zhuǎn)化
- 運(yùn)行轉(zhuǎn)化過得測(cè)試用例代碼
測(cè)試用例
匹配器
真假相關(guān)
toBe():使用Object.is()實(shí)現(xiàn)精確匹配
toEqual():遞歸檢查對(duì)象或數(shù)組的每一個(gè)字段。
toBeNull():判斷是否為null
toBeUndefined():判斷是否是undefined
toBeDefined():判斷是否是定義過的
toBeTruthy():判斷是否為真
toBeFalsy():判斷是否為假
not:取反匹配器 expect(a).not.toBeFalsy()
數(shù)字相關(guān)
toBeGreaterThan()
toBeLessThan()
toBeGreaterThanOrEqual()
toBeLessThanOrEqual();
toBeCloseTo(); 比較浮點(diǎn)數(shù)計(jì)算的時(shí)候
test("目標(biāo)數(shù)是否大于某個(gè)數(shù)字",()=>{
expect(12).toBeGreaterThan(13)
})
字符串相關(guān)
toMatch():參數(shù)可以是=字符串也可以是正則
test("判斷字符串是否包含某內(nèi)容",()=>{
var str = 'abcde'
expect(str).toMatch('a')
})
數(shù)組 Set
toContain()
test("判斷數(shù)組是否包含某項(xiàng)",()=>{
var arr = [1,2,3];
expect(arr).toContain(2)
})
異常
toThrow():參數(shù)可以為字符串也可為正則
const throwNewError = function(){
throw new Error("this is an error")
}
test("拋出異常",()=>{
expect(throwNewError).toThrow("this is an error");
})
jest命令行
w 進(jìn)入選擇模式
f 只運(yùn)行失敗的測(cè)試
a 每次將所有的測(cè)試用例都跑一次
o 只運(yùn)行修改之后的測(cè)試文件(多個(gè)文件時(shí),需要配合git使用)
"scripts": {
"test": "jest --watch" // watch表示默認(rèn)開啟o模式
},
t 根據(jù)一個(gè)測(cè)試用例名字正則表達(dá)式來過濾需要run的測(cè)試用例(可以理解為過濾模式)
p 配合matchAll使用 根據(jù)文件名過濾掉不需要測(cè)試的文件
異步測(cè)試
1. 回調(diào)類型異步函數(shù)測(cè)試
fetchData.js
export const fetchData =(fn)=> {
axios.get("http://www.dell-lee.com/react/api/demo111.json")
.then((response)=>{
fn(response.data)
});
}
fetchData.test.js
import {fetchData} from './fetchData.js';
test('測(cè)試fetchData函數(shù)返回結(jié)果為{success : true}',(done)=>{
fetchData((data)=>{
expect(data).toEqual({
success:true
})
done()
})
})
注意點(diǎn):需要在test方法第二個(gè)參數(shù)中傳入done函數(shù)作為參數(shù),并在異步方法執(zhí)行完成之后再執(zhí)行,即可正確進(jìn)行異步測(cè)試。
2. 直接返回promise對(duì)象異步測(cè)試
fetchData.js
export const fetchData =()=> {
return axios.get("http://www.dell-lee.com/react/api/demo111.json")
}
fetchData.test.js
test("測(cè)試fetchDate返回結(jié)果是{success:true}",()=>{
return fetchData()
.then((response)=>{
expect(response.data).toEqual({
success:true
})
})
})
注意點(diǎn):當(dāng)fetchData函數(shù)返回一個(gè)promise對(duì)象的時(shí)候必須將執(zhí)行結(jié)果return出去。
案例:就需要判斷必須是404
test("測(cè)試fetchData返回結(jié)果為404",()=>{
return fetchData()
.catch((e)=>{
expect(e.toString().indexOf('404')>-1).toBe(true)
})
})
注意:當(dāng)fetchData函數(shù)請(qǐng)求結(jié)果有正常數(shù)據(jù),那么就不會(huì)走catch方法,也就不會(huì)走expect(e.toString().indexOf('404')>-1).toBe(true)測(cè)試,那么測(cè)試結(jié)果默認(rèn)為通過。因此需要補(bǔ)一句expect.assertions(1),表示,在下面必須再執(zhí)行一條expect方法,否則就當(dāng)該測(cè)試用例不通過。代碼如下:
test("測(cè)試fetchData返回結(jié)果為404",()=>{
expect.assertions(1)
return fetchData()
.catch((e)=>{
expect(e.toString().indexOf('404')>-1).toBe(true)
})
})
- 使用resolves + toMatchObject()匹配器測(cè)試
export const fetchData =()=> {
return axios.get("http://www.dell-lee.com/react/api/demo.json")
}
test("測(cè)試返回結(jié)果中是否包含data:{success:true}這個(gè)對(duì)象",()=>{
return expect(fetchData()).resolves.toMatchObject({
data:{
success:true
}
})
})
注意:必須顯式的return出結(jié)果,主要是判斷fetchData返回結(jié)果中是否包含data:{success:true}這個(gè)對(duì)象。
- 使用reject + toThrow()匹配器檢測(cè)拋出異常
export const fetchData =()=> {
return axios.get("http://www.dell-lee.com/react/api/demo1.json")
}
test("使用toThrow()檢測(cè)拋出異常",()=>{
return expect(fetchData()).rejects.toThrow()
})
- 使用async/await測(cè)試異步
export const fetchData =()=> {
return axios.get("http://www.dell-lee.com/react/api/demo1.json")
}
test("測(cè)試fetchData返回結(jié)果為404", async ()=>{
await expect(fetchData()).resolves.toMatchObject({
data:{
success:true
}
})
})
test("測(cè)試fetchData返回結(jié)果為404", async ()=>{
await expect(fetchData()).rejects.toThrow()
})
注意:使用async/await可以代替使用return顯示返回promise對(duì)象,但是需要注意的是async需要和await配合使用。
- 使用await先獲取代碼執(zhí)行結(jié)果再做后續(xù)判斷
export const fetchData =()=> {
return axios.get("http://www.dell-lee.com/react/api/demo.json")
}
test("測(cè)試fetchData返回結(jié)果包含data:{success:true}", async ()=>{
const data = await fetchData();
expect(data).toMatchObject({
data:{
success:true
}
})
})
test("測(cè)試fetchData返回結(jié)果為404", async ()=>{
expect.assertions(1);
try{
await fetchData();
}catch(e){
expect(e.toString()).toMatch("404")
}
})
注意:在測(cè)試請(qǐng)求不通過的情況下需要使用try catch捕獲錯(cuò)誤,并且搭配expect.assertions()使用防止請(qǐng)求成功時(shí)不走catch邏輯,從而也顯示測(cè)試通過。
Jest鉤子函數(shù)
在jest執(zhí)行過程中自動(dòng)執(zhí)行的函數(shù)
beforeAll():表示在所有測(cè)試用例執(zhí)行之前執(zhí)行一次。
afterAll():表示在所有測(cè)試用例執(zhí)行完成之后執(zhí)行一次。
beforeEach():表示在每個(gè)測(cè)試用例執(zhí)行之前都會(huì)執(zhí)行一次。
afterEach():表示在每個(gè)測(cè)試用例執(zhí)行完成之后都會(huì)執(zhí)行一次。
描述方法,可以歸類所有的測(cè)試用例
describe('描述', () => {
//測(cè)試用例
})
描述方法可以嵌套使用
describe("測(cè)試用例", () => {
describe("測(cè)試加法",() => {
//所有關(guān)于加法的測(cè)試用例
})
describe("測(cè)試減法",() => {
//所有關(guān)于減法的測(cè)試用例
})
})
demo:
//Counter.js
class Counter {
constructor(){
this.count = 1;
}
addOne(){
this.count += 1;
}
minusOne(){
this.count -= 1
}
addTwo(){
this.count += 2;
}
minusTwo(){
this.count -= 2
}
}
export default Counter;
Counter.test.js
import Counter from './Counter';
let counter = null;
beforeAll(() => {
console.log("beforeAll")
})
beforeEach(() => {
counter = new Counter();
console.log("beforeEach")
})
afterEach(() => {
console.log("afterEach")
})
afterAll(() => {
console.log("afterAll")
})
describe("counter 測(cè)試代碼", () => {
describe("加法測(cè)試", () => {
test('測(cè)試counter +1 ', () => {
counter.addOne();
expect(counter.count).toBe(2)
})
test("測(cè)試counter +2", () => {
counter.addTwo();
expect(counter.count).toBe(3)
})
})
describe("減法測(cè)試", () => {
test("測(cè)試counter -1 ", () => {
counter.minusOne();
expect(counter.count).toBe(0)
})
test("測(cè)試counter -2", () => {
counter.minusTwo();
expect(counter.count).toBe(-1);
})
})
})
鉤子函數(shù)的作用域
每個(gè)describe方法都會(huì)對(duì)jest的鉤子函數(shù)產(chǎn)生一個(gè)作用域。比如:
describe("counter 測(cè)試", () => {
beforeAll(() => {
console.log("beforeAll")
})
beforeEach(() => {
counter = new Counter();
console.log("beforeEach")
})
afterEach(() => {
console.log("afterEach")
})
afterAll(() => {
console.log("afterAll")
})
describe("加法測(cè)試", () => {
beforeAll(() => {
console.log("beforeAll 加法測(cè)試")
})
beforeEach(() => {
counter = new Counter();
console.log("beforeEach 加法測(cè)試")
})
test('測(cè)試counter +1 ', () => {
counter.addOne();
expect(counter.count).toBe(2)
})
test("測(cè)試counter +2", () => {
counter.addTwo();
expect(counter.count).toBe(3)
})
})
describe("減法測(cè)試", () => {
test("測(cè)試counter -1 ", () => {
counter.minusOne();
expect(counter.count).toBe(0)
})
test("測(cè)試counter -2", () => {
counter.minusTwo();
expect(counter.count).toBe(-1);
})
})
})
描述為“加法測(cè)試”的describe中的鉤子函數(shù)不會(huì)在“減法測(cè)試”中的describe去執(zhí)行。
執(zhí)行順序?yàn)橄葓?zhí)行外層的鉤子函數(shù),再執(zhí)行內(nèi)層的鉤子函數(shù),先執(zhí)行beforeAll()鉤子函數(shù),再執(zhí)行beforeEach()鉤子函數(shù)。
注意:測(cè)試用例初始化準(zhǔn)備的代碼一般都寫在鉤子函數(shù)中,不能直接寫在describe中,因?yàn)閐escribe中的代碼會(huì)優(yōu)先于jest鉤子函數(shù)執(zhí)行。比如:
describe("outer", ()=>{
console.log("outer");
beforeAll(()=>{
console.log("outer beforeAll")
})
describe("inner",()=>{
console.log("inner")
beforeAll(()=>{
console.log("inner beforeAll")
})
// ...test() 測(cè)試用例
})
})
打印輸出順序?yàn)椋簅uter -> inner -> outer BeforeAll -> inner beforeAll
單個(gè)測(cè)試only
如果只想對(duì)其中一個(gè)測(cè)試用例進(jìn)行測(cè)試,而跳過其他測(cè)試用例可以使用only修飾符
describe("加法測(cè)試", () => {
beforeAll(() => {
console.log("beforeAll 加法測(cè)試")
})
beforeEach(() => {
counter = new Counter();
console.log("beforeEach 加法測(cè)試")
})
test.only('測(cè)試counter +1 ', () => {
counter.addOne();
expect(counter.count).toBe(2)
})
test("測(cè)試counter +2", () => {
counter.addTwo();
expect(counter.count).toBe(3)
})
})
此時(shí)只會(huì)執(zhí)行“測(cè)試counter +1”這個(gè)測(cè)試用例,而跳過其他測(cè)試用例。
Jest中的Mock
- 使用
jest.fn生成一個(gè)mock函數(shù),可以用來測(cè)試一個(gè)函數(shù)是否執(zhí)行(通過測(cè)試回調(diào)是否執(zhí)行來測(cè)試)。
// demo.js
export function Demo(cb){
cb();
}
// demo.test.js
import { Demo } from './demo.js';
test("demo 中的回調(diào)是否執(zhí)行" , () => {
let fn = jest.fn()
Demo(fn)
expect(fn).toBeCalled()
})
使用jest.fn()生成一個(gè)mock函數(shù),然后執(zhí)行Demo(fn),之后再使用toBeCalled()匹配器判斷Demo函數(shù)中的回調(diào)函數(shù)是否執(zhí)行,如果執(zhí)行說明Demo函數(shù)正常執(zhí)行,否則Demo函數(shù)中邏輯有錯(cuò)誤。
mock函數(shù)能幫我們干什么???
- 捕獲函數(shù)的調(diào)用和返回結(jié)果以及this和調(diào)用順序。
- 可以讓我們自由的設(shè)置返回結(jié)果
- 改變函數(shù)的內(nèi)部實(shí)現(xiàn)(比如只模擬axios發(fā)送請(qǐng)求,而不去測(cè)試返回結(jié)果)
test("demo 中的回調(diào)是否執(zhí)行" , () => {
let fn = jest.fn()
Demo(fn)
expect(fn).toBeCalled()
console.log(fn.mock);//mock的函數(shù)會(huì)有一個(gè)mock屬性,屬性里面包括了fn被調(diào)用的情況
})
打印出結(jié)果為:
{
calls: [ [] ],
instances: [ undefined ],
invocationCallOrder: [ 1 ],
results: [ { type: 'return', value: undefined } ]
}
參數(shù)解讀:
calls:數(shù)組,length,表示該fn被調(diào)用了幾次,里面的每一項(xiàng)表示調(diào)用函數(shù)時(shí),傳入的參數(shù),比如calls:[["123"],["123"]]表示fn被調(diào)用2次,每次調(diào)用的時(shí)候傳遞的參數(shù)都是“123”,舉例:
test("demo 中的回調(diào)是否執(zhí)行" , () => {
let fn = jest.fn()
Demo(fn);
Demo(fn);
expect(fn.mock.calls.length).toBe(2)
})
可以使用fn.mock.calls.length判斷是否執(zhí)行。
還可以判斷調(diào)用參數(shù)是否是“123”:
test("demo 中的回調(diào)是否執(zhí)行" , () => {
let fn = jest.fn()
Demo(fn);
Demo(fn);
expect(fn.mock.calls[0]).toEqual(['123'])
})
invocationCallOrder:數(shù)組,表示被調(diào)用的順序
test("demo 中的回調(diào)是否執(zhí)行" , () => {
let fn = jest.fn()
Demo(fn);
Demo(fn);
Demo(fn);
console.log(fn.mock.invocationCallOrder)
})
輸出[ 1, 2, 3 ]表示當(dāng)執(zhí)行3次Demo(fn)那么他會(huì)按照順序執(zhí)行。
results:數(shù)組,表示函數(shù)執(zhí)行之后每次的返回值是什么
test("demo 中的回調(diào)是否執(zhí)行" , () => {
let fn = jest.fn(()=>{
return "456"
})
Demo(fn);
Demo(fn);
Demo(fn);
console.log(fn.mock.results);
})
在jest.fn()中使用函數(shù)返回一個(gè)字符串“456”然后輸出結(jié)果為
[
{ type: 'return', value: '456' },
{ type: 'return', value: '456' },
{ type: 'return', value: '456' }
]
在給fn添加返回值時(shí)還有其他方法
使用fn.mockReturnValueOnce(value)api,表示模擬一個(gè)返回值,但只模擬一次。
test("demo 中的回調(diào)是否執(zhí)行" , () => {
let fn = jest.fn();
fn.mockReturnValueOnce("123")
Demo(fn);
Demo(fn);
Demo(fn);
console.log(fn.mock.results);
})
輸出結(jié)果為:
[
{ type: 'return', value: '123' },
{ type: 'return', value: undefined },
{ type: 'return', value: undefined }
]
可以使用這個(gè)方法模擬多次不同返回值:
test("demo 中的回調(diào)是否執(zhí)行" , () => {
let fn = jest.fn();
fn.mockReturnValueOnce("123");
fn.mockReturnValueOnce("456");
fn.mockReturnValueOnce("789");
Demo(fn);
Demo(fn);
Demo(fn);
console.log(fn.mock.results);
})
輸出結(jié)果為:
[
{ type: 'return', value: '123' },
{ type: 'return', value: '456' },
{ type: 'return', value: '789' }
]
也可以鏈?zhǔn)秸{(diào)用:
test("demo 中的回調(diào)是否執(zhí)行" , () => {
let fn = jest.fn();
fn.mockReturnValueOnce("123").mockReturnValueOnce("456").mockReturnValueOnce("789");
Demo(fn);
Demo(fn);
Demo(fn);
console.log(fn.mock.results);
})
如果每次只需要返回同一個(gè)值,可以使用剛才在jest.fn()中去封裝一個(gè)方法,還可以使用fn.mockReturnValue(value)
test("demo 中的回調(diào)是否執(zhí)行" , () => {
let fn = jest.fn();
fn.mockReturnValue("hello")
Demo(fn);
Demo(fn);
Demo(fn);
console.log(fn.mock.results);
})
輸出結(jié)果為:
[
{ type: 'return', value: 'hello' },
{ type: 'return', value: 'hello' },
{ type: 'return', value: 'hello' }
]
結(jié)合fn.mock的results屬性可以寫其他的測(cè)試用例了
比如:
test("demo 中的回調(diào)是否執(zhí)行" , () => {
let fn = jest.fn();
fn.mockReturnValue("hello")
Demo(fn);
Demo(fn);
Demo(fn);
expect(fn.mock.results[0].value).toBe("hello");
})
測(cè)試結(jié)果為通過。
instances:數(shù)組,每項(xiàng)表示每次fn執(zhí)行的時(shí)候fn中this指向。
//demo.js
export function createObj(classItem){
new classItem();
}
表示createObj方法接收一個(gè)類,在createObj方法中對(duì)類進(jìn)行實(shí)例化。
//demo.test.js
test.only('測(cè)試createObj方法',() => {
let fn = jest.fn();
createObj(fn);
console.log(fn.mock)
})
輸出結(jié)果為:
{
calls: [ [] ],
instances: [ mockConstructor {} ],
invocationCallOrder: [ 1 ],
results: [ { type: 'return', value: undefined } ]
}
也就是表示fn方法中的this,指向的是jest.fn()的構(gòu)造函數(shù)mockConstructor;
小總結(jié):通過jest.fn()模擬出來的函數(shù)它有一個(gè)mock屬性,mock屬性中的calls表示該函數(shù)被調(diào)用的幾次,以及每次傳入的參數(shù),instances表示該函數(shù)被調(diào)用的幾次,以及每次調(diào)用該函數(shù)中this指向,invocationCallOrder表示該函數(shù)被調(diào)用的幾次,以及調(diào)用順序,resultes表示該函數(shù)被調(diào)用的幾次,以及每次調(diào)用的返回值。
改變函數(shù)的內(nèi)部實(shí)現(xiàn)指的是,比如前端在測(cè)試后臺(tái)接口返回?cái)?shù)據(jù)問題時(shí),不必測(cè)試接口返回了什么東西,只需要測(cè)試,前端是否發(fā)送ajax請(qǐng)求即可,返回值可以前端自己模擬一下。核心apimockResolvedValue;
//demo.js
export function getData(){
return axios.get('http://www.dell-lee.com/react/api/demo.json').then((response)=>{
return response.data;
})
}
//demo.test.js
import { getData } from "./demo.js";
import axios from "axios";
jest.mock("axios");
test.only("測(cè)試 getData",async () => {
axios.get.mockResolvedValue({data:"hello"}) //模擬axios get返回值
await getData().then((data)=>{
expect(data).toBe("hello") //確認(rèn)發(fā)起請(qǐng)求
})
})
此段代碼表示,使用axios.get.mockResolvedValue({data:"hello"})方法模擬了axios的get方法返回值,當(dāng)調(diào)用getData()方法時(shí),請(qǐng)求回來的結(jié)果不是從服務(wù)器異步獲取到的,而是我們同步模擬的,因此可以節(jié)省時(shí)間,節(jié)省資源。
axios.get.mockResolvedValue()api也可以換成axios.get.mockResolvedValueOnce(),這個(gè)表示只模擬一次,當(dāng)發(fā)起多個(gè)請(qǐng)求的時(shí)候就會(huì)報(bào)測(cè)試不通過。
比如:
test.only("測(cè)試 getData",async () => {
axios.get.mockResolvedValue({data:"hello"})
await getData().then((data)=>{
expect(data).toBe("hello")
})
await getData().then((data)=>{
expect(data).toBe("hello")
})
})
上面這段代碼會(huì)通過測(cè)試。
下面這段代碼不會(huì)通過測(cè)試。
test.only("測(cè)試 getData",async () => {
axios.get.mockResolvedValueOnce({data:"hello"})
await getData().then((data)=>{
expect(data).toBe("hello")
})
await getData().then((data)=>{
expect(data).toBe("hello")
})
})
總結(jié)mock函數(shù)的作用:
- 捕獲函數(shù)的調(diào)用和返回結(jié)果,以及this和調(diào)用順序。
- 它可以讓我們自由地設(shè)置返回結(jié)果。
- 可以改變函數(shù)的內(nèi)部實(shí)現(xiàn)。
補(bǔ)充mock函數(shù)的一些語(yǔ)法。。。
改變函數(shù)返回值
第一種方法:直接在jest.fn()中去實(shí)現(xiàn)
test("demo 中的回調(diào)是否執(zhí)行" , () => {
let fn = jest.fn(()=>{
return "hello"
});
Demo(fn);
console.log(fn.mock.results)
})
第二種方法:使用fn.mockReturnValue()
test("demo 中的回調(diào)是否執(zhí)行" , () => {
let fn = jest.fn();
fn.mockReturnValue("hello");
Demo(fn);
console.log(fn.mock.results)
})
第三種方法:使用fn.mockImplementation()
test("demo 中的回調(diào)是否執(zhí)行" , () => {
let fn = jest.fn();
fn.mockImplementation(()=>{
return "world"
});
Demo(fn);
console.log(fn.mock.results)
})
還有fn.mockImplementationOnce()表示只模擬一次。
mockImplementationOnce()比較mockReturnValue()前者可以在里面的函數(shù)中去做一些其他邏輯操作,而后者只是一個(gè)返回結(jié)果。
返回this方法
fn.mockImplementationOnce(()=>{
return this;
})
或者
fn.mockReturnThis()
toBeCalledWith()匹配器
expect(fn.mock.calls[0].toEqual(["abc"]))
等價(jià)于
expect(fn).toBeCalledWith("abc")
區(qū)別是前者表示第一次調(diào)用fn的參數(shù)是abc,后者表示每次調(diào)用fn參數(shù)都是abc
vsCode插件jest,可以不用手動(dòng)去執(zhí)行npm run test。這個(gè)插件會(huì)自動(dòng)去檢測(cè)測(cè)試用例是否執(zhí)行成功,并且會(huì)給與提示。
Snapshot(快照測(cè)試)
基礎(chǔ)使用
在測(cè)試配置文件的時(shí)候,當(dāng)頻繁修改配置文件時(shí),需要同步更新測(cè)試文件,為了避免這種情況,可以使用快照匹配器。
//snap.js
export const generateConfig = function(){
return {
name:"lisa",
age:19,
sex:"male",
couple:true
}
}
//snap.test.js
import { generateConfig } from './snap.js';
test("測(cè)試配置文件",()=>{
expect(generateConfig()).toMatchSnapshot();
})
快照測(cè)試過程:
第一次執(zhí)行測(cè)試命令時(shí)生成一個(gè)和配置文件一樣的快照文件(snapshots),對(duì)比快照文件和配置文件,相同則表示測(cè)試通過。
當(dāng)修改配置文件之后,再執(zhí)行測(cè)試命令,會(huì)拿新的配置文件快照去和之前的快照做對(duì)比,如果相同則通過測(cè)試,否則不會(huì)通過。
不會(huì)通過的時(shí)候會(huì)提示具體的原因,可以使用jest命令行w查看所有的指令,之后使用u,再用新的配置文件快照去更新快照,這樣再去進(jìn)行測(cè)試。
當(dāng)有多個(gè)配置文件需要進(jìn)行快照測(cè)試時(shí),使用u會(huì)更新所有的配置文件方法,因此可以使用i,每次只提示一個(gè)配置文件方法,然后使用u去更新當(dāng)前快照,之后在重復(fù)循環(huán)執(zhí)行i->u即可實(shí)現(xiàn)每次只修改一個(gè)文件機(jī)制。
當(dāng)一個(gè)配置文件中有日期(new Date())時(shí),內(nèi)次更新快照和之前的date都不一樣,所以,這種情況下如下處理:
export const generateConfig = function(){
return {
name:"lisa",
age:40,
sex:"female",
couple:true,
date:new Date()
}
}
test("測(cè)試generateConfig配置文件",()=>{
expect(generateConfig()).toMatchSnapshot({
date:expect.any(Date)
});
})
在toMatchSnapshot()匹配器中傳入一個(gè)對(duì)象參數(shù),表示,date字段只要是Date類型即可,不需要完全相等。
行內(nèi)snapshot
需要安裝prettier模塊
npm install prettier@1.18.2 --save
import { generateConfig } from "./snap.js";
test("測(cè)試generateConfig配置文件", () => {
expect(generateConfig()).toMatchInlineSnapshot(
{
date: expect.any(Date)
}
);
});
使用toMatchInlineSnapshot()匹配器,執(zhí)行測(cè)試命令之后,會(huì)將生成的快照自動(dòng)在toMatchInlineSnapshot()中的第二個(gè)參數(shù)展示。
test("測(cè)試generateConfig配置文件", () => {
expect(generateConfig()).toMatchInlineSnapshot(
{
date: expect.any(Date)
},
`
Object {
"age": 40,
"couple": true,
"date": Any<Date>,
"name": "lisa",
"sex": "female",
}
`
);
});
行內(nèi)快照(toMatchInlineSnapshot)與普通快照(toMatchSnapshot)的區(qū)別就是行內(nèi)快照生成快照文件存放在測(cè)試文件里,而普通快照是將快照文件生成一個(gè)新的文件夾存放。
命令行s是跳過當(dāng)前錯(cuò)誤提示,直接到下一個(gè)提示。
mock深入學(xué)習(xí)
在之前學(xué)習(xí)過異步測(cè)試可以通過模擬返回值進(jìn)行測(cè)試
// deepmock.js
import axios from 'axios';
export const getData = () => {
return axios.get('http://www.dell-lee.com/react/api/demo.json')
}
// deepmock.test.js
import axios from 'axios';
jest.mock('axios');
test('getData',() => {
axios.get.mockResolvedValue({data:"123"})
return getData().then((data)=>{
expect(data.data).toEqual('123')
})
})
現(xiàn)在可以模擬一個(gè)專門用來模擬異步請(qǐng)求方法的文件:
在根目錄下創(chuàng)建一個(gè)文件夾__mocks__,里面寫一個(gè)和需要測(cè)試的文件完全一樣的名稱比如 deepmock.js里面可以模擬異步請(qǐng)求
// __mocks__/deepmock.js
export const getData = () => {
return new Promise((resolved,rejected)=>{
resolved("123")
})
}
在deepmock.test.js中這么測(cè)試
jest.mock('./deepmock.js');
import { getData } from './deepmock.js';
test('getData',() => {
return getData().then((data)=>{
expect(data).toEqual('123')
})
})
第一步模擬文件,第二步導(dǎo)入文件,此時(shí)導(dǎo)入的deepmock.js文件是__mocks__/deepmock.js文件,然后進(jìn)行測(cè)試。
如果將jest.config.js中automock屬性設(shè)置為true,那么相當(dāng)于默認(rèn)進(jìn)行mock,可以不用寫jest.mock('./deepmock.js').
如果deepmock.js中有同步代碼,不需要放在__mocks__文件夾中的deepmock.js中時(shí),可以使用const { getNum } = jest.requireActual('./deepmock.js')來實(shí)現(xiàn)引用源文件中的getNum,而不使用模擬文件中的getNum.
比如:
//deepmock.js
import axios from 'axios';
export const getData = () => {
return axios.get('http://www.dell-lee.com/react/api/demo.json')
}
export const getNum = () => {
return 456
}
//__mocks__/deepmock.js
export const getData = () => {
return new Promise((resolved,rejected)=>{
resolved("123")
})
}
//deepmock.test.js
jest.mock('./deepmock.js');
import { getData } from './deepmock.js';
const { getNum } = jest.requireActual('./deepmock.js');
test('getData',() => {
return getData().then((data)=>{
expect(data).toEqual('123')
})
})
test('getNum',() => {
expect(getNum()).toEqual(456)
})j
test文件中異步方法getData引用的是__mocks__/deepmock.js中的方法,而同步方法getNum引用的是根目錄下deepmock.js中的方法。
小總結(jié):
jest.mock("文件路徑"):模擬該文件中的方法。對(duì)應(yīng)在__mocks__/文件路徑下。
jest.unmock("文件路徑"):可以取消模擬文件。
requireActual:表示可以引用真實(shí)文件中的方法,而非模擬文件中的。
jest中的timer測(cè)試
根據(jù)之前學(xué)習(xí)可以測(cè)試延時(shí)代碼如下:
//timer.js
export const timer = function (fn){
setTimeout(()=>{
fn()
},3000)
}
//timer.test.js
import {timer} from './timer.js';
test("timer", (done) => {
timer(()=>{
expect(1).toBe(1);
done();
})
})
這樣表示等3s之后會(huì)執(zhí)行一部中的測(cè)試語(yǔ)句,問題來了,如果延時(shí)為很長(zhǎng)時(shí)間的話,使用等待時(shí)間這種機(jī)制不是很好,那么就需要另外一種方法:使用假的定時(shí)器。
方法:jest.useFakeTimers()
//timer.test.js
jest.useFakeTimers();
test("test timers",()=>{
const fn = jest.fn();
timer(fn);
jest.runAllTimers();
expect(fn).toHaveBeenCalledTimes(1)
})
使用jest.useFakeTimers(),表示開始使用假的定時(shí)器,之后配合jest.runAllTimers()表示立即運(yùn)行玩所有的定時(shí)器。再配合toHaveBeenCalledTimes(1)匹配器,表示fn方法被調(diào)用了幾次,來進(jìn)行測(cè)試。注意:fn必須是一個(gè)mock函數(shù),否則會(huì)報(bào)錯(cuò)。
假如timer.js是這樣
//timer.js
export const timer = function (fn){
setTimeout(()=>{
fn()
setTimeout(()=>{
fn()
},3000)
},3000)
}
jest.runAllTimers()表示運(yùn)行所有的timer,假如只想檢測(cè)外層的setTimeout()那么這個(gè)方法是不行的,可以使用jest.runOnlyPendingTimers()表示只會(huì)運(yùn)行處于當(dāng)前運(yùn)行處于隊(duì)列中即將運(yùn)行的timer,而不會(huì)運(yùn)行那些還沒有被創(chuàng)建的timer。代碼如下:
jest.useFakeTimers();
test("test timers",()=>{
const fn = jest.fn();
timer(fn);
jest.runOnlyPendingTimers()
expect(fn).toHaveBeenCalledTimes(1)
})
小總結(jié):
runAllTimers():表示運(yùn)行所有的timer
runOnlyPendingTimers():表示只運(yùn)行當(dāng)前隊(duì)列中的timer,而不管還未創(chuàng)建的。
更好的api:jest.advanceTimersByTime(3000)表示讓時(shí)間快進(jìn)3s。
//timer.test.js
import {timer} from './timer.js';
jest.useFakeTimers();
test("test timers",()=>{
const fn = jest.fn();
timer(fn);
jest.advanceTimersByTime(3000)
expect(fn).toHaveBeenCalledTimes(1)
})
很明顯快進(jìn)3s之后,回調(diào)函數(shù)fn只執(zhí)行了一次,因此expect(fn).toHaveBeenCalledTimes(1)測(cè)試會(huì)通過。
如果將修改一下參數(shù)jest.advanceTimersByTime(6000),那么回調(diào)fn被執(zhí)行了兩次,因此想要通過測(cè)試必須修改斷言expect(fn).toHaveBeenCalledTimes(2)
//timer.test.js
import {timer} from './timer.js';
jest.useFakeTimers();
test("test timers",()=>{
const fn = jest.fn();
timer(fn);
jest.advanceTimersByTime(6000)
expect(fn).toHaveBeenCalledTimes(2)
})
或者
test("test timers",()=>{
const fn = jest.fn();
timer(fn);
jest.advanceTimersByTime(3000)
expect(fn).toHaveBeenCalledTimes(1)
jest.advanceTimersByTime(3000)
expect(fn).toHaveBeenCalledTimes(2)
})
表示快進(jìn)3s后fn被調(diào)用1次,再快進(jìn)3s后fn被調(diào)用2次。那就存在一個(gè)問題,每一次快進(jìn)是在前一次快進(jìn)基礎(chǔ)上進(jìn)行調(diào)用的有可能會(huì)有沖突,那么解決辦法就是在鉤子函數(shù)中進(jìn)行處理。
beforeEach(()=>{
jest.useFakeTimers();
})
test("test timers",()=>{
const fn = jest.fn();
timer(fn);
jest.advanceTimersByTime(3000)
expect(fn).toHaveBeenCalledTimes(1)
jest.advanceTimersByTime(3000)
expect(fn).toHaveBeenCalledTimes(2)
})
test("test1 timers",()=>{
const fn = jest.fn();
timer(fn);
jest.advanceTimersByTime(3000)
expect(fn).toHaveBeenCalledTimes(1)
jest.advanceTimersByTime(3000)
expect(fn).toHaveBeenCalledTimes(2)
})
每次在進(jìn)入測(cè)試之前都重新jest.useFakeTimers()即可。
ES6中類的測(cè)試
單元測(cè)試:表示只僅僅對(duì)單一一個(gè)文件或方法進(jìn)行測(cè)試,從而忽略掉該文件中引用的其他內(nèi)容,如果其他內(nèi)容比較耗費(fèi)性能,那么在進(jìn)行單元測(cè)試的時(shí)候進(jìn)行mock簡(jiǎn)化引用。
舉例:在一個(gè)方法文件中引用了一個(gè)類。然后對(duì)該方法進(jìn)行單元測(cè)試。
//util.js (ES6類)
class Util {
a(){
//...邏輯復(fù)雜,耗費(fèi)性能
}
b(){
//...邏輯復(fù)雜,耗費(fèi)性能
}
}
export default Util
Util類中有兩個(gè)方法,都很耗性能,邏輯也很復(fù)雜。
//demoUtil.js
import Util from './util.js';
export const demoFunction = () => {
let util = new Util();
util.a();
util.b();
}
在demoUtil文件中引用了這個(gè)Util類,并且使用了。
//demoUtil.test.js
jest.mock('./util.js');
/*
Util Util.a Util.b jest.fn()
*/
import { demoFunction } from "./demoUtil.js";
import Util from './util.js';
test('測(cè)試demoFunction',() => {
demoFunction();
expect(Util).toHaveBeenCalled();
// expect()
expect(Util.mock.instances[0].a).toHaveBeenCalled();
expect(Util.mock.instances[0].b).toHaveBeenCalled();
console.log(Util.mock);
})
在demoUtil.test.js文件中對(duì)demoFunction進(jìn)行測(cè)試,此時(shí)由于Util中類邏輯復(fù)雜耗費(fèi)性能,那么我們需要采取mock對(duì)其進(jìn)行模擬,只需要判斷在demoFunction方法中Util類,以及它的實(shí)例化方法調(diào)用了沒有即可。
這段代碼流程解釋一下就是:
jest.mock('./util.js'):當(dāng)jest.mock()中的參數(shù)檢測(cè)到是一個(gè)類時(shí),那么就會(huì)直接默認(rèn)將類,以及里面的方法轉(zhuǎn)換成mock函數(shù),例如:Util ,Util.a,Util.b都將會(huì)轉(zhuǎn)換成jest.fn();
引入Util,此時(shí)的Util已經(jīng)轉(zhuǎn)換成了jest.fn(),那么此時(shí)就可以采用Util.mock下的一些屬性進(jìn)行測(cè)試?yán)病?/p>
還有一種辦法就是,直接在__mocks__文件夾中自己去模擬實(shí)現(xiàn)一下jest.mock()方法的內(nèi)部實(shí)現(xiàn)。
//__mocks__/util.js
const Util = jest.fn(()=>{
console.log("Util")
});
Util.prototype.a = jest.fn(()=>{
console.log("a")
});
Util.prototype.b = jest.fn(()=>{
console.log("b")
});
export default Util;
這樣就相當(dāng)于把jest.mock("./util"),自動(dòng)轉(zhuǎn)換過程手寫了一遍,這樣做比較優(yōu)雅,而且還可以對(duì)方法進(jìn)行拓展,寫一些邏輯。
還可以直接在demoUtil.test.js中直接寫
jest.mock('./util.js',()=>{
const Util = jest.fn(()=>{
console.log("Util")
});
Util.prototype.a = jest.fn(()=>{
console.log("a")
});
Util.prototype.b = jest.fn(()=>{
console.log("b")
});
return Util;
});
import { demoFunction } from "./demoUtil.js";
import Util from './util.js';
test('測(cè)試demoFunction',() => {
demoFunction();
expect(Util).toHaveBeenCalled();
// expect()
expect(Util.mock.instances[0].a).toHaveBeenCalled();
expect(Util.mock.instances[0].b).toHaveBeenCalled();
console.log(Util.mock);
})
在jest.mock()第二個(gè)參數(shù)中可以放剛才自動(dòng)轉(zhuǎn)換的邏輯。
單元測(cè)試就是指只對(duì)我自身做一些測(cè)試,集成測(cè)試是指對(duì)我自身以及自身其他依賴一起做測(cè)試。
jest中對(duì)dom節(jié)點(diǎn)測(cè)試
node環(huán)境中是沒有dom的,jest在node環(huán)境下自己模擬了一套dom的api,jsDom。
需要對(duì)dom操作為了方便,安裝jquery依賴。
看下面例子:
//dom.js
import $ from "jquery";
export const addDivToBody = () => {
$('body').append("<div/>")
}
//dom.test.js
import {addDivToBody} from './dom';
import $ from 'jquery';
test("addDivToBody",() => {
addDivToBody();
addDivToBody();
expect($('body').find("div").length).toBe(2)
})
練習(xí)源碼:https://github.com/Mstian/jest-test