这是在 react 中测试组件系列的第二部分。如果您之前有使用 Jest 的经验,则可以跳过并使用 GitHub 代码作为起点。
在 上一篇文章中,我们介绍了测试驱动开发背后的基本原则和思想。我们还设置了在 React 中运行测试所需的环境和工具。该工具集包括 Jest、ReactTestUtils、Enzyme 和 react-test-renderer。
React 使用 Jest 在 React 中测试组件:基础知识 Manjunath M
然后,我们使用 ReactTestUtils 为演示应用程序编写了几个测试,并发现与像 Enzyme 这样更健壮的库相比,它的缺点。
在这篇文章中,我们将通过编写更实际和现实的测试来更深入地了解 React 中的测试组件。在开始之前,您可以前往 GitHub 并克隆我的存储库。
开始使用 Enzyme api
Enzyme.js 是一个由 Airbnb 维护的开源库,它是 React 开发人员的绝佳资源。它使用底层的 ReactTestUtils API,但与 ReactTestUtils 不同的是,Enzyme 提供了高级 API 和易于理解的语法。如果你还没有安装 Enzyme。
Enzyme API 导出三种类型的渲染选项:
浅渲染
全域渲染
静态渲染
浅渲染用于单独渲染特定组件。子组件不会被渲染,因此您将无法断言它们的行为。如果你打算专注于单元测试,你会喜欢的。您可以像这样浅渲染组件:
import { shallow } from 'enzyme'; import ProductHeader from './ProductHeader'; // More concrete example below. const component = shallow(<ProductHeader/>);
全 dom 渲染在名为 jsdom 的库的帮助下生成组件的虚拟 DOM。shallow()
您可以通过将方法替换mount()
为上述示例中的方法来利用此功能 。明显的好处是您也可以渲染子组件。如果你想测试一个组件及其子组件的行为,你应该使用它。
静态渲染用于将 react 组件渲染为静态 html。它是使用一个名为 Cheerio 的库来实现的,您可以在docs中阅读有关它的更多信息。
重温我们以前的测试
以下是我们在上一个教程中编写的测试:
src/components/__tests__/ProductHeader.test.js
import ReactTestUtils from 'react-dom/test-utils'; // ES6 describe('ProductHeader Component', () => { it('has an h2 tag', () => { const component = ReactTestUtils .renderIntoDocument(<ProductHeader/>); var node = ReactTestUtils .findRenderedDOMComponentWithTag( component, 'h2' ); }); it('has a title class', () => { const component = ReactTestUtils .renderIntoDocument(<ProductHeader/>); var node = ReactTestUtils .findRenderedDOMComponentWithClass( component, 'title' ); }) })
第一个测试检查ProducerHeader
组件是否有<h2>
标签,第二个测试检查它是否有一个名为 的css类title
。代码很难阅读和理解。
以下是使用 Enzyme 重写的测试。
src/components/__tests__/ProductHeader.test.js
import { shallow } from 'enzyme' describe('ProductHeader Component', () => { it('has an h2 tag', () => { const component = shallow(<ProductHeader/>); var node = component.find('h2'); expect(node.length).toEqual(1); }); it('has a title class', () => { const component = shallow(<ProductHeader/>); var node = component.find('h2'); expect(node.hasClass('title')).toBeTruthy(); }) })
首先,我使用创建了 <ProductHeader/>
组件的浅渲染 DOMshallow()
并将其存储在一个变量中。然后,我用这个.find()
方法找到了一个带有标签'h2'的节点。它查询 DOM 以查看是否存在匹配项。由于节点只有一个实例,我们可以安全地假设它node.length
等于 1。
第二个测试与第一个非常相似。该hasClass('title')
方法返回当前节点是否className
有值为'title'的道具。我们可以使用 来验证真实性 toBeTruthy()
。
使用 运行测试yarn test
,两个测试都应该通过。
做得好!现在是重构代码的时候了。从测试人员的角度来看,这很重要,因为可读的测试更容易维护。在上述测试中,前两行对于两个测试都是相同的。您可以使用beforeEach()
函数重构它们。顾名思义,该beforeEach
函数在执行描述块中的每个规范之前被调用一次。
你可以传递一个箭头函数来beforeEach()
喜欢这个。
src/components/__tests__/ProductHeader.test.js
import { shallow } from 'enzyme' describe('ProductHeader Component', () => { let component, node; // Jest beforeEach() beforeEach((()=> component = shallow(<ProductHeader/>) )) beforeEach((()=> node = component.find('h2')) ) it('has an h2 tag', () => { expect(node).toBeTruthy() }); it('has a title class', () => { expect(node.hasClass('title')).toBeTruthy() }) })
使用 Jest 和 Enzyme 编写单元测试
让我们为ProductDetails组件编写一些单元测试。它是一个展示组件,显示每个单独产品的详细信息。
我们将测试突出显示的部分
单元测试将尝试断言以下假设:
该组件存在并且道具正在传递。
显示产品名称、描述和可用性等道具。
当 props 为空时会显示错误消息。
这是测试的基本结构。第一个beforeEach()
将产品数据存储在一个变量中,第二个安装组件。
src/components/__tests__/ProductDetails.test.js
describe("ProductDetails component", () => { var component, product; beforeEach(()=> { product = { id: 1, name: 'NIKE Liteforce Blue Sneakers', description: 'Lorem ipsum.', status: 'Available' }; }) beforeEach(()=> { component = mount(<ProductDetails product={product} foo={10}/>); }) it('test #1' ,() => { }) })
第一个测试很简单:
it('should exist' ,() => { expect(component).toBeTruthy(); expect(component.props().product).toEqual(product); })
这里我们props()
使用方便获取组件道具的方法。
对于第二个测试,您可以通过元素的类名查询元素,然后检查产品名称、描述等是否属于该元素的innerText
.
it('should display product data when props are passed', ()=> { let title = component.find('.product-title'); expect(title.text()).toEqual(product.name); let description = component.find('.product-description'); expect(description.text()).toEqual(product.description); })
在这种情况下,该text()
方法对于检索元素的内部文本特别有用。尝试为 写一个期望,product.status()
看看是否所有的测试都通过了。
对于最终测试,我们将在ProductDetails
没有任何道具的情况下安装组件。然后我们将寻找一个名为“.product-error”的类并检查它是否包含文本“对不起,产品不存在”。
it('should display an error when props are not passed', ()=> { /* component without props */ component = mount(<ProductDetails />); let node = component.find('.product-error'); expect(node.text()).toEqual('Sorry. Product doesnt exist'); })
而已。我们已经成功地单独测试了该<ProductDetails />
组件。这种类型的测试称为单元测试。
使用存根和间谍测试回调
我们刚刚学会了如何测试道具。但是要真正孤立地测试一个组件,您还需要测试回调函数。在本节中,我们将为ProductList组件编写测试,并在此过程中为回调函数创建存根。这是我们需要断言的假设。
列出的产品数量应等于组件作为道具接收的对象数量。
点击
<a>
应该调用回调函数。
让我们创建一个 beforeEach()
为我们的测试填充模拟产品数据的函数。
src/components/__tests__/ProductList.test.js
beforeEach( () => { productData = [ { id: 1, name: 'NIKE Liteforce Blue Sneakers', description: 'Lorem ipsu.', status: 'Available' }, // Omitted for brevity ] })
现在,让我们将我们的组件安装在另一个beforeEach()
块中。
beforeEach(()=> { handleProductClick = jest.fn(); component = mount( <ProductList products = {productData} selectProduct={handleProductClick} /> ); })
通过ProductList
props 接收产品数据。除此之外,它还会收到来自父级的回调。尽管您可以为父级的回调函数编写测试,但如果您的目标是坚持单元测试,那么这不是一个好主意。由于回调函数属于父组件,结合父组件的逻辑会使测试变得复杂。相反,我们将创建一个存根函数。
什么是存根?
存根是一个虚拟函数,它伪装成其他函数。这允许您在不导入父组件或子组件的情况下独立测试组件。在上面的示例中,我们创建了一个handleProductClick
通过 invoking 调用的存根函数 jest.fn()
。
现在我们只需要找到<a>
DOM 中的所有元素并模拟单击第一个<a>
节点。被点击后,我们会检查是否handleProductClick()
被调用。如果是,可以公平地说我们的逻辑按预期工作。
it('should call selectProduct when clicked', () => { const firstLink = component.find('a').first(); firstLink.simulate('click'); expect(handleProductClick.mock.calls.length).toEqual(1); }) })
Enzyme 让您可以轻松地模拟用户操作,例如使用 simulate()
方法的点击。 handlerProductClick.mock.calls.length
返回调用模拟函数的次数。我们希望它等于 1。
另一个测试相对容易。您可以使用该find()
方法检索<a>
DOM 中的所有节点。节点的数量<a>
应该等于我们之前创建的 productData 数组的长度。
it('should display all product items', () => { let links = component.find('a'); expect(links.length).toEqual(productData.length); })
测试组件的状态、LifeCycleHook 和方法
接下来,我们将测试该ProductContainer
组件。它有一个状态、一个生命周期钩子和一个类方法。以下是需要验证的断言:
componentDidMount
只调用一次。组件的状态在组件安装后填充。
handleProductClick()
当产品 id 作为参数传入时,该方法应该更新状态。
为了检查是否componentDidMount
被调用,我们将对其进行监视。与存根不同,当您需要测试现有功能时使用间谍。一旦设置了 spy,您就可以编写断言来确认该函数是否被调用。
您可以按如下方式监视函数:
src/components/__tests__/ProductContainer.test.js
it('should call componentDidMount once', () => { componentDidMountSpy = spyOn(ProductContainer.prototype, 'componentDidMount'); //To be finished });
第一个参数是jest.spyOn
一个对象,它定义了我们正在监视的类的原型。第二个是我们要监视的方法的名称。
现在渲染组件并创建一个断言来检查是否调用了 spy。
component = shallow(<ProductContainer/>); expect(componentDidMountSpy).toHaveBeenCalledTimes(1);
要在组件挂载后检查组件的状态是否已填充,我们可以使用 Enzyme 的state()
方法来检索状态中的所有内容。
it('should populate the state', () => { component = shallow(<ProductContainer/>); expect(component.state().productList.length) .toEqual(4) })
第三个有点棘手。我们需要验证它handleProductClick
是否按预期工作。如果您查看代码,您会看到该handleProductClick()
方法将产品 ID 作为输入,然后this.state.selectedProduct
使用该产品的详细信息进行更新。
为了测试这一点,我们需要调用组件的方法,实际上你可以通过调用 component.instance().handleProductClick()
. 我们将传入一个示例产品 ID。在下面的示例中,我们使用第一个产品的 id。然后,我们可以测试状态是否已更新,以确认断言为真。这是整个代码:
it('should have a working method called handleProductClick', () => { let firstProduct = productData[0].id; component = shallow(<ProductContainer/>); component.instance().handleProductClick(firstProduct); expect(component.state().selectedProduct) .toEqual(productData[0]); })
我们已经编写了 10 个测试,如果一切顺利,您应该看到以下内容:
概括
呸!我们已经涵盖了开始使用 Jest 和 Enzyme 在 React 中编写测试所需了解的几乎所有内容。现在可能是前往Enzyme 网站深入了解他们的 API 的好时机。
- src/components/__tests__/ProductHeader.test.js
- src/components/__tests__/ProductHeader.test.js
- src/components/__tests__/ProductHeader.test.js
- src/components/__tests__/ProductDetails.test.js
- src/components/__tests__/ProductList.test.js
- 什么是存根?
- src/components/__tests__/ProductContainer.test.js