随着我在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这样的东西),那么无论你是使用渲染道具还是其他东西,你都不需要测试任何不同的东西。你只需以用户的方式与组件进行交互(键入输入,选择项目等)。这可能是显而易见的,但我认为这提出了一个相当重要的问题。你在"测试金字塔 "上的位置越高,实现细节就越不重要,当你往下走时,你必须处理更多的实现细节。

集成测试
这就是说,我建议把重点放在集成测试上。有了集成测试,你同样不需要对测试组件的方式有太多的改变。下面是回购中的集成测试。你会注意到,没有迹象表明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),你只需在一个提供者中渲染你的组件。我在我的前端大师测试研讨会上有一些这样做的例子。
我希望这对你有帮助!祝您好运!