如何使用 React Testing Library 来测试组件表面

2025-06-09

如何使用 React Testing Library 来测试组件表面

距离我第一次发表这篇文章已经过去好几个月了。一位细心的读者注意到,这个包已经改名了。最新的 API 和安装说明请查看React Testing Library。

在Twitter上关注我,很高兴接受您对主题或改进的建议/Chris

React 测试库是一个与众不同的测试库,它测试的是组件的表面而不是内部。你可以随意更改组件,只要它们以相同的方式渲染数据,或者在交互(例如填写数据或按下按钮)后以相同的方式渲染 React 即可。

图书馆作者 Kent C. Dodds 对此是这样评价的:

简单而完整的 React DOM 测试实用程序,鼓励良好的测试实践

它是测试 React 组件的轻量级解决方案。它基于 提供了实用函数react-dom。您的测试针对 DOM 节点进行,而不是 React 组件实例。

在本文中,我们将介绍以下内容:

  • 编写测试,展示编写测试、实例化组件并对其进行断言是多么简单
  • 处理事件,我们将学习如何触发事件并在随后对结果组件进行断言
  • 异步操作,我们将学习如何触发和等待异步操作完成
  • 管理输入,我们将学习如何将按键发送到组件上的输入元素并对结果进行断言

开始使用非常简单,您只需安装react-testing-library

yarn add @testing-library/react

编写测试

让我们看一个真实的场景来理解我们的意思。我们将创建:

  • Todos.js允许您呈现列表Todos并选择特定Todo item
  • Todos.test.js,我们的测试文件

我们的组件代码如下所示:

// Todos.js
import React from 'react';
import './Todos.css';


const Todos = ({ todos, select, selected }) => (
  <React.Fragment>
  {todos.map(todo => (
    <React.Fragment key={todo.title}>
      <h3 data-testid="item" className={ selected && selected.title === todo.title ? 'selected' :'' }>{todo.title}</h3>
      <div>{todo.description}</div>
      <button onClick={() => select(todo)}>Select</button>
    </React.Fragment>
  ))}
  </React.Fragment>
);
class TodosContainer extends React.Component {
  state = {
    todo: void 0,
  }
  select = (todo) => {
    this.setState({
      todo,
    })
  }
  render() {
    return (
      <Todos { this.props } select={this.select} selected={this.state.todo} />
    );
  }
}
export default TodosContainer;

现在进行测试:

// Todos.test.js
import {render, fireEvent, wait} from 'react-testing-library';
import 'react-testing-library/cleanup-after-each';
import React from 'react';
import 'jest-dom/extend-expect';
import Todos from '../Todos';

const todos = [
  {
    title: 'todo1'
  },
  {
    title: 'todo2'
  }];

describe('Todos', () => {
  it('finds title', () => {
    const {getByText, getByTestId, container} = render(<Todos todos={todos} />);
  })
});

从上面的代码中我们可以看到,我们正在使用一些帮助程序react-testing-library

  • render(),这将渲染我们的组件
  • fireEvent,这将帮助我们触发诸如点击事件或更改输入数据之类的事情
  • 等待,这允许我们等待元素出现

查看测试本身,我们发现当我们调用 render 时,我们得到一个对象,并且我们从中解构了 3 个值:

const {getByText, getByTestId, container} = render(<Todos todos={todos} />)

我们最终得到了以下帮助:

  • getByText,通过文本内容获取元素
  • getByTestId,它通过以下方式获取元素data-testid,因此,如果您的元素上有一个属性,那么data-testid="saved"您可以像这样查询它getByTestId('saved')
  • 容器,即你的组件被渲染到的 div

让我们填写该测试:

// Todos.test.js
import {render, fireEvent, wait} from 'react-testing-library';
import React from 'react';
import 'jest-dom/extend-expect';
import 'react-testing-library/cleanup-after-each';
import Todos from '../Todos';


