思考测试
- 什么是单元测试?什么事集成测试
如果一个测试,需要用到真实的系统时间,真实的文件系统,或者真实的数据库,那么这个测试就进入了集成测试的范围。我们开发常说的测试属于单元测试的范畴 - 测试的作用是什么
- 保证项目的稳定
- 提升代码的质量(一个好的代码,一定是可测试的)
- 应该测试什么
- 只测试输入和输出的正确性,不测试代码实现
- 只测试自己的代码,不测试三方库,或者引入的其他代码(三方库、其他代码的稳定应该由各自自己保证)
- 测试交互
- 测试结果分支
- 测试渲染结果
- 一个UI测试框架应该具备哪些能力
- 渲染页面 render(显示正确)
- 事件触发 event(交互正确)
- mock操作,callback,timer、fetch、fs 等,因为这些东西属于三方提供的东西,不应该由自己的代码保证正确
- 其他
- 断言
- 易编写
- 良好的错误提示等
demo
import React, { useState, useEffect } from 'react'
import { Radio, Button } from 'antd'
import { RadioChangeEvent } from 'antd/lib/radio';
export default function Calc(props: { id?: number }) {
const [value, setValue] = useState<'increase' | 'decrease'>('increase');
const [count, setCount] = useState(0);
const [userName, setUserName] = useState('')
function handleChange(e: RadioChangeEvent) {
setValue(e.target.value)
}
function handleClick () {
setCount(value === 'increase' ? count + 1 : count - 1)
}
async function fetchUserData(userId: number) {
const response = await fetch("/" + userId);
setUserName(await response.json());
}
useEffect(() => {
fetchUserData(props.id!);
}, [props.id]);
return (
<div>
<div id="user-name">copy right {userName}</div>
{value === 'increase' ? <div>每次加一</div> : <div>每次减一</div>}
<Radio.Group id="test" onChange={handleChange} value={value}>
<Radio value={1}>+</Radio>
<Radio value={2}>-</Radio>
</Radio.Group>
<div>
<span>{count}</span>
<Button onClick={handleClick}>modify</Button>
</div>
</div>
)
}
需要测试的东西
- 能正常渲染
- value === ‘increase’ 分支
- radio onChange 触发逻辑
- Button onclick 触发逻辑
- username 正常渲染(根据ajax返回结果渲染)
import React from 'react'
import { render, act } from '@testing-library/react'
import Calc from './index'
beforeAll(() => {
const fakerUser = 'enochjs'
jest.spyOn(global, 'fetch').mockImplementation(() => {
return Promise.resolve({
json: () => Promise.resolve(fakerUser)
}) as any
})
})
describe.only('test calc', () => {
test('render right', async () => {
let container: any
await act(async () => {
container = render(<Calc />)
})
expect(container.queryByText('每次加一')).toBeInTheDocument()
expect(container.queryByText('0')).toBeInTheDocument()
})
test('radio change', async () => {
let container: any
await act(async () => {
container = render(<Calc />)
})
const radios = document.querySelectorAll('input')
act(() => {
radios[1].dispatchEvent(new MouseEvent("click", { bubbles: true }));
});
expect(container.queryByText('每次减一')).toBeInTheDocument()
})
test('count change', async () => {
let container: any
await act(async () => {
container = render(<Calc />)
})
const button = container.queryByText('modify')
act(() => {
button.dispatchEvent(new MouseEvent("click", { bubbles: true }));
});
expect(container.queryByText('1')).toBeInTheDocument()
const radios = document.querySelectorAll('input')
act(() => {
radios[1].dispatchEvent(new MouseEvent("click", { bubbles: true }));
});
act(() => {
button.dispatchEvent(new MouseEvent("click", { bubbles: true }));
});
expect(container.queryByText('0')).toBeInTheDocument()
})
test('render with fetch data', async () => {
let container: any
await act(async () => {
container = render(<Calc id={123} />)
})
expect(container.queryByText("copy right enochjs")).toBeInTheDocument();
})
})
__mock__ 文件夹
__mock__ 文件需要放在 request的同级目录下
// request.js
const http = require('http');
export default function request(url) {
return new Promise(resolve => {
// This is an example of an http request, for example to fetch
// user data from an API.
// This module is being mocked in __mocks__/request.js
http.get({path: url}, response => {
let data = '';
response.on('data', _data => (data += _data));
response.on('end', () => resolve(data));
});
});
}
// __mocks__/request.js
const users = {
4: {name: 'Mark'},
5: {name: 'Paul'},
};
export default function request(url) {
return new Promise((resolve, reject) => {
const userID = parseInt(url.substr('/users/'.length), 10);
process.nextTick(() =>
users[userID]
? resolve(users[userID])
: reject({
error: 'User with ' + userID + ' not found.',
}),
);
});
}
// user.js
import request from './request';
export function getUserName(userID) {
return request('/users/' + userID).then(user => user.name);
}
// test
jest.mock('./request');
import * as user from './user';
// The assertion for a promise must be returned.
it('works with promises', () => {
expect.assertions(1);
return user.getUserName(4).then(data => expect(data).toEqual('Mark'));
});
// Testing for async errors using Promise.catch.
it('tests error with promises', () => {
expect.assertions(1);
return user.getUserName(2).catch(e =>
expect(e).toEqual({
error: 'User with 2 not found.',
}),
);
});
// Or using async/await.
it('tests error with async/await', async () => {
expect.assertions(1);
try {
await user.getUserName(1);
} catch (e) {
expect(e).toEqual({
error: 'User with 1 not found.',
});
}
});
// Testing for async errors using `.rejects`.
it('tests error with rejects', () => {
expect.assertions(1);
return expect(user.getUserName(3)).rejects.toEqual({
error: 'User with 3 not found.',
});
});
// Or using async/await with `.rejects`.
it('tests error with async/await and rejects', async () => {
expect.assertions(1);
await expect(user.getUserName(3)).rejects.toEqual({
error: 'User with 3 not found.',
});
});
mockTimer
// timeGame
export default function infiniteTimerGame(callback) {
console.log('Ready....go!');
setTimeout(() => {
console.log("Time's up! 10 seconds before the next game starts...");
callback && callback();
// Schedule the next game in 10 seconds
setTimeout(() => {
infiniteTimerGame(callback);
}, 10000);
}, 1000);
}
// test
import infiniteTimerGame from './timerGame'
jest.useFakeTimers();
describe('infiniteTimerGame', () => {
test('schedules a 10-second timer after 1 second', () => {
// const infiniteTimerGame = require('../infiniteTimerGame');
const callback = jest.fn();
infiniteTimerGame(callback);
// At this point in time, there should have been a single call to
// setTimeout to schedule the end of the game in 1 second.
expect(setTimeout).toHaveBeenCalledTimes(1);
expect(setTimeout).toHaveBeenLastCalledWith(expect.any(Function), 1000);
// Fast forward and exhaust only currently pending timers
// (but not any new timers that get created during that process)
jest.runOnlyPendingTimers();
// At this point, our 1-second timer should have fired it's callback
expect(callback).toBeCalled();
// And it should have created a new timer to start the game over in
// 10 seconds
expect(setTimeout).toHaveBeenCalledTimes(2);
expect(setTimeout).toHaveBeenLastCalledWith(expect.any(Function), 10000);
});
});