jest + enzyme 测试 React Todo 应用

811 阅读2分钟

一 引言

本文通过对react 的 todo 应用做单元测试,来了解一下 jest + enzyme 如何做单测,jest测试环境的搭建本文不做讲解,对照官方文档很容易实现。

todo应用虽然很简单,但是通过单测我们能够了解到如何验证组件在componentDidMount后调用了http接口,如何验证组件更新后的数据,如何实现交互等。

本文实现了如下测试功能:

    1. 如何用jest mock项目中的http接口获取数据
    1. 如何模拟函数
    1. 如何模拟事件
    1. 如何验证数据的正确性

二 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')
}

三 写测试

  1. 创建一个测试文件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', () => {

})
  1. 模拟接口

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))
  }
})
  1. 测试组件在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)
})
  1. 测试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')
})
  1. 测试点击添加按钮
 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)
})
  1. 测试点击完成任务
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')
})

四 测试结果

alt 属性文本