如何使用 react-testing-library 测试 react-select

Posted

技术标签:

【中文标题】如何使用 react-testing-library 测试 react-select【英文标题】:how to test react-select with react-testing-library 【发布时间】:2019-08-29 17:53:27 【问题描述】:

App.js

import React,  Component  from "react";
import Select from "react-select";

const SELECT_OPTIONS = ["FOO", "BAR"].map(e => 
  return  value: e, label: e ;
);

class App extends Component 
  state = 
    selected: SELECT_OPTIONS[0].value
  ;

  handleSelectChange = e => 
    this.setState( selected: e.value );
  ;

  render() 
    const  selected  = this.state;
    const value =  value: selected, label: selected ;
    return (
      <div className="App">
        <div data-testid="select">
          <Select
            multi=false
            value=value
            options=SELECT_OPTIONS
            onChange=this.handleSelectChange
          />
        </div>
        <p data-testid="select-output">selected</p>
      </div>
    );
  


export default App;

App.test.js

import React from "react";
import 
  render,
  fireEvent,
  cleanup,
  waitForElement,
  getByText
 from "react-testing-library";
import App from "./App";

afterEach(cleanup);

const setup = () => 
  const utils = render(<App />);
  const selectOutput = utils.getByTestId("select-output");
  const selectInput = document.getElementById("react-select-2-input");
  return  selectOutput, selectInput ;
;

test("it can change selected item", async () => 
  const  selectOutput, selectInput  = setup();
  getByText(selectOutput, "FOO");
  fireEvent.change(selectInput,  target:  value: "BAR"  );
  await waitForElement(() => getByText(selectOutput, "BAR"));
);

这个最小示例在浏览器中按预期工作,但测试失败。我认为未调用 onChange 处理程序。如何在测试中触发 onChange 回调?查找要触发事件的元素的首选方法是什么?谢谢

【问题讨论】:

【参考方案1】:

这一定是关于 RTL 被问得最多的问题:D

最好的策略是使用jest.mock(或测试框架中的等效项)来模拟选择并呈现 html 选择。

关于为什么这是最好的方法的更多信息,我写了一些适用于这种情况的东西。 OP询问了Material-UI中的选择,但想法是一样的。

Original question 和我的回答:

因为您无法控制该 UI。它在第 3 方模块中定义。

所以,你有两个选择:

您可以找出材质库创建的 HTML,然后使用 container.querySelector 查找其元素并与之交互。这需要一段时间,但应该是可能的。完成所有这些后,您必须希望在每个新版本中它们不会过多地更改 DOM 结构,否则您可能必须更新所有测试。

另一种选择是相信 Material-UI 将制作一个可以工作并且您的用户可以使用的组件。基于这种信任,您可以简单地将测试中的该组件替换为更简单的组件。

是的,选项一测试用户看到的内容,但选项二更容易维护。

根据我的经验,第二个选项很好,但当然,您的用例可能会有所不同,您可能需要测试实际组件。

这是一个模拟选择的示例:

jest.mock("react-select", () => ( options, value, onChange ) => 
  function handleChange(event) 
    const option = options.find(
      option => option.value === event.currentTarget.value
    );
    onChange(option);
  
  return (
    <select data-testid="select" value=value onChange=handleChange>
      options.map(( label, value ) => (
        <option key=value value=value>
          label
        </option>
      ))
    </select>
  );
);

您可以阅读更多here。

【讨论】:

