使用渲染道具测试⚛️组件

57 阅读4分钟

随着我在egghead.io 上的高级 React 组件模式课程的发布,很多人都在问我关于渲染道具的问题。特别是关于测试的问题。也许我最终会在egghead.io上创建一个关于测试 React 组件的课程。在那之前,我决定写这篇关于一些方法的文章,这些方法可以帮助你测试一个渲染道具组件的组件 :)

注意:这不是关于如何测试实现渲染道具模式的组件,而是关于如何测试使用实现渲染道具模式的_组件的组件 :)

在准备这篇博文时,我创建了此版本它是完全有效的,如果你想了解更多细节,你可以看一下 :)在那个 repo 中,我们有一个叫做FruitAutocomplete 的组件,(基本上)是这样实现的:

import * as React from 'react'
import {render} from 'react-dom'
import Downshift from 'downshift'

const items = ['apple', 'pear', 'orange', 'grape', 'banana']

function FruitAutocomplete({onChange}) {
  return (
    <Downshift
      onChange={onChange}
      render={({
        getInputProps,
        getItemProps,
        getLabelProps,
        isOpen,
        inputValue,
        highlightedIndex,
        selectedItem,
      }) => (
        <div>
          <label {...getLabelProps()}>Enter a fruit</label>
          <input {...getInputProps()} />
          {isOpen ? (
            <div data-test="menu">
              {items
                .filter(i => !inputValue || i.includes(inputValue))
                .map((item, index) => (
                  <div
                    {...getItemProps({
                      key: item,
                      'data-test': `item-${item}`,
                      index,
                      item,
                      style: {
                        backgroundColor:
                          highlightedIndex === index ? 'lightgray' : 'white',
                        fontWeight: selectedItem === item ? 'bold' : 'normal',
                      },
                    })}
                  >
                    {item}
                  </div>
                ))}
            </div>
          ) : null}
        </div>
      )}
    />
  )
}

export default FruitAutocomplete

端到端测试

首先,我应该说,渲染道具实际上只是一个实现细节。因此,如果你正在编写E2E测试(使用像令人惊叹的Cypress.io这样的东西),那么无论你是使用渲染道具还是其他东西,你都不需要测试任何不同的东西。你只需以用户的方式与组件进行交互(键入输入,选择项目等)。这可能是显而易见的,但我认为这提出了一个相当重要的问题。你在"测试金字塔 "上的位置越高实现细节就越不重要,当你往下走时,你必须处理更多的实现细节。

UI, Service, Unit

集成测试

这就是说,我建议把重点放在集成测试上。有了集成测试,你同样不需要对测试组件的方式有太多的改变。下面是回购中的集成测试。你会注意到,没有迹象表明FruitAutocomplete 组件是用渲染道具组件实现的(一个实现细节)。

import * as React from 'react'
import {mount} from 'enzyme'
import FruitAutocomplete from '../fruit-autocomplete'

// some handy utilities
// learn more about this `sel` function
// from my other blog post: http://kcd.im/sel-util
const sel = id => `[data-test="${id}"]`
const hasMenu = wrapper => wrapper.find(sel('menu')).length === 1

test('menu is closed by default', () => {
  const wrapper = mount(<FruitAutocomplete />)
  expect(hasMenu(wrapper)).toBe(false)
})

test('lists fruit with a keydown of ArrowDown on the input', () => {
  const wrapper = mount(<FruitAutocomplete />)
  const input = wrapper.find('input')
  input.simulate('keydown', {key: 'ArrowDown'})
  expect(hasMenu(wrapper)).toBe(true)
})

test('can search for and select "banana"', () => {
  const onChange = jest.fn()
  const wrapper = mount(<FruitAutocomplete onChange={onChange} />)
  const input = wrapper.find('input')
  input.simulate('change', {target: {value: 'banana'}})
  input.simulate('keydown', {key: 'ArrowDown'})
  input.simulate('keydown', {key: 'Enter'})
  expect(onChange).toHaveBeenCalledTimes(1)
  const downshift = expect.any(Object)
  expect(onChange).toHaveBeenCalledWith('banana', downshift)
  expect(input.instance().value).toBe('banana')
})

