React 中的组件测试:使用 Jest 和 Enzyme 测试什么以及如何测试
对于初学者和经验丰富的开发人员来说,测试 React 组件可能颇具挑战性。将您自己的方法与我们在项目中使用的方法进行比较可能会很有趣。为了覆盖代码库,您必须知道哪些组件必须测试,以及组件中的哪些代码应该被覆盖。
在阅读过程中,我将讨论以下主题:
- 根据项目结构定义组件测试的正确顺序
- 找出测试覆盖范围内需要省略的内容(不需要测试的内容)
- 确定快照测试的必要性
- 定义组件中要测试的内容以及测试顺序
- 提供详细的自定义代码示例
本文要求读者已了解 Jest 和 Enzyme 的设置。有关安装和配置的信息可在网上或官方网站上轻松找到。
假设以下情况:您需要用测试覆盖项目代码库,那么您应该从哪里开始,测试结束时又应该达到什么水平?100% 的测试覆盖率?这是您应该追求的指标,但在大多数情况下您无法达到。为什么?因为您不应该测试所有代码。我们将找出原因以及哪些代码应该从测试中剔除。此外,100% 的测试覆盖率并不一定能确保组件经过全面测试。此外,如果组件发生任何更改,也无法保证它会通知您。不要追求百分比,避免编写虚假测试,并且尽量不要遗漏主要的组件细节。
根据项目结构定义组件测试的正确顺序
让我们在项目结构的下一部分讨论这个问题:
我选择shared
这个目录是因为它最为重要;它包含了项目多个不同页面中使用的组件。这些组件可重用,通常体积小巧,结构简单。如果某个组件发生故障,会导致其他地方也发生故障。因此,我们应该确保这些组件的编写正确。该目录的结构分为多个文件夹,每个文件夹包含相应的组件。
如何定义shared
目录中组件测试的正确顺序:
- 始终遵循从简到繁的规则。分析每个目录并定义哪些组件是独立的
independent
——也就是说,它们的渲染不依赖于其他组件;它们是自完成的,可以作为一个单元单独使用。从上面的结构来看,它是一个文件夹inputs
下的目录forms
。它包含 redux-forms 的输入组件,例如 TextInput、SelectInput、CheckboxInput、DateInput 等。 - 接下来,我需要定义一些辅助组件,这些组件经常在
inputs
组件中使用,但应该单独进行测试。它是utils
一个目录。此文件夹中的组件并不复杂,但非常重要。它们经常被重复使用,并有助于执行重复的操作。 - 下一步是定义哪些组件也可以独立使用。如果有的话,就把它们拿去测试。从我们的结构来看,它们是
widgets
功能简单的小组件。它们将是测试覆盖队列中的第三个项目。 - 进一步分析其余目录并定义更复杂的组件,这些组件可以独立使用或与其他组件结合使用。
modals
在我们的例子中,它是目录;这些组件将在下面详细解释。 - 测试组件最复杂的部分留到了最后。它们是
hoc
目录和文件fields
夹forms
。如何定义哪个应该先测试?我选择已在测试组件中使用过的目录。因此,hoc
目录中的组件存在于widgets
组件中;因此我已经知道这个目录及其组件的用途和使用位置。 - 最后一个是
fields
文件夹;它包含与 redux-forms 连接的组件。
最终组件顺序(基于我们的示例)将如下所示:
按照此顺序,逐步增加测试组件的复杂性;因此,当涉及到操作更复杂的组件时,您已经了解最小组件的行为方式。例如,如果您不确定如何测试“文本”字段,请不要测试“数组”字段;如果您尚未测试“表单”字段本身,请不要使用 redux-form 装饰的组件。在选择时保持一致,不要选择第一个想到的组件,并根据逻辑进行切换。当然,您的项目结构可以有所不同;它可以有其他目录名称,也可以包含其他组件、操作和 Reducer,但定义测试组件顺序的逻辑是相同的。
让我们定义一下测试覆盖率中应该省略的内容:
- 第三方库。请勿测试从其他库中获取的功能;您无需对该代码负责。如果您需要它来测试您的代码,请跳过它或模拟其实现。
- 常量。顾名思义,它们是不可更改的;它是一组静态代码,不打算改变。
- 内联样式(如果您在组件中使用它们)。为了测试内联样式,您需要在测试中复制包含样式的对象;如果样式对象发生变化,您也必须在测试中对其进行更改。不要在测试中复制组件的代码;您永远不会记得在测试中更改它。而且,您的同事也永远不会猜到复制的内容。在大多数情况下,内联样式不会改变组件的行为;因此,不应对其进行测试。但如果您的样式动态变化,则可能会有例外。
- 与被测组件无关的内容。跳过对已导入测试组件的测试覆盖;如果该组件被其他组件包装,请务必小心。不要测试包装器,只需单独分析和测试它们即可。
那么如何实际编写测试呢?我结合了两种测试方法:
- 快照测试
- 组件逻辑测试
如果您想确保用户界面没有发生改变,快照测试是一个非常有用的测试工具。首次接触这款测试工具时,可能会遇到关于快照的组织和管理的问题。它的工作原理非常简单,但遗憾的是,目前还没有任何地方对其进行完整的描述;在官方网站 jestjs.io 上,对快照测试工作原理的描述非常少。
如何使用快照进行测试
步骤 1.为组件编写测试,并在期望块中使用.toMatchSnapshot()
创建自身的方法Snapshot
。
it('render correctly text component', () => {
const TextInputComponent = renderer.create(<TextInput />).toJSON();
expect(TextInputComponent).toMatchSnapshot();
});
第 2 步。当您在第一级第一次运行测试时,随着测试的进行,将会创建一个名为的目录,__snapshots__
其中包含自动生成的文件,扩展名为.snap
。
快照如下:
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Render TextInput correctly component 1`] = `
<input
className="input-custom"
disabled={undefined}
id={undefined}
name={undefined}
onBlur={undefined}
onChange={[Function]}
pattern={undefined}
placeholder={undefined}
readOnly={false}
required={undefined}
type="text"
value={undefined}
/>
`;
步骤 3.将快照推送到存储库并将其与测试一起存储。
如果组件已更改,则只需使用—updateSnapshot
标志更新快照或使用快照形式的u
标志。
快照已创建;它是如何工作的?
让我们考虑两种情况:
1. 组件已更改
- 运行测试
- 创建新快照,并与目录中存储的自动生成的快照进行比较
__snapshots__
- 由于快照不同,测试失败
2. 组件没有改变
- 运行测试
- 创建新快照,并与目录中存储的自动生成的快照进行比较
__snapshots__
- 由于快照相同,测试通过
当我测试一个没有逻辑、只有 UI 渲染的小组件时,一切正常,但实践表明,实际项目中没有这样的组件。即使有,数量也很少。
是否有足够的快照进行完整的组件测试?
组件测试的主要说明
1. 每个组件应该只有一个快照。如果一个快照失败,其他快照很可能也会失败,所以不要创建和存储一堆不必要的快照,这会堵塞空间,并让后续阅读测试的开发人员感到困惑。当然,当您需要测试组件在两种状态下的行为时,也有例外;例如,在弹出窗口打开之前和打开之后的状态下。但是,即使是这种情况,也可以用以下方法替代:第一个测试在快照中存储组件在没有弹出窗口时的默认状态,第二个测试模拟事件并检查特定类的存在。通过这种方式,您可以轻松避免创建多个快照。
2. 测试道具:通常,我会将道具的测试分为两个测试:
defaultProps
首先,检查默认 prop 值的渲染;当渲染组件时,如果此 prop 具有,我希望其值与 相等defaultProps
。- 其次,检查 prop 的自定义值;我设置了自己的值,并希望在组件渲染后收到它。
3. 测试数据类型:为了测试 props 中传入的数据类型,或者某些操作后会返回什么类型的数据,我使用了 jest-extended(Jest 的附加匹配器)这个特殊的库,它提供了 Jest 中没有的扩展匹配器。有了这个库,测试数据类型会变得更加轻松有趣。测试 proptypes 是一个矛盾的问题。有些开发者可能会反对测试 proptypes,因为它是第三方包,不应该进行测试,但我坚持测试组件的 proptypes,因为我不会测试包本身的功能;我只是确保 proptypes 的正确性。数据类型是一个非常重要的编程部分,不应该被忽略。
4. 事件测试:创建快照并用测试覆盖 props 后,您可以确保组件的正确渲染,但如果组件中存在事件,这不足以实现全面覆盖。您可以通过多种方式检查事件;最广泛使用的方式是:
- 模拟事件 => 模拟它 => 期望事件被调用
- 模拟事件 => 用参数模拟事件 => 期望事件通过传递的参数被调用
- 传递必要的道具 => 渲染组件 => 模拟事件 => 期望调用事件的特定行为
5. 测试条件:通常,您可以为特定类的输出、渲染特定代码段、传递所需的 props 等设置条件。不要忘记这一点,因为使用默认值时,只有一个分支会通过测试,而第二个分支将无法通过测试。在包含大量计算和条件的复杂组件中,您可能会遗漏一些分支。为了确保代码的所有部分都已被测试覆盖,请使用测试覆盖率工具,并直观地检查哪些分支已覆盖,哪些未覆盖。
6.状态测试:为了检查状态,大多数情况下,需要编写两个测试:
- 第一个检查当前状态。
- 第二个测试在调用事件后检查状态。渲染组件 => 直接在测试中调用函数 => 检查状态如何变化。要调用组件的函数,你需要获取组件的实例,然后才能调用其方法(示例见下一个测试)。
完成以上说明后,您的组件将覆盖 90% 到 100%。我留出了 10% 以应对文章中未提及但可能在代码中出现的特殊情况。
测试示例
让我们逐步转到示例并按照上述结构对组件进行测试。
1. 从表单/输入测试组件。
从 forms/inputs 目录中取出一个组件;让它成为 DateInput.js,即 datepicker 字段的组件。
测试组件的代码列表:DateInput.js
如下所示:
DateInput 组件使用库 react-datepicker,它有两个实用程序:valueToDate(将值转换为日期)和 dateToValue(反之亦然)、用于操作日期的 moment 包和用于检查 React props 的 PropTypes。
根据组件代码,我们可以看到组件渲染所需的默认 props 列表:
const defaultProps = {
inputClassName: 'input-custom',
monthsShown: 1,
dateFormat: 'DD.MM.YYYY',
showMonthYearsDropdowns: false,
minDate: moment()
};
所有 props 都适用于创建快照,除了一个 props 之外minDate: moment()
,moment() 每次运行测试时都会返回当前日期,而快照会失败,因为它存储的是过期日期。解决方案是模拟这个值:
const defaultProps = {
minDate: moment(0)
}
我需要在每个渲染的组件中使用 minDate 属性;为了避免属性重复,我创建了 HOC,它接收 defaultProps 并返回漂亮的组件:
import TestDateInput from '../DateInput';
const DateInput = (props) =>
<TestDateInput
{...defaultProps}
{...props}
/>;
不要忘记moment-timezone
,尤其是当您的测试将由来自不同国家/地区、不同时区的开发人员运行时。他们将收到模拟值,但会受到时区偏移的影响。解决方案是设置默认时区:
const moment = require.requireActual('moment-timezone').tz.setDefault('America/Los_Angeles')
现在日期输入组件已准备好进行测试:
1.首先创建快照:
it('render correctly date component', () => {
const DateInputComponent = renderer.create(<DateInput />).toJSON();
expect(DateInputComponent).toMatchSnapshot();
});
2.测试道具:
查看道具并找到重要的;要测试的第一个道具是 showMonthYearsDropdowns,如果设置为 true,则会显示月份和年份的下拉菜单:
it('check month and years dropdowns displayed', () => {
const props = {
showMonthYearsDropdowns: true
},
DateInputComponent = mount(<DateInput {...props} />).find('.datepicker');
expect(DateInputComponent.hasClass('react-datepicker-hide-month')).toEqual(true);
});
测试 null prop 值;需要进行此检查以确保组件在没有定义值的情况下呈现:
it('render date input correctly with null value', () => {
const props = {
value: null
},
DateInputComponent = mount(<DateInput {...props} />);
expect((DateInputComponent).prop('value')).toEqual(null);
});
3.测试 proptypes 的值,日期应为字符串:
it('check the type of value', () => {
const props = {
value: '10.03.2018'
},
DateInputComponent = mount(<DateInput {...props} />);
expect(DateInputComponent.prop('value')).toBeString();
});
4.测试项目:
4.1.检查 onChange 事件,模拟 onChange 回调 => 渲染日期输入组件 => 然后使用新的目标值模拟 change 事件 => 最后检查 onChange 事件是否已使用新值调用。
it('check the onChange callback', () => {
const onChange = jest.fn(),
props = {
value: '20.01.2018',
onChange
},
DateInputComponent = mount(<DateInput {...props} />).find('input');
DateInputComponent.simulate('change', { target: {value: moment('2018-01-22')} });
expect(onChange).toHaveBeenCalledWith('22.01.2018');
});
4.2.确保在点击日期输入后打开日期选择器弹出窗口,为此,找到日期输入 => 模拟点击事件 => 并期望.react-datepicker
出现带有类的弹出窗口。
it('check DatePicker popup open', () => {
const DateComponent = mount(<DateInput />),
dateInput = DateComponent.find("input[type='text']");
dateInput.simulate('click');
expect(DateComponent.find('.react-datepicker')).toHaveLength(1);
});
完整测试列表: DateInput.test.js
2.实用性测试:
测试实用程序的代码清单: valueToDate.js
此实用程序的目的是将值转换为自定义格式的日期。
首先,让我们分析一下给定的实用程序并定义测试的主要用例:
1.根据此实用程序的用途,它会转换值,因此我们需要检查此值:
- 如果值未定义:我们需要确保实用程序不会返回异常(错误)。
- 如果值已定义:我们需要检查实用程序是否返回时刻日期。
2.返回值应该属于 moment 类;这就是为什么它应该是 moment 的实例。
3.第二个参数是 dateFormat;在测试之前将其设置为常量。这就是为什么它会在每个测试中被传递,并根据日期格式返回值。我们应该单独测试 dateFormat 吗?我想不需要。这个参数是可选的;如果我们不设置 dateFormat,该实用程序不会崩溃,它只会以默认格式返回日期;这是 moment 的工作,我们不应该测试第三方库。
正如我之前提到的,我们不应该忘记 moment-timezone;这一点非常重要,尤其是对于来自不同时区的开发人员而言。
让我们编写代码:
1. 为第一种情况编写测试;当我们没有值时,它是空的。
const format = 'DD.MM.YYYY';
it('render valueToDate utility with empty value', () => {
const value = valueToDate('', format);
expect(value).toEqual(null);
});
2.检查值是否已定义。
const date = '21.11.2015',
format = ‘DD.MM.YYYY’;
it('render valueToDate utility with defined value', () => {
const value = valueToDate(date, format);
expect(value).toEqual(moment(date, format));
});
3.检查该值是否属于时刻类。
const date = '21.11.2015',
format = 'DD.MM.YYYY';
it('check value is instanceof moment', () => {
const value = valueToDate(date, format);
expect(value instanceof moment).toBeTruthy();
});
完整测试列表: valueToDate.test.js
3. 小部件测试
为了测试小部件,我使用了 Spinner 组件。
测试小部件的代码清单: Spinner.js
看起来像:
Spinner 组件就不用解释了,几乎所有网页资源都有这个组件。
直接写测试吧:
1.第一步-创建快照:
it('render correctly Spinner component', () => {
const SpinnerComponent = mount(<Spinner />);
expect(SpinnerComponent).toMatchSnapshot();
});
2.测试道具:
2.1默认道具标题,检查是否正确呈现。
it('check prop title by default', () => {
const SpinnerComponent = mount(<Spinner />);
expect(SpinnerComponent.find('p').text()).toEqual('Please wait');
});
2.2检查自定义属性 title;我需要检查它是否返回正确定义的属性。查看代码,将 title 封装在 rawMarkup 工具中,并借助 dangerlySetInnerHTML 属性进行输出。
rawMarkup 实用程序的代码清单:
export default function rawMarkup(template) {
return {__html: template};
}
我们需要在 Spinner 组件中添加 rawMarkup 的测试吗?不需要,它是一个独立的工具,应该与 Spinner 分开测试。我们不关心它是如何工作的;我们只需要确保 title 属性返回正确的结果。
说明:使用 dangerlySetInnerHTML 属性的原因如下。我们的网站是多语言的,翻译营销团队负责。他们可以简单地用单词组合来翻译,甚至可以用 HTML 标签(例如<strong>
、<i>
)来修饰文本,甚至可以用、<s>
列表来切片文本;我们不确定他们是如何翻译和修饰文本的。我们只需要正确渲染所有这些内容。<ol>
<ul>
我在一个测试中合并了两个主要测试用例:
- 返回正确的自定义道具标题
- 使用 html 标签正确渲染 prop title
it('check prop title with html tags', () => {
const props = {
title: '<b>Please wait</b>'
},
SpinnerComponent = mount(<Spinner {...props} />);
expect(SpinnerComponent.find('p').text()).toEqual('Please wait');
});
采用下一个道具 subTitle;它是可选的,这就是它没有默认道具的原因,因此跳过使用默认道具的步骤并测试自定义道具:
- 检查 subTitle 属性中的文本是否正确呈现:
const props = {
subTitle: 'left 1 minute'
},
SpinnerComponent = mount(<Spinner {...props} />);
it('render correct text', () => {
expect(SpinnerComponent.find('p').at(1).text()).toEqual(props.subTitle);
});
我们知道 subTitle 是可选的;因此我们需要根据切片标记检查它是否未使用默认 props 渲染。只需检查标签数量<p>
:
it('check subTitle is not rendered', () => {
const SpinnerComponent = mount(<Spinner />);
expect(SpinnerComponent.find('p').length).toEqual(1);
});
3.测试道具类型:
- 对于标题属性,预计为字符串:
it('check prop type for title is string', () => {
const props = {
title: 'Wait'
},
SpinnerComponent = mount(<Spinner {...props} />);
expect(SpinnerComponent.find('p').text()).toBeString();
});
- 对于 subTitle 属性也应为字符串:
const props = {
subTitle: 'left 1 minute'
},
SpinnerComponent = mount(<Spinner {...props} />);
it('type for subTitle is string', () => {
expect(SpinnerComponent.find('p').at(1).text()).toBeString();
});
完整测试列表: Spinner.test.js
4. 模态框测试(ModalWrapper.js 和 ModalTrigger.js)
如何测试模态框:
首先,我想解释一下我们项目中模态框是如何组织的。我们有两个组件:ModalWrapper.js和ModalTrigger.js。
ModalWrapper负责弹出窗口的布局。它包含模态框容器、“关闭”按钮、模态框标题和主体部分。
ModalTrigger负责模态框的处理。它包含 ModalWrapper 布局,以及用于模态框布局控制(打开、关闭操作)的事件。
我分别概述了每个组件:
1.测试组件的代码清单: ModalWrapper.js
让我们编写代码:
1.1 ModalWrapper 接收组件并在其中渲染。首先,检查 ModalWrapper 是否在没有组件的情况下失败。使用默认 props 创建快照:
it('without component', () => {
const ModalWrapperComponent = shallow(<ModalWrapper />);
expect(ModalWrapperComponent).toMatchSnapshot();
});
1.2接下来通过 props 传递组件渲染来模拟其实际情况:
it('with component', () => {
const props = {
component: () => {}
},
ModalWrapperComponent = shallow(<ModalWrapper {...props} />);
expect(ModalWrapperComponent).toMatchSnapshot();
});
1.3测试道具:
接收自定义类名 prop:
it('render correct class name', () => {
const props = {
modalClassName: 'custom-class-name'
},
ModalWrapperComponent = shallow(<ModalWrapper {...props} />).find('Modal');
expect(ModalWrapperComponent.hasClass('custom-class-name')).toEqual(true);
});
接收自定义标题道具:
it('render correct title', () => {
const props = {
title: 'Modal Title'
},
ModalWrapperComponent = shallow(<ModalWrapper {...props} />).find('ModalTitle');
expect(ModalWrapperComponent.props().children).toEqual('Modal Title');
});
接收正确的表演道具:
it('check prop value', () => {
const props = {
show: true
},
ModalWrapperComponent = shallow(<ModalWrapper {...props} />).find('Modal');
expect(ModalWrapperComponent.props().show).toEqual(true);
});
1.4测试proptypes:
- 用于表演道具
it('check prop type', () => {
const props = {
show: true
},
ModalWrapperComponent = shallow(<ModalWrapper {...props} />).find('Modal');
expect(ModalWrapperComponent.props().show).toBeBoolean();
});
- 对于 onHide 属性
it('render correct onHide prop type', () => {
const props = {
onHide: () => {}
},
ModalWrapperComponent = shallow(<ModalWrapper {...props} />).find('Modal');
expect(ModalWrapperComponent.props().onHide).toBeFunction();
});
- 用于组件道具
it(‘render correct component prop type’, () => {
const props = {
component: () => {}
},
ModalWrapperComponent = mount(<ModalWrapper {...props} />);
expect(ModalWrapperComponent.props().component).toBeFunction();
});
完整测试列表: ModalWrapper.test.js
2.测试组件的代码清单: ModalTrigger.js
模态包装器已完成测试;第二部分是模态触发组件的测试。
组件概述:它基于toggled
指示 ModalWrapper 可见性的状态。如果toggled: false
,则弹出窗口隐藏,否则可见。函数open()在子元素上打开弹出窗口;click 事件和函数close()在 ModalWrapper 中渲染的按钮上隐藏弹出窗口。
2.1快照创建:
it('render ModalTrigger component correctly', () => {
const ModalTriggerComponent = shallow(<ModalTrigger><div /></ModalTrigger>);
expect(ModalTriggerComponent).toMatchSnapshot();
});
我们应该测试 ModalTrigger 是否需要使用组件 prop 渲染功能?不需要;因为component
它会在 ModalWrapper 组件内部渲染,所以它不依赖于被测试的组件。ModalWrapper 测试中已经涵盖了它。
2.2测试 props。我们有一个 prop children
,并且想要确保它只有一个子元素。
it('ensure to have only one child (control element)', () => {
expect(ModalTriggerComponent.findWhere(node => node.key() === 'modal-control').length).toEqual(1);
});
2.3测试 proptypes。子 prop 应该是对象,在下一个测试中检查这一点:
const ModalTriggerComponent = mount(<ModalTrigger><div /></ModalTrigger>);
it('check children prop type', () => {
expect(ModalTriggerComponent.props().children).toBeObject();
});
2.4 ModalTrigger 组件的一个重要部分是检查状态。
我们有两个状态:
弹出窗口已打开。要知道模态框是否已打开,我们需要检查其状态。为此,从组件实例调用 open 函数,并期望其toggled
状态为 true。
it('check the modal is opened', () => {
const event = {
preventDefault: () => {},
stopPropagation: () => {}
};
ModalTriggerComponent.instance().open(event);
expect(ModalTriggerComponent.state().toggled).toBeTruthy();
});
弹出窗口已关闭,反之亦然,toggled
状态应为假。
it('check the modal is closed', () => {
ModalTriggerComponent.instance().close();
expect(ModalTriggerComponent.state().toggled).toBeFalsy();
});
完整测试列表: ModalTrigger.test.js
现在,模态框已经完全测试过了。对于测试相互依赖的组件,我有一个建议:首先仔细检查所有组件并编写测试计划,明确每个组件需要测试的内容,检查每个组件的测试用例,并确保不要在两个组件中重复相同的测试用例。仔细分析可能的和最佳的测试覆盖率方案。
5. HOC 测试(高阶组件)
最后两部分(HOC 和表单字段测试)是相互关联的。我想与大家分享如何使用 HOC 测试字段布局。
解释一下什么是 BaseFieldLayout,为什么我们需要这个组件,以及我们在哪里使用它:
- BaseFieldLayout.js 是表单输入组件的包装器,例如 TextInput、CheckboxInput、DateInput、SelectInput 等。它们的名称以 结尾,
-Input
因为我们使用 redux-form 包,并且这些组件是 redux-form 逻辑的输入组件。 - 我们需要 BaseFieldLayout 来创建表单字段组件的布局,即渲染标签、工具提示、前缀(货币、平方米缩写等)、图标、错误……
- 我们在 BaseFieldHOC.js 中使用它来将 inputComponent 包装在字段布局中,并借助
<Field/>
组件将其与 redux-form 连接起来。
测试组件的代码清单: BaseFieldHOC.js
它是一个接收表单输入组件并返回组件的 HOC,与 redux-form 相连。
分析 HOC:
- 这个组件只接收一个 prop,
component
首先,我需要创建这个组件并将其包装在 BaseFieldHOC 中。 - 接下来,我需要用 redux-form 装饰包装的 HOC,以便将字段与 redux-form 连接起来。
- 在 React Redux 组件中渲染此字段,
<Provider>
以使 store 可供测试组件使用。要模拟 store,只需执行以下操作:
const store = createStore(() => ({}));
现在,每次测试之前,我需要做下一步:
let BaseFieldHOCComponent;
beforeEach(() => {
const TextInput = () => { return 'text input'; },
BaseFieldHOCWrapper = BaseFieldHOC(TextInput),
TextField = reduxForm({ form: 'testForm' })(BaseFieldHOCWrapper);
BaseFieldHOCComponent = renderer.create(
<Provider store={store}>
<TextField name="text-input" />
</Provider>
).toJSON();
});
此后,组件就可以进行测试了:
1.创建快照:
it('render correctly component', () => {
expect(BaseFieldHOCComponent).toMatchSnapshot();
});
2.确保输入组件在渲染后被包裹在BaseFieldLayout中:
it('check input component is wrapped in BaseFieldLayout', () => {
expect(BaseFieldHOCComponent.props.className).toEqual('form-group');
});
好了,HOC 部分讲完了。Redux-Form 组件测试中最复杂的部分是准备字段(用 Redux Form 装饰并设置 Store);剩下的就很简单了,只需按照说明操作即可。
完整测试清单: BaseFieldHOC.test.js
6. 表格/字段测试
Field HOC 已经覆盖了测试,我们可以转到 BaseFieldLayout 组件。
测试组件的代码清单: BaseFieldLayout.js
让我们编写 BaseFieldLayout.js 代码;按照上面的说明编写测试:
1.首先,创建快照。
如果没有 defaultProps,则不会渲染此组件:
- 输入组件
- redux-form 提供的 props:input 和 meta 对象。input 带有属性 name,meta 带有属性 error 和 touched:
const defaultProps = {
meta: {
touched: null,
error: null
},
input: {
name: 'field-name'
},
inputComponent: () => { return 'test case'; }
}
要在每个测试的包装器中使用 defaultProps,请执行以下操作:
import TestBaseFieldLayout from '../BaseFieldLayout';
const BaseFieldLayout = (props) => <TestBaseFieldLayout {...defaultProps} {...props} />;
现在我们准备创建快照:
it('render correctly BaseFieldLayout component', () => {
const BaseFieldLayoutComponent = renderer.create(<BaseFieldLayout />).toJSON();
expect(BaseFieldLayoutComponent).toMatchSnapshot();
});
2.测试道具:
这个组件有很多 props。我会展示几个例子;其余的 props 可以通过类比来测试。
- 确保
icon
道具正确呈现
it('render correctly icon prop', () => {
const props = {
icon: <span className="icon-exclamation" />
},
BaseFieldLayoutComponent = mount(<BaseFieldLayout {...props} />);
expect(BaseFieldLayoutComponent.find('span').hasClass('icon-exclamation')).toBeTruthy();
});
- 确保工具提示内容呈现在标签旁边
const props = {
labelTooltipContent: 'tooltip for label'
},
BaseFieldLayoutComponent = mount(<BaseFieldLayout {...props} />);
it('check prop is rendered', () => {
expect(BaseFieldLayoutComponent.find('span').hasClass('tooltip-icon')).toBeTruthy();
});
- 测试
fieldLink
道具- 确保 fieldLink 默认为空
it('check prop is null by default', () => {
const BaseFieldLayoutComponent = shallow(<BaseFieldLayout />);
expect(BaseFieldLayoutComponent.props().fieldLink).toBe(null);
});
- 确保 fieldLink 使用自定义值正确呈现
3.测试错误:
it('check if field has error', () => {
const props = {
meta: {
touched: true,
error: 'This field is required'
}
},
BaseFieldLayoutComponent = mount(<BaseFieldLayout {...props} />);
expect(BaseFieldLayoutComponent.find('.error')).toHaveLength(1);
});
完整测试列表: BaseFieldLayout.test.js
底线
现在,您已经掌握了如何根据项目结构对组件进行全面覆盖测试的完整指南。我根据自己的经验,尝试解释哪些测试是必须的、测试的顺序以及在测试覆盖率中可以省略哪些内容。此外,我还演示了几个测试组件的示例,并指出了代码库覆盖率的顺序。希望您觉得本文有用,并愿意分享您的反馈。感谢您的阅读。
本文由前端开发人员 Alyona Pysarenko 撰写。这篇关于React 组件测试的文章最初发表于 Django Stars 博客。您也可以访问我们的内容平台Product Tribe,该平台由专业人士为参与产品开发和增长流程的人士创建。
我们随时欢迎您提出问题并分享您想阅读的主题!
鏂囩珷鏉ユ簮锛�https://dev.to/django_stars/what-and-how-to-test-with-jest-and-enzyme-full-instruction-on-react-components-testing-56fm