@GiorgioPolvara-Gpx 虽然我得到了你建议的方法,但我很想知道这是否真的违反了测试库的指导原则。该库鼓励测试最终用户实际与之交互的内容(所以对我来说更多的是集成/功能测试而不是单元测试)。在您的方法中,您正在模拟外部依赖项(这对单元测试很有用),但是如果依赖项得到更新,那么就会对失败的软件进行成功的测试。您对此有何看法? @GiorgioPolvara-Gpx 我读了你的博客,我使用的是react-select/async,所以我使用了jest.mock("react-select/async",...,但我得到一个无法通过以下方式找到元素:[data-testid= "select"] 在尝试fireEvent.change(getByTestId("select"), target: value: "foo" ); 时,我有一个render(&lt;MySearchEngine /&gt;),这就像getByTestId 正在调查它而不是jest.mock 块。我错过了什么?谢谢 如果他们将组件模拟到这种程度,人们应该对他们的组件的测试完全没有信心。我强烈建议不要采用这种方法。在这种情况下,您正在测试一个完全不同的组件。 我很难从这里帮助你。在此处或官方 Spectrum 页面上打开一个新问题 @GiorgioPolvara-Gpx 我不同意你应该模拟第三方库。如果该库更改/中断,我想知道它(不必阅读更改日志/发行说明),测试是如何发生的。【参考方案2】:

在我的项目中,我使用的是 react-testing-library 和 jest-dom。 我遇到了同样的问题 - 经过一番调查,我找到了解决方案,基于线程:https://github.com/airbnb/enzyme/issues/400

请注意,渲染的***函数必须是异步的,以及各个步骤。

这种情况下不需要使用焦点事件,它允许选择多个值。

另外,getSelectItem 内部必须有异步回调。

const DOWN_ARROW =  keyCode: 40 ;

it('renders and values can be filled then submitted', async () => 
  const 
    asFragment,
    getByLabelText,
    getByText,
   = render(<MyComponent />);

  ( ... )

  // the function
  const getSelectItem = (getByLabelText, getByText) => async (selectLabel, itemText) => 
    fireEvent.keyDown(getByLabelText(selectLabel), DOWN_ARROW);
    await waitForElement(() => getByText(itemText));
    fireEvent.click(getByText(itemText));
  

  // usage
  const selectItem = getSelectItem(getByLabelText, getByText);

  await selectItem('Label', 'Option');

  ( ... )


【讨论】:

我个人更喜欢这个解决方案,而不是公认的答案,因为你保持原样。这样,您就可以真正像用户测试它们一样测试事物。如果你模拟react-select,你甚至需要测试你自己的模拟,这会适得其反。如果你使用更复杂的属性,react-select 提供你的模拟也会变得更复杂,也很难维护恕我直言 这个答案效果很好,不需要模拟。谢谢! 您是否已将其与 ant 4 一起使用?我有一个类似的解决方案,效果很好,但升级后找不到选项.. 虽然我不认为其他解决方案本质上是错误的,但我也更喜欢这种解决方案,因为它更接近现实世界的场景。感谢您分享此内容,这帮助我和我的同事解决了我们在模拟选择中遇到了一段时间但没有成功的问题。 只是出色的解决方案。顺便说一句,现在不推荐使用 waitforElement() 。我这样做了:await screen.findByText(itemText);【参考方案3】:

与@momimomo 的回答类似,我写了一个小助手来从 TypeScript 中的react-select 中选择一个选项。

帮助文件:

import  getByText, findByText, fireEvent  from '@testing-library/react';

const keyDownEvent = 
    key: 'ArrowDown',
;

export async function selectOption(container: HTMLElement, optionText: string) 
    const placeholder = getByText(container, 'Select...');
    fireEvent.keyDown(placeholder, keyDownEvent);
    await findByText(container, optionText);
    fireEvent.click(getByText(container, optionText));

用法:

export const MyComponent: React.FunctionComponent = () => 
    return (
        <div data-testid="day-selector">
            <Select ...reactSelectOptions />
        </div>
    );
;
it('can select an option', async () => 
    const  getByTestId  = render(<MyComponent />);
    // Open the react-select options then click on "Monday".
    await selectOption(getByTestId('day-selector'), 'Monday');
);

【讨论】:

我最喜欢这个答案,不需要安装额外的包总是一个加分项【参考方案4】:
export async function selectOption(container: HTMLElement, optionText: string) 
  let listControl: any = '';
  await waitForElement(
    () => (listControl = container.querySelector('.Select-control')),
  );
  fireEvent.mouseDown(listControl);
  await wait();
  const option = getByText(container, optionText);
  fireEvent.mouseDown(option);
  await wait();

注意: container:选择框的容器(例如:container = getByTestId('seclectTestId'))

【讨论】:

await wait() 是从哪里来的? wait() 仅来自反应测试库。如果我们在 act() 中结合 fireEvent 会更好。 fireEvent 不需要包裹在 act() 中【参考方案5】:

最后,有一个库可以帮助我们:https://testing-library.com/docs/ecosystem-react-select-event。完美适用于单选或多选:

来自@testing-library/react docs:

import React from 'react'
import Select from 'react-select'
import  render  from '@testing-library/react'
import selectEvent from 'react-select-event'

const  getByTestId, getByLabelText  = render(
  <form data-testid="form">
    <label htmlFor="food">Food</label>
    <Select options=OPTIONS name="food" inputId="food" isMulti />
  </form>
)
expect(getByTestId('form')).toHaveFormValues( food: '' ) // empty select

// select two values...
await selectEvent.select(getByLabelText('Food'), ['Strawberry', 'Mango'])
expect(getByTestId('form')).toHaveFormValues( food: ['strawberry', 'mango'] )

// ...and add a third one
await selectEvent.select(getByLabelText('Food'), 'Chocolate')
expect(getByTestId('form')).toHaveFormValues(
  food: ['strawberry', 'mango', 'chocolate'],
)

感谢https://github.com/romgain/react-select-event 提供这么棒的软件包!

【讨论】:

使用 Formik 和 chakra-ui 嵌入 react-select 就像一个魅力,前夕 好东西 react-select-event,我一直在努力正确测试 react-select 如果您想要开箱即用的东西,react-select 是一个很棒的包。不幸的是,可访问性和测试是痛苦的。还有,给项目带来了情感,不轻,一年前切换到downshift,永不回头。它需要一些设置,但结果更轻、更容易测试并且开箱即用。 @Constantin 我没有使用情感,只是普通的 CSS 模块来定制它【参考方案6】:

这个解决方案对我有用。

fireEvent.change(getByTestId("select-test-id"),  target:  value: "1"  );

希望对奋斗者有所帮助。

【讨论】:

react-select 不会将任何data-testid 传递给它的任何子元素,而且您不能自己提供它。您的解决方案适用于常规的 select HTML 元素,但恐怕它不适用于 react-select lib。 @StanleySathler 正确,这不适用于 react-select,而仅适用于 HTML select【参考方案7】:

另一种解决方案适用于我的用例,不需要在 react-testing-library spectrum chat. 上找到 react-select 模拟或单独的库(感谢 @Steve Vaughan)

这样做的缺点是我们必须使用container.querySelector,RTL 建议不要使用它来支持其更具弹性的选择器。

【讨论】:

【参考方案8】:

如果您没有使用 label 元素,则使用 react-select-event 的方法是:

const select = screen.container.querySelector(
  "input[name='select']"
);

selectEvent.select(select, "Value");

【讨论】:

【参考方案9】:

如果出于某种原因有一个同名的标签使用这个

const [firstLabel, secondLabel] = getAllByLabelText('State');
    await act(async () => 
      fireEvent.focus(firstLabel);
      fireEvent.keyDown(firstLabel, 
        key: 'ArrowDown',
        keyCode: 40,
        code: 40,
      );

      await waitFor(() => 
        fireEvent.click(getByText('Alabama'));
      );

      fireEvent.focus(secondLabel);
      fireEvent.keyDown(secondLabel, 
        key: 'ArrowDown',
        keyCode: 40,
        code: 40,
      );

      await waitFor(() => 
        fireEvent.click(getByText('Alaska'));
      );
    );

或者如果你有办法查询你的部分——例如使用 data-testid——你可以使用 inside:

within(getByTestId('id-for-section-A')).getByLabelText('Days')
within(getByTestId('id-for-section-B')).getByLabelText('Days')

【讨论】:

以上是关于如何使用 react-testing-library 测试 react-select的主要内容,如果未能解决你的问题,请参考以下文章

如何使用本机反应创建登录以及如何验证会话

如何在自动布局中使用约束标识符以及如何使用标识符更改约束? [迅速]

如何使用 AngularJS 的 ng-model 创建一个数组以及如何使用 jquery 提交?

如何使用laravel保存所有行数据每个行名或相等

如何使用 Math.Net 连接矩阵。如何使用 Math.Net 调用特定的行或列?

WSARecv 如何使用 lpOverlapped?如何手动发出事件信号?