**那么,你如何测试一个使用渲染道具组件的组件呢?**如果你使用E2E或集成测试,你几乎不需要做任何不同的事情。只要装上你的组件,并以你通常的方式与之互动。我应该注意的一点是,downshift 本身就是一个非常好的测试组件,所以你不应该测试它提供的开箱即用的交互。只需关注你的组件正在做什么。这就是我的建议:把你的渲染道具组件测试得非常好,然后为组件的用户做一些高层次的测试。

单元测试

关于单元测试,事情变得有点棘手。如果你不想在你的测试中包括downshift ,那么你就必须获得对你传递给render prop的函数的访问。有几种方法可以做到这一点。

第一个也是最明显的方法是提取renderprop函数并导出。

function FruitAutocomplete({onChange}) {
  return <Downshift onChange={onChange} render={fruitAutocompleteRender} />
}

// NOTE: this is _not_ technically component, it's _like_ a function component
// but it's not rendered with React.createElement, so it's simply
// a function that returns JSX.
function fruitAutocompleteRender(arg) {
  return <div>{/* what you render */}</div>
}

export {fruitAutocompleteRender}
export default FruitAutocomplete

现在你可以将该函数直接导入你的测试中,并像这样使用它来渲染JSX。

import * as React from 'react'
import {render} from 'enzyme'

const downshiftStub = {
  isOpen: false,
  getLabelProps: p => p,
  getInputProps: p => p,
  getItemProps: p => p,
}

const sel = id => `[data-test="${id}"]`
const hasMenu = wrapper => wrapper.find(sel('menu')).length === 1
const hasItem = (wrapper, item) =>
  wrapper.find(sel(`item-${item}`)).length === 1
const renderFruitAutocompleteRenderer = props =>
  render(fruitAutocompleteRender({...downshiftStub, ...props}))

test('shows no menu when isOpen is false', () => {
  const wrapper = renderFruitAutocompleteRenderer({isOpen: false})
  expect(hasMenu(wrapper)).toBe(false)
})

test('shows the menu when isOpen is true', () => {
  const wrapper = renderFruitAutocompleteRenderer({isOpen: true})
  expect(hasMenu(wrapper)).toBe(true)
})

test('when the inputValue is banana, it shows banana', () => {
  const wrapper = renderFruitAutocompleteRenderer({
    isOpen: true,
    inputValue: 'banana',
  })
  expect(hasItem(wrapper, 'banana')).toBe(true)
})

所以这样做很好有几件事需要注意:

  • 这样做需要的代码更少,而且明显更简单
  • 我们必须把downshift 传递给我们的东西存根出来
  • 我们必须将渲染道具提取到一个单独的函数中,并将其导出。

这两点让我很困扰。不过,还有一种方法可以在不提取和导出渲染道具的情况下获取它。下面是最后一个测试,就像我们没有导出渲染道具的函数一样。

import * as React from 'react'
import {mount, render} from 'enzyme'
import Downshift from 'downshift'
import FruitAutocomplete from '../fruit-autocomplete'

const downshiftStub = {
  isOpen: false,
  getLabelProps: p => p,
  getInputProps: p => p,
  getItemProps: p => p,
}

test('when the inputValue is banana, it shows banana', () => {
  const fruitAutocompleteRender = mount(<FruitAutocomplete />)
    .find(Downshift)
    .prop('render')
  const wrapper = render(
    fruitAutocompleteRender({
      ...downshiftStub,
      isOpen: true,
      inputValue: 'banana',
    }),
  )
  expect(hasItem(wrapper, 'banana')).toBe(true)
})

我也不太喜欢这样,因为我不喜欢说。"嘿,FruitAutocomplete,我知道你在使用Downshift,而Downshift使用了一个叫render的道具"。对我来说,这是对实现细节的进一步挖掘。

另外,这仍然没有解决我所关心的存根问题,即downshift 。请在这篇博文中阅读更多我对此的看法。

实际上,我们还有另一种方法可以做到这一点,那就是用jest.mock 来模拟downshift 模块。但我不打算创造一个这样的例子,因为它没有更好的办法:)

结论

所以我建议你在这里坚持使用集成测试,而不要费力地对你的渲染函数进行单元测试。我认为如果你这样做,你会有更多的信心,事情不会被破坏。

我还应该注意到,对于一些需要提供者存在的组件(如react-redux 或 React Router),你只需在一个提供者中渲染你的组件。我在我的前端大师测试研讨会上有一些这样做的例子

我希望这对你有帮助!祝您好运!