const todos = [
  {
    title: 'todo1'
  },
  {
    title: 'todo2'
   }];

describe('Todos', () => {
  it('finds title', () => {
    const {getByText, getByTestId, container} = render(<Todos todos={todos} />);
    const elem = getByTestId('item');
    expect(elem.innerHTML).toBe('todo1');
  })
});

如上所示,我们能够使用容器和 querySelector 渲染组件并查询 h3 元素。最后,我们对元素内的文本进行断言。

处理操作

让我们再看一下我们的组件。或者更确切地说,让我们看一下它的摘录:

// excerpt of Todos.js
const Todos = ({ todos, select, selected }) => (
  <React.Fragment>
  {todos.map(todo => (
    <React.Fragment key={todo.title}>
      <h3 className={ selected && selected.title === todo.title ? 'selected' :'' }>{todo.title}</h3>
      <div>{todo.description}</div>
      <button onClick={() => select(todo)}>Select</button>
    </React.Fragment>
  ))}
  </React.Fragment>
);

上面我们看到,我们尝试将 CSS 类设置为“selected当待办事项被选中时”。获取todo选中状态的方法是点击它,我们可以看看当点击渲染的按钮时如何调用 select 方法(每个待办事项对应一个按钮)。让我们尝试通过添加一个测试来测试一下:

import {render, fireEvent, wait} from 'react-testing-library'
import React from 'react';
import 'jest-dom/extend-expect'
import 'react-testing-library/cleanup-after-each'
import Todos from '../Todos';
const todos = [
  {
    title: 'todo1'
  },
  {
    title: 'todo2'
  }
];


describe('Todos', () => {
  it('finds title', () => {
    const {getByText, getByTestId, container} = render(<Todos todos={todos} />);
    const elem = getByTestId('item');
    expect(elem.innerHTML).toBe('todo1');
  })


  it('select todo', () => {
    const {getByText, getByTestId, container} = render(<Todos todos={todos} />);
    fireEvent.click(getByText('Select'));
    const elem = getByTestId('item');
    expect(elem.classList[0]).toBe('selected');
  })
});

我们最后一个新添加的测试是使用fireEvent辅助函数执行 a 操作click,我们可以看到我们正在使用getByText辅助函数来查找按钮。我们再次使用容器来查找并断言所选的 CSS 类。

异步测试和输入处理

到目前为止,我们已经向您展示了如何渲染组件、查找结果元素以及对其进行断言。我们还展示了如何执行诸如点击按钮之类的操作。在本节中,我们将展示两件事:

  • 处理输入
  • 处理异步操作

我们将构建以下内容:

  • Note.js是一个允许我们输入数据并保存结果的组件,它还允许我们获取数据
  • tests /Note.js,测试文件

让我们看一下该组件:

// Note.js

import React from 'react';

class Note extends React.Component {
  state = {
    content: '',
    saved: '',
  };

  onChange = (evt) => {
    this.setState({
      content: evt.target.value,
    });
    console.log('updating content');
  }

  save = () => {
    this.setState({
      saved: `Saved: ${this.state.content}`,
    });
  }

  load = () => {
    var me = this;
    setTimeout(() => {
      me.setState({
        data: [{ title: 'test' }, { title: 'test2' }]
      })
    }, 3000);
  }

  render() {
    return (
      <React.Fragment>
        <label htmlFor="change">Change text</label>
        <input id="change" placeholder="change text" onChange={this.onChange} />
        <div data-testid="saved">{this.state.saved}</div>
        {this.state.data &&
        <div data-testid="data">
        {this.state.data.map(item => (
          <div className="item" >{item.title}</div>
        ))}
        </div>
       }
       <div>
         <button onClick={this.save}>Save</button>
         <button onClick={this.load}>Load</button>
       </div>
     </React.Fragment>
   );
  }
}

export default Note;

处理用户输入

为了在我们的示例应用程序中保存数据,我们在输入中输入文本并按保存按钮。

