通过前面的介绍,我们基本上对jest有了一个初步的了解,并搭建了环境开始上手测试,接下来就开始说函数。本节内容主要如下:
- mock函数
- 什么是mock函数
- 模拟函数
- 模拟返回值
- 模拟部分函数
- 模拟函数重新实现
- 模拟第三方
- 模拟moment
- 模拟react-query
- 模拟axios
- setup&teardown
- 两类设置
- 作用域
测试mock函数
模拟函数允许您通过擦除函数的实际实现来测试代码之间的链接,捕获对函数的调用(以及在这些调用中传递的参数),在实例化时捕获构造函数的实例,并允许在测试时new配置返回值。模拟函数有两种方法:
创建模拟函数以在测试代码中使用
编写manual mock覆盖模块依赖项的方法
在这一节当中,我会尽可能的将所有的mock方法都讲解到,并分享我工作当中遇到的解决方案
记住:mock是函数,或者说替别的函数做事,如模拟返回值
模拟函数
模拟函数即使用jest.fn()充当函数传给我们需要测试的函数,返回一个新的、未使用的模拟函数
可选地采用模拟实现。
因此我们新建文件:utils/mock/index.ts
type Fn = (val: number) => number
export const handleChange = (val: number , callback: Fn) => {
callback(val);
}
新建测试文件_test/testMock.test.tsx
import * as fnMock from 'utils/mock/index';
describe('test mock',() => {
test('test mock function',() => {
const mockFn = jest.fn(val => val*val);
mockFn.mockName('mock_fn') // 设置jest.fn()别名
const count = 2;
fnMock.handleChange(count,mockFn);
console.log(mockFn.mock.calls,'calls'); // [ [ 2 ] ]
console.log(mockFn.mock.calls.length,'length'); // 1
console.log(mockFn.mock.results,'results'); // [ { type: 'return', value: 4 } ]
fnMock.handleChange(count,mockFn);
console.log(mockFn.mock.calls,'calls2'); // [ [ 2 ], [ 2 ] ]
console.log(mockFn.mock.calls.length,'length2'); // 2
console.log(mockFn.mock.results,'results2'); // [ { type: 'return', value: 4 }, { type: 'return', value: 4 } ]
console.log(mockFn.getMockName(),'mockName'); // mock_fn
expect(mockFn.mock.calls.length).toBe(2);
})
})
测试讲解:
- mockFn.mock.calls 返回函数调用参数的情况,是一个二维数组,没调用一次数组增加一组:[ [ 2 ] ,[ 2 ] ]
- mockFn.mock.calls.length 返回调用函数的次数
- mockFn.mock.results 返回mock函数的执行结果,上面返回:[ { type: 'return', value: 4 }, { type: 'return', value: 4 } ]
- jest.fn(function) 相当于jest.fn.mockImplementation(function) 用于模拟内部函数实现,对于接口函数的模拟很有效果
- mockFn.mockName('') 设置jest.fn()别名
- mockFn.getMockName() 获取mock的名称,如果没有设置,则返回默认的: jest.fn()
模拟返回值
现在utils/mock/index.ts这个文件里面添加一个方法
...
export const getCallbackValue = (val: number, callback: Fn) => {
// 这里应该有一段逻辑代码
const res = callback(val);
return res;
}
然后在testMock.test.tsx中添加新的测试
...
test('test mock return value',() => {
const count = 2;
let res = 0;
const mockFn = jest.fn();
mockFn.mockReturnValueOnce(1).mockReturnValueOnce(2).mockReturnValue(3);
// eslint-disable-next-line
res += fnMock.getCallbackValue(count,mockFn);
console.log(res,'result1') // 1 因为第一次返回值是1
res += fnMock.getCallbackValue(count,mockFn);
console.log(res,'result2') // 3 = 1 + 2
res += fnMock.getCallbackValue(count,mockFn);
console.log(res,'result3') // 6 = 3 + 3
})
- mockReturnValueOnce() 返回一次mock函数值,返回之后便舍弃
- mockReturnValue 返回mock函数固定值
这种连续编写函数的风格可以避免需要复杂的存根来重新创建它们所代表的真实组件的行为,有利于在使用它们之前将值直接注入到测试中
在实际工作当中,我们jest.fn()实际上模拟的是getCallbackValue的第二行逻辑内容,和实际上这个回调函数造成的结果。比如我们这个回调函数是需要将item扩大10被,那么我们jest.fn()返回的值应该是item的10被,即
test('test mock return value',() => {
...
const mockFn2 = jest.fn(val => 10*val);
const res2 = fnMock.getCallbackValue(count,mockFn2);
console.log(res2,'----') // 20
})
模拟部分函数
故名思义,我们只会模拟一个文件中的某部分函数!
官方给出的方法:
https://www.jestjs.cn/docs/mock-functions#mocking-modules
我们新建文件 utils/mock/someFn.ts
export const foo = 'foo';
export const bar = () => 'bar';
export const get = () => 'get';
export default () => 'baz';
在tsetMock.test.tsx增加内容
import defaultExport, {bar, foo} from 'utils/mock/someFn';
import * as Api from 'utils/mock/someFn';
jest.mock('utils/mock/someFn', () => {
const originModule = jest.requireActual('utils/mock/someFn');
return {
__esModule: true,
...originModule,
default: jest.fn( () => 'mocked baz'),
foo: 'mocked foo'
}
})
接着新增测试内容
test('test mock partials', async () => {
const defaultExportResult = defaultExport();
console.log(defaultExportResult,'defaultExportResult'); // mocked baz
expect(defaultExport).toHaveBeenCalled();
expect(bar()).toBe('bar');
const res = bar();
expect(res).toBe('bar');
expect(mockBar).toBeCalled();
expect(bar).toBeCalled();
})
如果需要多次mock一个函数,那么推荐使用jest.spyOn(),我们添加新的代码,并将上述内容进行注释
const mockBar = jest.spyOn(Api,'bar');
测试代码就变成了这样
afterEach(() => {
// restore the spy created with spyOn
jest.restoreAllMocks();
});
test('test mock partials', async () => {
const defaultExportResult = defaultExport();
console.log(defaultExportResult,'defaultExportResult'); // mocked baz
expect(defaultExport).toHaveBeenCalled();
expect(bar()).toBe('bar');
const res = bar();
expect(res).toBe('bar');
expect(mockBar).toBeCalled();
expect(bar).toBeCalled();
})
模拟函数的重新实现mockImplementation
相当于重写了函数的实现,它等同于jest.fn(function)
上面的例子,我们可以将mockBar重新实现一下
const mockBar = jest.spyOn(Api,'bar');
mockBar.mockImplementation(
() => {
return 'mock_bar'
}
)
接着测试结果就会发生改变
test('test mock partials', async () => {
...
const res = bar();
expect(res).toBe('mock_bar');
...
expect(bar()).toBe('mock_bar');
})
也许您会好奇为啥要这么做,实际上我们是为了更好的渲染页面,换句话说是为了后面我们测试组件的时候有妙用,这个到时候再谈,现在我们只需要这样做,就ok了。
模拟三方库
当项目中使用到了三方库的方法时,尤其是页面渲染,此时一般会提示'xxxx_.default.xxx'找不到,或者为undefined,此时我们就需要为这些三方库添加mock。
moment
moment是一款操作时间和获取时间格式的一款三方,避免了额外的方法来实现业务中需要的时间格式。同时还支持国际化。
如果我们想模拟整个moment本身,而不要求具体的返回值,此时mock返回的时间值是invalidate date。
jest.mock('moment',() => {
const oMoment = jest.requireActual('moment');
return {
__esModule: true,
...oMoment,
// 这个时间是没效果的
default: () => oMoment('10:02')
}
})
模拟某一个函数
目标:模拟 const currentTime = moment().local().format('HH:mm'),同时在页面中展示currentTime
思路:和上面是类似的,不过我们会根据他使用风格不断往下mock
jest.mock('moment', () => {
const oMoment = jest.requireActual('moment');
const ff = jest.fn(() => ({
local: jest.fn(() => {
return {
// 这样页面中的currentTime = '12:02'
format: jest.fn().mockReturnValue('12:02')
}
}),
}))
return {
__esModule: true,
...oMoment,
default: ff
};
});
目标:模拟moment.utc()
思路:和上一个例子不同的是,utc是直接挂在moment身上,因此我们可以认为moment就是一个对象
jest.mock('moment', () => {
const oMoment = jest.requireActual('moment');
const ff = {
// 此时,页面如果执行了moment.utc('xxxx'),返回内容就是'xxxx'
utc: jest.fn((value: string) => value)
}
return {
__esModule: true,
...oMoment,
default: ff
};
});
这里有一个问题,如果页面中既有moment().local()又有moment.utc(),此时jest貌似就无法同时模拟了,如果有好的方法可以告知给我,万分感谢!
react-query
大多数核心 Web 框架并没有提供一种以整体方式获取或更新数据的固执己见的方式。因此,开发人员最终要么构建封装有关数据获取的严格意见的元框架,要么发明自己的获取数据的方法。这通常意味着将基于组件的状态和副作用拼凑在一起,或者使用更通用的状态管理库在整个应用程序中存储和提供异步数据(官网:
https://tanstack.com/query/latest/docs/react/overview)。
模拟get
思路:写一个query,然后组件引用,最后测试
query:
export const useGetUserData = (
option?: Omit<
UseQueryOptions<
UserData,
AxiosError<any>,
UserData,
QueryKey
>,
'queryKey' | 'queryFn'
>
)=> {
return useQuery<UserData,AxiosError<any>>(
homeKey.toHome.userDD('getUserData'),
() => getUserData(),
option
)
}
测试:
const mockUseGetUserData = useGetUserData as jest.MockedFunction<
typeof useGetUserData
>
mockUseGetUserData.mockImplementation(
() => {
return {
data: userData
} as unknow as any
}
)
模拟post
过程类似,直接上测试:
const mockUseGetUserData = useGetUserData as jest.MockedFunction<
typeof useGetUserData
>
mockUseGetUserData.mockImplementation(
() => {
return {
isLoading: true,
mutate: jest.fn((_,{ onSuccess }) => {
onSuccess(userData)
})
} as unknow as any
}
)
主要看useGetUserData返回了些什么内容,依样画瓢来整即可。
axios
axios网络请求用的比较多,可以直接看 模拟异步:
https://www.toutiao.com/article/7251131952856613380/?log_from=ff9647c1140ab_1690534256654
setup&teardown
上面代码中有一个afterEach,这个是什么意思呢?以下就是说这个玩意!
这个官方有解读:通常在编写测试时,您需要在测试运行前进行一些设置工作,而在测试运行后需要进行一些收尾工作。Jest提供了辅助函数来处理此问题。翻译成人话就是拿生命周期大概的意思,来理解;生命周期说程序开始之前做的一些事,或者程序结束又需要做的事。jest只有两类,一类是循环事件,一类是一次性事件。
两类设置
循环性设置:beforeEach和afterEach,如果是异步代码那就直接返回一个promise。
beforeEach(() => {
initializeCityDatabase();
// return initializeCityDatabase();
});
afterEach(() => {
clearCityDatabase();
});
特点:每执行一次测试就会自行一遍钩子,即有几轮,次数=轮数*次数
一次性设置:beforeAll 和 afterAll
beforeAll(() => {
return initializeCityDatabase();
});
afterAll(() => {
return clearCityDatabase();
});
作用域
这里的作用域主要是三个:describe外,describe与it/test之间以及it/test之内。而作用域指的是前面我们的两类事件在这里面的不同之行结果。就跟生命周期套娃类似:页面套组件,组件继续套组件等。这里有一个我总结的是:describe包裹的事件只适用于describe内;it/test包裹的只适用于it/test内。
这里有一个例子,大家可以复制下来,仔细分析一下他的规律,好好理解什么就一次性,什么叫循环性!
特点:只会执行一次,即有几个执行几次
执行顺序
无钩子情况
先执行describe内的打印,再执行test内的打印
有钩子情况
1先执行外层describe循环性事件,接着执行test,然后再执行内层describe循环性事件,执行内层时需要
再次执行外层的循环性事件,然后一直往内运行;一循环性事件先执行beforeEach,再执行到test之后再执行
afterEach
2如果还有一次性事件beforeAll/afterAll,那么他们是每次test事件第一执行beforeAll,和最后一次test执
行完之后执行afterAll
beforeAll(() => console.log('1 - beforeAll'));
afterAll(() => console.log('1 - afterAll'));
beforeEach(() => console.log('1 - beforeEach'));
afterEach(() => console.log('1 - afterEach'));
test('', () => console.log('1 - test'));
describe('Scoped / Nested block', () => {
beforeAll(() => console.log('2 - beforeAll'));
afterAll(() => console.log('2 - afterAll'));
beforeEach(() => console.log('2 - beforeEach'));
afterEach(() => console.log('2 - afterEach'));
test('', () => console.log('2 - test'));
});
// 1 - beforeAll
// 1 - beforeEach
// 1 - test
// 1 - afterEach
// 2 - beforeAll
// 1 - beforeEach
// 2 - beforeEach
// 2 - test
// 2 - afterEach
// 1 - afterEach
// 2 - afterAll
// 1 - afterAll
通过上面这个例子:可以理解为一组beforeEach/afterEach包裹着一组测试,如果这组测试里面还有此时,就相当于套娃,子测试也会被外面这组beforeEach/afterEach所包裹,并且父子组beforeEach/afterEach执行顺序是先执行父亲,再执行儿子的。
如果测试失败,我们可以单独对那个有误的进行单独测试,从而跳过其它测试
test.only('xxx',()=> {
....
})
以上就是本章节的全部内容,如果对您有帮助的话,帮忙给个支持,万分感谢~