一 引言
本文通过对react 的 todo 应用做单元测试,来了解一下 jest + enzyme 如何做单测,jest测试环境的搭建本文不做讲解,对照官方文档很容易实现。
todo应用虽然很简单,但是通过单测我们能够了解到如何验证组件在componentDidMount后调用了http接口,如何验证组件更新后的数据,如何实现交互等。
本文实现了如下测试功能:
-
- 如何用jest mock项目中的http接口获取数据
-
- 如何模拟函数
-
- 如何模拟事件
-
- 如何验证数据的正确性
二 Todo 应用的源码
TodoApp.js
import { getAllTodos } from './api'
const TodoApp = () => {
const [todo, setTodo] = useState("");
const [todoList, setTodoList] = useState([])
useEffect(() => {
getAllTodos().then(todos => {
setTodoList(todos)
})
}, [])
const handleTodoChange = (e) => {
setTodo(e.target.value);
}
const handleTodoAdd = () => {
if (todo) {
todoList.push({ text: todo, completed: false })
setTodoList([...todoList])
setTodo("");
}
}
const handleComplete = (e, item) => {
const newTodoList = todoList.map(t => {
if (t.id === item.id) {
return { ...t, completed: e.target.checked }
} else {
return t
}
})
setTodoList([...newTodoList])
}
return (
<div className="todo-app">
<div className="todo-header"></div>
<div className="todo-form">
<input
className="todo-input"
value={todo}
autoFocus={true}
placeholder="Enter new todo"
onChange={handleTodoChange}
/>
<button className="add-btn" onClick={handleTodoAdd}>ADD</button>
</div>
<div className="todo-list">
<ul>
{todoList.map((item, index) => {
const clx = item.completed ? 'todo-item completed' : 'todo-item'
return (
<li className={clx} key={index}>
<input className="check-input" type="checkbox" defaultChecked={item.completed} onChange={(e) => handleComplete(e, item)} />
<span>
{item.text}
</span>
</li>)
})
}
</ul>
</div>
<div className="todo-footer">
Total: <span className="total">{todoList.length}</span> Uncompleted: <span className="uncompleted">{todoList.filter(item => !item.completed).length}</span>
</div>
</div>
)
}
export default TodoApp
api.js
export function getAllTodos() {
return fetch('/all/todos')
}
三 写测试
- 创建一个测试文件todo.spec.js
import React from 'react'
import { mount } from 'enzyme'
import { act } from 'react-dom/test-utils';
import TodoApp from './TodoApp'
import { getAllTodos } from './Api'
describe('TodoApp', () => {
})
- 模拟接口
todoApp里调用了getAllTodos这个接口,在测试代码里面不会真正去调用这个接口,所以需要通过jest模拟这个接口
const initData = [
{ id: 1, text: 'eat apple', completed: false },
{ id: 2, text: 'play game', completed: false },
{ id: 3, text: 'housework', completed: false },
]
jest.mock('./api', () => {
return {
__esModule: true,
getAllTodos: jest.fn(() => Promise.resolve(initData))
}
})
- 测试组件在compoentDidMount里调用了http的接口,验证渲染结果
test('测试调用getAllTodos接口数据渲染是否正确', async () => {
let wrapper
await act(async () => {
wrapper = mount(<TodoApp />)
})
expect(getAllTodos).toHaveBeenCalled()
wrapper.update()
expect(wrapper.find('.todo-item')).toHaveLength(initData.length)
})
- 测试Total和Uncompleted显示是否正确
test('测试Total和Uncompleted显示是否正确', async () => {
let wrapper
await act(async () => {
wrapper = mount(<TodoApp />)
})
expect(wrapper.find('.total').text()).toBe('3')
expect(wrapper.find('.uncompleted').text()).toBe('3')
})
- 测试点击添加按钮
test('测试点击添加数据', async () => {
let wrapper
await act(async () => {
wrapper = mount(<TodoApp />)
})
wrapper.find('.todo-input').simulate('change', { target: { value: 'go shoping' } });
wrapper.find('.add-btn').simulate('click')
expect(wrapper.find('.todo-item')).toHaveLength(4)
})
- 测试点击完成任务
test('测试点击完成任务', async () => {
let wrapper
await act(async () => {
wrapper = mount(<TodoApp />)
})
wrapper.update()
wrapper.find('.check-input').at(0).simulate('change', { target: { checked: true }})
wrapper.update()
expect(wrapper.find('.total').text()).toBe('4')
expect(wrapper.find('.uncompleted').text()).toBe('3')
wrapper.find('.check-input').at(0).simulate('change', { target: { checked: false }})
expect(wrapper.find('.uncompleted').text()).toBe('4')
})