首页 > 文章列表 > 使用Jest和Enzyme在React中进行组件测试

使用Jest和Enzyme在React中进行组件测试

React Jest Enzyme
276 2023-09-02

这是 React 中测试组件系列的第二部分。如果您之前有使用 Jest 的经验,则可以跳过并使用 GitHub 代码作为起点。

在上一篇文章中,我们介绍了测试驱动开发背后的基本原则和思想。我们还设置了在 React 中运行测试所需的环境和工具。该工具集包括 Jest、ReactTestUtils、Enzyme 和 React-test-renderer。

然后,我们使用 ReactTestUtils 为演示应用程序编写了几个测试,并发现了与 Enzyme 等更强大的库相比的缺点。

在这篇文章中,我们将通过编写更实用和现实的测试来更深入地了解 React 中的测试组件。在开始之前,您可以前往 GitHub 并克隆我的存储库。

Enzyme API 入门

Enzyme.js 是一个由 Airbnb 维护的开源库,对于 React 开发人员来说是一个很好的资源。它底层使用了 ReactTestUtils API,但与 ReactTestUtils 不同的是,Enzyme 提供了高级 API 和易于理解的语法。如果尚未安装 Enzyme,请安装。

Enzyme API 导出三种类型的渲染选项:

  1. 浅层渲染
  2. 完整 DOM 渲染
  3. 静态渲染

浅层渲染用于单独渲染特定组件。子组件不会被渲染,因此您将无法断言它们的行为。如果您要专注于单元测试,您会喜欢这个。您可以像这样浅层渲染组件:

import { shallow }  from 'enzyme';
import ProductHeader from './ProductHeader';

// More concrete example below.
 const component = shallow(<ProductHeader/>);  

完整 DOM 渲染在名为 jsdom 的库的帮助下生成组件的虚拟 DOM。您可以通过将上例中的 shallow() 方法替换为 mount() 来利用此功能。明显的好处是您还可以渲染子组件。如果您想测试组件及其子组件的行为,您应该使用它。

静态渲染用于将 React 组件渲染为静态 HTML。它是使用名为 Cheerio 的库实现的,您可以在文档中阅读有关它的更多信息。

回顾我们之前的测试

以下是我们在上一篇教程中编写的测试:

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 组件是否具有

标记,第二个测试检查它是否具有名为 标题。该代码很难阅读和理解。

以下是使用 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();
    })
  })

首先,我使用 shallow() 创建了 组件的浅渲染 DOM,并将其存储在变量中。然后,我使用 .find() 方法查找带有标签“h2”的节点。它查询 DOM 以查看是否存在匹配。由于该节点只有一个实例,因此我们可以放心地假设 node.length 将等于 1。

第二个测试与第一个测试非常相似。 hasClass('title') 方法返回当前节点是否具有值为 'title' 的 className 属性。我们可以使用 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 组件编写一些单元测试。它是一个展示组件,显示每个单独产品的详细信息。

使用Jest和Enzyme在React中进行组件测试

单元测试将尝试断言以下假设:

  • 组件存在并且 props 正在被传递。
  • 显示产品名称、描述和可用性等属性。
  • 当 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() 方法,该方法可以方便地获取组件的 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() 的期望并查看是否所有测试都通过。

对于最终测试,我们将在不使用任何 props 的情况下安装 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');
    })

就是这样。我们已经成功地单独测试了 组件。这种类型的测试称为单元测试。

使用存根和间谍测试回调

我们刚刚学习了如何测试 props。但要真正独立地测试组件,您还需要测试回调函数。在本节中,我们将为 ProductList 组件编写测试,并在此过程中为回调函数创建存根。以下是我们需要断言的假设。

使用Jest和Enzyme在React中进行组件测试

  1. 列出的产品数量应等于组件接收的作为 props 的对象数量。
  2. 点击 应调用回调函数。

让我们创建一个 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接收商品数据。除此之外,它还会收到来自父级的回调。尽管您可以为父级回调函数编写测试,但如果您的目标是坚持单元测试,那么这不是一个好主意。由于回调函数属于父组件,合并父组件的逻辑会使测试变得复杂。相反,我们将创建一个存根函数。

什么是存根?

存根是一个虚拟函数,它伪装成其他函数。这允许您独立测试组件,而无需导入父组件或子组件。在上面的示例中,我们通过调用 jest.fn() 创建了一个名为 handleProductClick 的存根函数。

现在我们只需要找到 DOM 中的所有 元素并模拟点击第一个 节点即可。单击后,我们将检查 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() 方法检索 DOM 中的所有 节点。 节点的数量应该等于我们之前创建的productData 数组的长度。

    it('should display all product items', () => {
    
        let links = component.find('a');
        expect(links.length).toEqual(productData.length);
    })
    

测试组件的状态、LifeCycleHook 和方法

接下来,我们将测试 ProductContainer 组件。它有一个状态、一个生命周期挂钩和一个类方法。以下是需要验证的断言:

  1. componentDidMount 只被调用一次。
  2. 组件的状态在组件安装后填充。
  3. 当产品 ID 作为参数传入时,handleProductClick() 方法应更新状态。

为了检查 componentDidMount 是否被调用,我们将对其进行监视。与存根不同,当您需要测试现有函数时,会使用间谍。一旦设置了间谍,您就可以编写断言来确认该函数是否被调用。

您可以按如下方式监视函数:

src/components/__tests__/ProductContainer.test.js

   it('should call componentDidMount once', () => {
        componentDidMountSpy = spyOn(ProductContainer.prototype, 
                               'componentDidMount');
        //To be finished
    });

jest.spyOn 的第一个参数是一个对象,它定义我们正在监视的类的原型。第二个是我们想要监视的方法的名称。

现在渲染组件并创建一个断言来检查间谍是否被调用。

     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中进行组件测试

摘要

唷!我们已经涵盖了您开始使用 Jest 和 Enzyme 在 React 中编写测试所需了解的几乎所有内容。现在可能是前往 Enzyme 网站深入了解其 API 的好时机。

您对在 React 中编写测试有何看法?我很想在评论中听到他们的声音。