让我们为此创建一个测试:

// __tests__/Note.js
import {render, fireEvent, wait} from 'react-testing-library'
import React from 'react';
import 'jest-dom/extend-expect'
import 'react-testing-library/cleanup-after-each'
import Select from '../Note';


describe('Note', () => {
  it('save text', async() => {
    const {getByText, getByTestId, getByPlaceholderText, container, getByLabelText} = render(<Select />);
    const input = getByLabelText('Change text');
    input.value= 'input text';
    fireEvent.change(input);
    fireEvent.click(getByText('Save'));
    console.log('saved', getByTestId('saved').innerHTML);
    expect(getByTestId('saved')).toHaveTextContent('input text')
  })
});

上面我们可以看到,我们使用了辅助函数getByLabelText来获取输入的引用,然后我们只需执行input.value = 'input text'此操作即可。接下来,我们需要调用该方法fireEvent.change(input)来实现更改。之后,我们可以通过输入以下内容对结果进行断言:expect(getByTestId('saved')).toHaveTextContent('input text')

处理异步代码

我们的组件中还有另一项功能,即按下“加载”按钮即可调用load()方法,如下所示:

load = () => {
  var me = this;
  setTimeout(() => {
    me.setState({
      data: [{ title: 'test' }, { title: 'test2' }]
    })
  }, 3000);
}

我们可以在上面看到,更改不会立即生效,这是因为我们使用了 setTimeout()。查看我们的组件,我们可以看到,除非将 data 属性设置为某个值,否则我们不会渲染它:

{this.state.data &&
  <div data-testid="data">
  {this.state.data.map(item => (
    <div className="item" >{item.title}</div>
  ))}
  </div>
}

我们的测试需要满足这一点,等待带有属性的 divdata-testid="data"出现后才能对其进行断言。这可以通过 async/await 来处理。我们导入waitForElementfrom react-testing-library,这样我们就可以在等待元素出现时暂停执行。让我们通过在测试文件中添加一个测试来看一下效果:

import {
  render,
  fireEvent,
  wait,
  waitForElement,
} from 'react-testing-library'
import 'react-testing-library/cleanup-after-each';
import React from 'react';
import 'jest-dom/extend-expect'
import Select from '../Note';


describe('Note', () => {
  it('save text', async () => {
    const {getByText, getByTestId, getByPlaceholderText, container, getByLabelText} = render(<Select />);
    const input = getByLabelText('Change text');
    input.value= 'input text';
    fireEvent.change(input);
    fireEvent.click(getByText('Save'));
    console.log('saved', getByTestId('saved').innerHTML);
    expect(getByTestId('saved')).toHaveTextContent('input text')
  })


  it('load data', async() => {
    const {getByText, getByTestId, getByPlaceholderText, container} = render(<Select />);
    fireEvent.click(getByText('Load'));
    const elem = await waitForElement(() => getByTestId('data'))
    const elem = getByTestId('item');
    expect(elem).toHaveTextContent('test');
  })
});

await waitForElement(() => getByTestId('data'))上面我们看到了阻止测试继续执行直到元素出现的结构。它waitForElement返回一个 Promise,该 Promise 直到元素在 DOM 上存在才会解析。之后,我们对结果进行断言。

概括

我们学习了 React-testing-library,并编写了涵盖核心用例的测试。我们学习了如何处理事件、异步操作以及如何管理用户输入。我们了解了这个库的大部分功能,但更重要的是,我们学会了如何以不同的方式思考测试。

也许我们不必测试内部,而是测试组件的表面?

进一步阅读

这个库还有很多内容,鼓励你看看

继续阅读 https://dev.to/itnext/how-you-can-use-react-testing-library-to-test-component-surface-49pm
PREV
逆向工程——理解 JavaScript 中的 Promises
NEXT
如何学习 JavaScript 中的闭包并了解何时使用它们