# 使用 Vitest 进行项目测试
# 普通js测试
test.concurrent('handles js', () => {
expect(1 + 1).toBe(2);
});
1
2
3
2
3
# 基于testing-library的dom操作测试
import '@testing-library/jest-dom';
describe('dom test', () => {
test.concurrent('testing-library jest-dom', async () => {
// 创建div,并设置id
const div = document.createElement('div');
div.id = 'adm-mask';
// 此时div不为空
expect(div).not.toBeNull();
expect(div).toBeDefined();
expect(div).toBeInstanceOf(HTMLDivElement);
// 追加到body上
await document.body.appendChild(div);
const mask: HTMLElement = document.body.querySelector('#adm-mask');
expect(mask).toBeInTheDocument();
// 移除div
div.remove();
expect(mask).not.toBeInTheDocument();
});
});
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# 组件测试
这里测的是 antd-mobile,必须以es的形式导入,否则test会报错
# 组件
import demo from '@/assets/demo.svg'; // 继承了vite的alias配置
import styles from './Test.module.scss'; // 使用模块化样式
import { CheckOutline } from 'antd-mobile-icons'; // 使用antd-mobile-icons
import { Loading } from 'antd-mobile/es'; // 使用antd-mobile
import './Demo.scss'; // 使用scss -> npm i sass
import './Demo.less'; // 使用less -> npm i less
export default function Demo() {
return (
<div className="demo a b">
<p data-testid="demo" className={styles.test}>Hello demo</p>
{/* 使用svg标签 */}
<svg xmlns="http://www.w3.org/2000/svg" version="1.1">
<circle cx="100" cy="50" r="40" stroke="black" strokeWidth="2" fill="red" />
</svg>
{/* 使用svg图标 */}
<img src={demo} alt="svg" />
{/* 使用antd mobile的svg标签 */}
<CheckOutline />
{/* 使用antd mobile的组件 */}
<Loading />
</div>
);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
# 测试用例
import Demo from '@/views/test-demo/Demo';
import { render } from '@testing-library/react';
import '@testing-library/jest-dom';
test('render component', async () => {
const { unmount, getByText, findByTestId, container } = await render(<Demo />);
expect(getByText('Hello demo')).not.toBeEmptyDOMElement();
expect((await findByTestId('demo')).textContent).toEqual('Hello demo');
unmount();
expect(container).toBeEmptyDOMElement();
expect(container.innerHTML).toBeFalsy();
});
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# service mock
项目中会遇到对于接口请求的封装,比如将请求统一安排到XxxService.ts中 然后通过await XxxService.queryXxx(..); 去获取数据
- 在XxxService.ts的所在目录下创建__mocks__文件夹
- *touch mocks/*XxxService.ts
- 修改*mocks/*XxxService.ts
// 对,你没看错,就这样简单
import { vi } from 'vitest';
export default {
updateAppointOrderState: vi.fn(() => Promise.resolve(false)),
queryAllGuides: vi.fn(() => Promise.resolve({
guiders: [{ name: 'jerryime' }],
defaultGuider: { name: 'jerryime' },
})),
}
1
2
3
4
5
6
7
8
9
10
2
3
4
5
6
7
8
9
10
// 测试用例方式1:(全局mock测试)
import { BookingService } from '@/services';
import { vi } from 'vitest';
vi.mock('@/services/BookingService'); // 全局mock
describe.skip('mock api', () => {
it('mock async service in global scope', async () => {
const result = false;
const res = await BookingService.updateAppointOrderState(); // 不会走真实接口请求
expect(res).toBe(result);
expect(res).toEqual(result);
expect(res).toBeFalsy();
});
});
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 测试用例方式2:(不使用全局mock)
import { BookingService } from '@/services';
import { vi } from 'vitest';
describe.skip('mock api', () => {
it('mock async service', async () => {
const response = true;
// 手动模拟指定请求函数,返回一次mock数据
// TIPS:该方式也可以覆盖测试用例方式1的数据
const spy = vi.spyOn(BookingService, 'updateAppointOrderState').mockImplementation(() => Promise.resolve(response));
const res = await BookingService.updateAppointOrderState(); // 不会走真实接口请求
expect(res).toEqual(response);
expect(spy).toBeCalledTimes(1);
expect(spy.mock.results[0].value).toBe(response);
});
});
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# axios mock
- 项目root目录创建_mocks__文件夹
- touch _mocks__/axios.ts
import { AxiosRequestConfig, AxiosResponse } from 'axios';
import { vi } from 'vitest';
const getMockResponse = (config: AxiosRequestConfig, type = 'request') => {
let response = null;
const { url } = config;
console.log(`${type} mock invoked url:: `, url);
const res = {
status: 200,
statusText: 'OK',
data: {
code: 200,
data: response,
},
} as AxiosResponse;
// console.log('mock axios response:', res);
return res;
};
const mockByRequest = vi.fn((config = {} as AxiosRequestConfig) => {
const res = getMockResponse(config, 'request');
return Promise.resolve(res);
});
const mockByGet = vi.fn((url = '', params = {}) => {
const res = getMockResponse({ url, params, method: 'get' }, 'get');
return Promise.resolve(res);
});
const mockByPost = vi.fn((url = '', params = {}) => {
const res = getMockResponse({ url, params, method: 'post' }, 'post');
return Promise.resolve(res);
});
const mockByPut = vi.fn((url = '', params = {}) => {
const res = getMockResponse({ url, params, method: 'put' }, 'put');
return Promise.resolve(res);
});
function Axios(config = {} as AxiosRequestConfig) {
if (config.url) {
return Axios.request(config);
}
}
Axios.request = mockByRequest;
Axios.get = mockByGet;
Axios.post = mockByPost;
Axios.put = mockByPut;
export default Axios;
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
39
40
41
42
43
44
45
46
47
48
49
50
51
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
39
40
41
42
43
44
45
46
47
48
49
50
51
// 测试用例
import axios from 'axios';
import { vi, expect, test, JestMockCompatFn, beforeEach } from 'vitest';
vi.mock('axios');
beforeEach(() => {
vi.clearAllMocks();
});
test('axios test:: 全局mock', async () => {
const result = await axios.get('/invalid-path');
expect(axios.patch).toBeUndefined();
expect(axios.get).toHaveBeenCalledWith('/invalid-path');
expect(axios.get).toBeCalledTimes(1);
expect(result).toMatchObject({
status: 200,
statusText: 'OK',
data: { code: 200, data: null },
});
});
test('axios test:: axios(config)调用', async () => {
// axios(config)调用,内部走的是axios.request
const result = await axios({ url: '/invalid-path', method: 'get' });
expect(axios.request).toHaveBeenCalledWith('/invalid-path');
expect(axios.request).toBeCalledTimes(1);
expect(result).toMatchObject({
status: 200,
statusText: 'OK',
data: { code: 200, data: null },
});
});
test('axios test:: 手动mock', async () => {
const defaultVal = 'hello';
(axios.get as JestMockCompatFn<any[], any>).mockResolvedValueOnce(defaultVal);
const result = await axios.get('/xxx');
expect(axios.get).toHaveBeenCalledWith('/xxx');
expect(axios.get).toBeCalledTimes(1);
expect(result).toEqual(defaultVal);
});
test('can get actual axios', async () => {
const ax = await vi.importActual<typeof axios>('axios');
expect(vi.isMockFunction(ax.get)).toBe(false);
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
39
40
41
42
43
44
45
46
47
48
49
50
51
52
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
39
40
41
42
43
44
45
46
47
48
49
50
51
52