如何测试 React Hooks ?
如果你是一个 React 开发人员,我相信你一定已经很熟悉了 React Hooks 了。我们今天先简单的回顾一下。
什么是 React Hooks ?
通过 React Hooks 可以编写组件时在不使用 Class 的情况下使用 State,生命周期等等, 可以使组件更加简洁,便于重用逻辑
按照惯例我们还是先上这个熟悉的计数器 demo
import React from "react"
class Counter extends React.Component {
constructor(props) {
super(props)
this.state = {
count: 0
}
}
increment = () => {
this.setState(prevState => ({count: prevState.count + 1}))
}
decrement = () => {
this.setState(prevState => ({count: prevState.count - 1}))
}
render() {
return ()
}
}
接下来我们先通过 useState Hooks 来改写一番
import React, {useState} from "react"
const Counter = function() {
const [count, setCount] = useState(0)
const increment = () => setCount(count+1)
const decrement = () => setCount(count-1)
return ()
}
非常的棒,代码简洁了不少,但是这样的代码结构使得业务逻辑任然无法被其他的组件所重用,为了达到这个目的, 我们使用自定义 Hooks 来抽离出业务逻辑
import React, {useState} from "react"
const useCounter = function(initial) {
const [count, setCount] = useState(initial)
const increment = () => setCount(count+1)
const decrement = () => setCount(count-1)
return { count, increment, decrement }
}
const Counter = function() {
const { count, increment, decrement } = useCounter(0)
return ()
}
上面3份代码是拥有的同样的效果,但是代码结构逐渐改变的更加简洁和清晰。我们可以看到 useCounter
是在组件外部的一个单独函数,他单纯的只有纯业务逻辑。 这意味着它可以导入和导出,其业务逻辑可以轻松地在其他组件中重用了。
回到我们文章的重点,单元测试。写测试的同学都知道函数是最容易被测试的,因为仅仅依赖于其参数的输入及其输出返回值就可以完成测试,我们提供示例输入,并将输出与预期进行比较即可。
从代码看来 React Hooks 就像一个纯函数,实际不然,文档中说明: Hooks 不像通常的函数可以随意的任意地方使用,它只能在 functional 组件 或者其他的 Hooks 函数中所调用,他依赖于 Component render 时候设置的 currentDispatcher,一旦脱离所需要的上下文它就不能够正确的工作。所以我们不能像通常的函数一样测试它。
如何写自定义 React Hooks 的测试?
所以要为自定义 Hooks 写单元测试,需要真的创建一个 Component ,并在这个 Component 中使用你的自定义 Hooks 函数,并且再mount这个组件。就可以进行测试啦, 下面的例子我们基于 react-testing-library 这个测试框架来编写, 例如我们测试 initial count, 以及加法。
import React from "react";
import { act, render } from 'react-testing-library'
import useCounter from './useCounter'
describe("useCounter test", () => {
it('should get initial count', () => {
let result
const Demo = function() {
result = useCounter(0)
return null
}
render()
expect(result.count).toEqual(0)
})
it('should get initial count through params', () => {
let result
const Demo = function() {
result = useCounter(10)
return null
}
render()
expect(result.count).toEqual(10)
})
it('should get increase count', () => {
let result
const Demo = function() {
result = useCounter(0)
return null
}
render()
act(() => {
result.increment()
})
expect(result.count).toEqual(1)
})
})
这样写可以满足我们的测试需求,但是如果有大量的测试用例,会使得整个测试文件充满了为了能测试而写的模板代码。这时候我们就可以引入 hooks-test-util 了
npm install hooks-test-util --dev
hooks-test-util 原理和上面我们借用 Component 进行测试一样,通过 callback 将 hooks 的返回值暴露给调用者,这样就能更加专注的进行业务逻辑的测试了
import React from "react"
import render, { act } from 'hooks-test-util'
import useCounter from './useCounter'
describe("useCounter test", () => {
it('should get initial count', () => {
const {container} =render(() => useCounter(0))
expect(container.hook.count).toEqual(0)
})
it('should get initial count through params', () => {
const {container} =render(() => useCounter(10))
expect(container.hook.count).toEqual(10)
})
it('should get increase count', () => {
const {container} =render(() => useCounter(0))
act(() => {
container.hook.increment()
})
expect(container.hook.count).toEqual(1)
})
})
我们通过访问 container.hook 能够总是拿到当前的 hooks 返回值。是不是简洁了不少,少了模板代码,使得我们能够更加的专注于业务的测试。
当然我们有时候自定义的 Hooks 方法并不是完全单纯的业务,比如是一个表单控件的一个抽象和封装,我们测试的时候肯定期待能够带着dom一起进行测试,才能有足够的信心
如何结合DOM 测试呢?
例如有下面这样一个 Hooks 函数例子,例子是对表单项接口的封装
import {useState} from "react"
const useInputField = name => {
const [value, setValue] = useState('')
const onChange = event => setValue(event.target.value)
return {
name,
value,
onChange,
placeholder: `place input ${name}`,
}
}
我们可以使用 hooks-test-util 的 render option,渲染出我们期望的dom,并进行测试即可
hooks-test-util 是基于 react-testing-library 开发的, 所以 react-testing-library 提供的所有的 dom selector 我们都可以一模一样的使用。 我们的测试思路,
1 测试使用了 hooks,是否按照预期绑定了dom属性
2. 测试我们输入值之后 input 的 value 以及 hooks 中的 state 是否正确。
import React from "react"
import render, { act, cleanup } from 'hooks-test-util'
import userEvent from 'user-event'
import useInputField from './useInputField'
describe("state test", () => {
beforeEach(() => {
cleanup()
})
it('should get input initial input value', () => {
const { container, getByTestId } = render(
() => useInputField('name'),
{
render({ hook }) =>
}
)
const input = getByTestId('input')
expect(input.value).toEqual('')
expect(input.placeholder).toEqual('place input name')
})
it('should set value when trigger input event', () => {
const { container, getByTestId } = render(
() => useInputField('name'),
{
render({ hook }) =>
}
)
const input = getByTestId('input')
const text = "Hello, World!";
act(() => {
userEvent.type(input, text);
})
expect(input.value).toEqual(text)
expect(container.hook.value).toEqual(text)
})
})