您的位置:主页 > HTML/JS > 如何测试 React Hooks ?
<<上一页

如何测试 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)
  })
})

作者:ariesjia

前端少先队员,光荣的红领巾在胸前飘扬

继续八卦

留言万岁!吹水有益健康!

此站点使用Akismet来减少垃圾评论。了解我们如何处理您的评论数据