周东明 發表於 2021-1-12 07:54:00

React:React Testing Library(RTL)教程

<p>&gt; 原文链接:https://www.robinwieruch.de/react-testing-library</p>
<blockquote>
<p>第一次翻译文章,可能会难以阅读,建议英文过关的都阅读原文</p>
</blockquote>
<p>Kent C. Dodds的 React Testing Library (RTL)是Airbnb的 Enzyme 的替代品。Enzyme 为测试 React 组件提供了很多实用的工具,而 React Testing Library(简称<strong>RTL</strong>)则是后退一步并提出疑问:『怎么样的测试可以让我们对开发的 React 组件充满信心?』,相较于测试组件的内部实现细节,RTL把开发者当成一位 React 程序的终端用户</p>
<p>在这篇<strong>React Testing Library教程</strong>,我们将会学习怎么对 React 组件进行有信心的单元测试及集成测试。</p>
<h2 id="jest-和-react-testing-library">Jest 和 React Testing Library</h2>
<p>React 初学者可能会对React体系的测试工具感到迷惑。<strong>React Testing Library和Jest不是非此即彼</strong>,而是相互依赖并且都有自己的专属功能。</p>
<p>在现代的React中,<strong>Jest</strong>是最热门的JavaScript程序的<strong>测试框架</strong>,我们不可避免要去接触。如果是通过 create-react-app 来创建项目,则 Jest 及 React Testing Library 已经默认安装了,在<code>package.json</code>可以看到test script,我们可以通过npm test来运行测试。在此之前,我们先看下面的测试代码:</p>
<pre><code class="language-javascript">describe('my function or component', () =&gt; {
test('does the following', () =&gt; {

});
});
</code></pre>
<p>describe块是<strong>测试套件(test suite)</strong>,test块是<strong>测试用例(test case)</strong>,其中<code>test</code>关键字也可以写成<code>it</code>。</p>
<p>一个测试套件可以包含多个测试用例,但是一个测试用例不能包含测试套件。</p>
<p>写在测试用例内部的是<strong>断言(assertions)</strong>(例如:Jest的<code>expect</code>),断言结果可以是成功,可以是失败,下面是两个断言成功的例子:</p>
<pre><code class="language-javascript">describe('true is truthy and false is falsy', () =&gt; {
test('true is truthy', () =&gt; {
    expect(true).toBe(true);
});

test('false is falsy', () =&gt; {
    expect(false).toBe(false);
});
});
</code></pre>
<p>当你把上面的代码复制到一个<code>test.js</code>文件中,并且运行<code>npm test</code>命令,Jest 会自动找到上述代码并执行。当我们执行<code>npm test</code>时,Jest测试运行器默认会自动匹配所有<code>test.js</code>结尾的文件,你可以通过Jest配置文件来配置匹配规则和其他功能。</p>
<p>当你通过Jest测试运行器执行<code>npm test</code>后,你会看到以下输出:</p>
<p><img src="https://img2020.cnblogs.com/blog/1605282/202101/1605282-20210113143745537-755596817.png" alt="image-20210112110603334" loading="lazy"></p>
<p>在运行所有测试后,你能看到测试用例变为绿色,Jest提供了交互式命令,让我们可以进一步下达命令。一般而言,Jest会一次性显示所有的测试结果(对于你的测试用例)。如果你修改了文件(不管源代码还是测试代码),Jest都会重新运行所有的测试用例。</p>
<pre><code class="language-javascript">function sum(x, y) {
return x + y;
}

describe('sum', () =&gt; {
test('sums up two values', () =&gt; {
    expect(sum(2, 4)).toBe(6);
});
});
</code></pre>
<p>在实际开发中,被测试代码一般与测试代码在不同的文件,所以需要通过 import 去测试:</p>
<pre><code class="language-javascript">import sum from './math.js';

describe('sum', () =&gt; {
test('sums up two values', () =&gt; {
    expect(sum(2, 4)).toBe(6);
});
});
</code></pre>
<p>简而言之,这就是Jest,与任何 React 组件无关。Jest 就是一个通过命令行来提供运行测试的能力的测试运行器。虽然它还提供如『测试套件、测试用例、断言』等函数,以及其他更加强大的功能,但是本质上上述就是我们需要Jest的原因。</p>
<p>与 Jest 相比,React Testing Library 是一个测试 React 组件的测试库。另一个热门的测试库是之前提到的 Enzyme。下面我们会学习使用 React Testing Library 来测试 React 组件。</p>
<h2 id="rtl渲染组件">RTL:渲染组件</h2>
<p>在这个章节,你会学习到怎么通过 RTL 渲染 React 组件。我们会使用 str/App.js 文件下的 App function component:</p>
<pre><code class="language-javascript">import React from 'react';

const title = 'Hello React';

function App() {
return &lt;div&gt;{title}&lt;/div&gt;;
}
export default App;
</code></pre>
<p>在 src/App.test.js 文件添加测试代码:</p>
<pre><code class="language-javascript">import React from 'react';
import { render } from '@testing-library/react';

import App from './App';

describe('App', () =&gt; {
test('renders App component', () =&gt; {
    render(&lt;App /&gt;);
});
});
</code></pre>
<p>RTL的 render 函数通过 JSX 去渲染内容,然后,你就能在测试代码中访问你的组件,通过 RTL 的 debug 函数,可以确保看到渲染的内容:</p>
<pre><code class="language-javascript">import React from 'react';
import { render, screen } from '@testing-library/react';

import App from './App';

describe('App', () =&gt; {
test('renders App component', () =&gt; {
    render(&lt;App /&gt;);

    screen.debug();
});
});
</code></pre>
<p>运行 <code>npm test </code>后,在控制台能够看到 APP 组件的 HTML 输出。当你通过 RTL 编写测试代码时,都可以先通过 <code>debug</code> 函数查看组件在 RTL 中的渲染结果。这样可以更高效的编写代码。</p>
<pre><code class="language-html">&lt;body&gt;
&lt;div&gt;
    &lt;div&gt;
      Hello React
    &lt;/div&gt;
&lt;/div&gt;
&lt;/body&gt;
</code></pre>
<p><strong>RTL的厉害之处在于,它不关心实际的组件代码</strong>。我们接下来看一下利用了不同特性(useState、event handler,props)和概念(controlled component)的 React 组件:</p>
<pre><code class="language-javascript">import React from 'react';

function App() {
const = React.useState('');

function handleChange(event) {
    setSearch(event.target.value);
}

return (
    &lt;div&gt;
      &lt;Search value={search} onChange={handleChange}&gt;
      Search:
      &lt;/Search&gt;

      &lt;p&gt;Searches for {search ? search : '...'}&lt;/p&gt;
    &lt;/div&gt;
);
}

function Search({ value, onChange, children }) {
return (
    &lt;div&gt;
      &lt;label htmlFor="search"&gt;{children}&lt;/label&gt;
      &lt;input
      id="search"
      type="text"
      value={value}
      onChange={onChange}
      /&gt;
    &lt;/div&gt;
);
}

export default App;
</code></pre>
<p>当你执行<code>npm test</code>后,你能看到 debug 函数有以下输出:</p>
<pre><code class="language-html">&lt;body&gt;
&lt;div&gt;
    &lt;div&gt;
      &lt;div&gt;
      &lt;label
          for="search"
      &gt;
          Search:
      &lt;/label&gt;
      &lt;input
          id="search"
          type="text"
          value=""
      /&gt;
      &lt;/div&gt;
      &lt;p&gt;
      Searches for
      ...
      &lt;/p&gt;
    &lt;/div&gt;
&lt;/div&gt;
&lt;/body&gt;
</code></pre>
<p>RTL 能够让你的 React 组件与呈现给人看的时候类似,看到的是React 组件渲染成 HTML,所以你会看到 HTML 结构的输出,而不是两个独立的 React 组件。</p>
<h2 id="rtl定位元素">RTL:定位元素</h2>
<p>在渲染了 React 组件后,RTL 提供了不同的函数去定位元素。定位后的元素可用于『断言』或者是『用户交互』。现在我们先来学习,怎么去定位元素:</p>
<pre><code class="language-javascript">import React from 'react';
import { render, screen } from '@testing-library/react';

import App from './App';

describe('App', () =&gt; {
test('renders App component', () =&gt; {
    render(&lt;App /&gt;);

    screen.getByText('Search:');
});
});
</code></pre>
<p>当你不清楚 RTL 的 render 函数会渲染出什么时,请保持使用 RTL 的 <code>debug</code> 函数。当你知道渲染的 HTML 的结构后,你才能够通过 RTL 的 <code>screen</code> 对象的函数进行定位。定位后的元素可用于用户交互或断言。我们会检查元素是否在 DOM 的断言:</p>
<pre><code class="language-javascript">import React from 'react';
import { render, screen } from '@testing-library/react';

import App from './App';

describe('App', () =&gt; {
test('renders App component', () =&gt; {
    render(&lt;App /&gt;);

    expect(screen.getByText('Search:')).toBeInTheDocument();
});
});
</code></pre>
<p>当找不到元素,<code>getByText</code> 函数会抛出一个异常。少数人利用这个特性去使用诸如<code>getByText</code>的定位函数作为隐式的断言,通过该函数替代通过 <code>expect</code> 进行显式的断言:</p>
<pre><code class="language-javascript">import React from 'react';
import { render, screen } from '@testing-library/react';

import App from './App';

describe('App', () =&gt; {
test('renders App component', () =&gt; {
    render(&lt;App /&gt;);

    // implicit assertion:隐式断言
    screen.getByText('Search:');

    // explicit assertion:显式断言
    // 更推荐该方法
    expect(screen.getByText('Search:')).toBeInTheDocument();
});
});
</code></pre>
<p><code>getByText</code> 函数接收一个 string 作为参数,例如我们上面的调用。它也可以接收一个regular expression(正则表达式)作为参数。通过 string 作为参数用于精准匹配,通过 regular expression 可用于部分匹配(模糊匹配),更加便利:</p>
<pre><code class="language-javascript">import React from 'react';
import { render, screen } from '@testing-library/react';

import App from './App';

describe('App', () =&gt; {
test('renders App component', () =&gt; {
    render(&lt;App /&gt;);

    // fails
    expect(screen.getByText('Search')).toBeInTheDocument();

    // succeeds
    expect(screen.getByText('Search:')).toBeInTheDocument();

    // succeeds
    expect(screen.getByText(/Search/)).toBeInTheDocument();
});
});
</code></pre>
<p><code>getByText</code>是 RTL 众多定位函数类型之一,下面我们看下其他的</p>
<h2 id="rtl-定位类型">RTL: 定位类型</h2>
<p>你已经学习了 <code>getByText</code> ,其中 <code>Text</code> 是 RTL 中常用语定位元素的一个定位类型,另一个是 <code>getByRole</code> 的 <code>Role</code>。</p>
<p><code>getByRole</code>方法常用于通过 aria-label属性。但是,HTML 元素可能也会有隐式 role,例如 button元素的button role。因此你不仅可以通过『存在的 text』 来定位元素,也可以通过『可得到的 role』来定位元素。<code>getByRole</code> 有一个巧妙的特性:如果你提供的 role 不存在,它会显示所有的可选择的 role。</p>
<p><code>getByText</code> 和 <code>getByRole</code> 是 RTL 中应用最为广泛的定位函数。</p>
<p><code>getByRole</code> 巧妙的特性:如果你提供的 role 不存在于渲染后的HTML,它会显示所有的可选择的 role。</p>
<pre><code class="language-javascript">import React from 'react';
import { render, screen } from '@testing-library/react';

import App from './App';

describe('App', () =&gt; {
test('renders App component', () =&gt; {
    render(&lt;App /&gt;);

    screen.getByRole('');
});
});
</code></pre>
<p>运行<code>npm test</code>命令后,它会有以下输出:</p>
<pre><code class="language-bash">Unable to find an accessible element with the role ""

Here are the accessible roles:

document:

Name "":
&lt;body /&gt;

--------------------------------------------------
textbox:

Name "Search:":
&lt;input
id="search"
type="text"
value=""
/&gt;

--------------------------------------------------
</code></pre>
<p>由于 HTML 元素的隐式 roles,我们拥有至少一个 textbox(在这是&lt;input /&gt;),我们可以通过它使用 <code>getByRole</code> 进行定位</p>
<pre><code class="language-javascript">import React from 'react';
import { render, screen } from '@testing-library/react';

import App from './App';

describe('App', () =&gt; {
test('renders App component', () =&gt; {
    render(&lt;App /&gt;);

    expect(screen.getByRole('textbox')).toBeInTheDocument();
});
});
</code></pre>
<p>因为 DOM 已经给 HTML 附加了隐式 roles,所以一般情况下我们不需要为了测试而在 HTML 元素中显式指定 aria roles 。这就是 <code>getByRole</code> 成为 <code>getByText</code> 在 RTL 的定位函数中的有力竞争者。</p>
<p>还有其他更特定元素的查询类型:</p>
<ul>
<li><strong>LabelText:</strong> getByLabelText: &lt;label for="search" /&gt;</li>
<li><strong>PlaceholderText:</strong> getByPlaceholderText:&lt;input placeholder="Search" /&gt;</li>
<li><strong>AltText:</strong> getByAltText:&lt;img alt="profile" /&gt;</li>
<li><strong>DisplayValue:</strong> getByDisplayValue:&lt;input value="JavaScript" /&gt;</li>
</ul>
<p>还有一种不得已的情况下使用的定位类型, <code>TestId</code>的 <code>getByTestId</code> 函数需要在源代码中添加 <code>data-testid</code> 属性才能使用。毕竟一般而言,<code>getByText</code> 和 <code>getByRole</code> 应该是你定位元素的首选定位类型。</p>
<ul>
<li>getByText</li>
<li>getByRole</li>
<li>getByLabelText</li>
<li>getByPlaceholderText</li>
<li>getByAltText</li>
<li>getByDisplayValue</li>
</ul>
<p>最后强调一遍,以上这些都是 RTL 的不同的<strong>定位类型</strong>。</p>
<h2 id="rtl定位的变异种类variants">RTL:定位的变异种类(VARIANTS)</h2>
<p>与定位类型相比,也存在定位的变异种类(简称变种)。其中一个定位变种是 RTL 中 <code>getByText</code> 和 <code>getByRole</code> 使用的 <code>getBy</code> 变种,这也是测试 React 组件时,默认使用的定位变种。</p>
<p>另外两个定位变种是 <code>queryBy</code> 和 <code>findBy</code>,它们都可以通过 <code>getBy</code> 有的定位类型进行扩展。例如,<code>queryBy</code> 拥有以下的定位类型:</p>
<ul>
<li>queryByText</li>
<li>queryByRole</li>
<li>queryByLabelText</li>
<li>queryByPlaceholderText</li>
<li>queryByAltText</li>
<li>queryByDisplayValue</li>
</ul>
<p>而 <code>findBy</code> 则拥有以下定位类型:</p>
<ul>
<li>findByText</li>
<li>findByRole</li>
<li>findByLabelText</li>
<li>findByPlaceholderText</li>
<li>findByAltText</li>
<li>findByDisplayValue</li>
</ul>
<h3 id="getby-和-queryby-有什么不同"><code>getBy</code> 和 <code>queryBy</code> 有什么不同?</h3>
<p>现在面临一个问题:什么时候使用 <code>getBy</code>,什么时候使用其他两个变种 <code>queryBy</code> 和 <code>findBy</code>。你已经知道 <code>getBy</code> 在无法定位元素时,会抛出一个异常。这是一个便利的副作用,因为这可以让开发者更早地注意到测试代码中发生了某些错误。但是,这也会导致在断言时一些不应该发生的异常:</p>
<pre><code class="language-javascript">import React from 'react';
import { render, screen } from '@testing-library/react';

import App from './App';

describe('App', () =&gt; {
test('renders App component', () =&gt; {
    render(&lt;App /&gt;);

    screen.debug();

    // fails
    expect(screen.getByText(/Searches for JavaScript/)).toBeNull();
});
});
</code></pre>
<p>例子中的断言失败了,通过 <code>debug</code> 函数的输出我们得知:因为 <code>getBy</code> 在当前HTML中找不到文本 "Searches for JavaScript" ,所以 <code>getBy</code> 在我们进行断言之前抛出了一个异常。为了验证某个元素不在页面中,我们改用 <code>queryBy</code> 来替代 <code>getBy</code>:</p>
<pre><code class="language-javascript">import React from 'react';
import { render, screen } from '@testing-library/react';

import App from './App';

describe('App', () =&gt; {
test('renders App component', () =&gt; {
    render(&lt;App /&gt;);

    expect(screen.queryByText(/Searches for JavaScript/)).toBeNull();
});
});
</code></pre>
<p>所以,当你想要验证一个元素不在页面中,使用 <code>queryBy</code> ,否则使用 <code>getBy</code>。那么什么时候使用 <code>findBy</code> 呢?</p>
<h3 id="什么时候使用-findby">什么时候使用 <code>findBy</code></h3>
<p><code>findBy</code> 变体用于那些最终会显示在页面当中的异步元素,我们创建一个新的 React 组件来说明该场景:</p>
<pre><code>function getUser() {
return Promise.resolve({ id: '1', name: 'Robin' });
}

function App() {
const = React.useState('');
const = React.useState(null);

React.useEffect(() =&gt; {
    const loadUser = async () =&gt; {
      const user = await getUser();
      setUser(user);
    };

    loadUser();
}, []);

function handleChange(event) {
    setSearch(event.target.value);
}

return (
    &lt;div&gt;
      {user ? &lt;p&gt;Signed in as {user.name}&lt;/p&gt; : null}

      &lt;Search value={search} onChange={handleChange}&gt;
      Search:
      &lt;/Search&gt;

      &lt;p&gt;Searches for {search ? search : '...'}&lt;/p&gt;
    &lt;/div&gt;
);
}
</code></pre>
<p>该组件首次渲染后,App 组件会 fetches 一个用于通过模拟的 API,该 API 返回一个立马 resolves 为 user 对象的JavaScript promise 对象,并且组件以 React 组件 State 的方式存储来自 promise 的 user。在组件更新并重新渲染之后,由于条件渲染的原因,会在组件中渲染出文本:"Signed in as"。</p>
<p>如果我们要测试组件从第一次渲染到第二次渲染的过程中,promise 被 resolved,我们需要写一个异步的测试因为我们必须等待 promise 对象被异步 resolve。换句话说,我们需要等待 user 对象在组件更新一次后重新渲染:</p>
<pre><code class="language-javascript">import React from 'react';
import { render, screen } from '@testing-library/react';

import App from './App';

describe('App', () =&gt; {
test('renders App component', async () =&gt; {
    render(&lt;App /&gt;);

    expect(screen.queryByText(/Signed in as/)).toBeNull();

    expect(await screen.findByText(/Signed in as/)).toBeInTheDocument();
});
});
</code></pre>
<p>在组件的初始化渲染中,我们在 HTML 中无法通过 <code>queryBy</code> 找到 "Signed in as"(这里使用 <code>queryBy</code> 代替了 <code>getBy</code>), 然后,我们 await 一个新的元素被找到,并且最终确实被找到当 promise resolves 并且组件重新渲染之后。</p>
<p>如果你不相信这个结果,可以添加两个 <code>debug</code> 函数并在命令行验证它们的输出:</p>
<pre><code class="language-javascript">import React from 'react';
import { render, screen } from '@testing-library/react';

import App from './App';

describe('App', () =&gt; {
test('renders App component', async () =&gt; {
    render(&lt;App /&gt;);

    expect(screen.queryByText(/Signed in as/)).toBeNull();

    screen.debug();

    expect(await screen.findByText(/Signed in as/)).toBeInTheDocument();

    screen.debug();
});
});
</code></pre>
<p>对于任何开始不显示、但迟早会显示的元素,使用 <code>findBy</code> 取代 <code>getBy</code> 及 <code>queryBy</code>。如果你想要验证一个元素不在页面中,使用 <code>queryBy</code>,否则默认使用 <code>getBy</code>。</p>
<h3 id="如果是多个元素怎么办">如果是多个元素怎么办</h3>
<p>我们已经学习了三个定位变种:<code>getBy</code>,<code>queryBy</code>,<code>findBy</code>。这些都能与定位类型关联(例如:Text,Role,PlaceholderText,DisplayValue)。但这些定位函数只返回一个变量,那怎么验证返回多个变量的情况呢(例如:React 组件中的 list)?</p>
<p>所有的定位变种都可以通过 <code>All</code>关键字扩展:</p>
<ul>
<li>getAllBy</li>
<li>queryAllBy</li>
<li>findAllBy</li>
</ul>
<p>它们都会返回一个元素的数组并且可以再次通过定位类型进行定位。(原文是这么说的,但实际使用中似乎不能)</p>
<h3 id="断言函数">断言函数</h3>
<p>断言函数出现在断言过程的右边。在上一个例子中,你已经使用了两个断言函数:<code>toBeNull</code> 和 <code>toBeInTheDocument</code>。它们都是 RTL 中主要被用于检查元素是否显示在页面中的函数。</p>
<p>通常所有的断言函数都来自 Jest 。但是, RTL 通过自己的实现扩展了已有的 API ,例如 <code>toBeInTheDocument</code> 函数。所有这些来自一个额外包 的扩展的函数都已经在你使用 <code>create-react-app</code> 创建项目时自动设置。</p>
<ul>
<li>toBeDisabled</li>
<li>toBeEnabled</li>
<li>toBeEmpty</li>
<li>toBeEmptyDOMElement</li>
<li>toBeInTheDocument</li>
<li>toBeInvalid</li>
<li>toBeRequired</li>
<li>toBeValid</li>
<li>toBeVisible</li>
<li>toContainElement</li>
<li>toContainHTML</li>
<li>toHaveAttribute</li>
<li>toHaveClass</li>
<li>toHaveFocus</li>
<li>toHaveFormValues</li>
<li>toHaveStyle</li>
<li>toHaveTextContent</li>
<li>toHaveValue</li>
<li>toHaveDisplayValue</li>
<li>toBeChecked</li>
<li>toBePartiallyChecked</li>
<li>toHaveDescription</li>
</ul>
<h2 id="rtlfire-event">RTL:Fire Event</h2>
<p>目前为止,我们只学习了通过 <code>getBy</code>(或 <code>queryBy</code>)测试 React 组件的元素渲染,以及拥有条件渲染元素的 React 组件的再渲染。但是实际的用户交互是怎么样的呢?当用户想 <code>input</code> 输入文字,组件可能会再渲染(例如我们的例子),或者一个新的值会被显示(或者被使用在任何地方)。</p>
<p>我们可以通过 RTL 的 <code>fireEvent</code> 函数去模拟终端用户的交互。下面我们学习一下:</p>
<pre><code class="language-javascript">import React from 'react';
import { render, screen, fireEvent } from '@testing-library/react';

import App from './App';

describe('App', () =&gt; {
test('renders App component', () =&gt; {
    render(&lt;App /&gt;);

    screen.debug();

    fireEvent.change(screen.getByRole('textbox'), {
      target: { value: 'JavaScript' },
    });

    screen.debug();
});
});
</code></pre>
<p><code>fireEvent</code> 函数需要两个参数,一个参数是定位的元素(例子中使用 textbox role 定位 <code>input</code>),另一个参数是<code>event</code>(例子中的 <code>event</code> 拥有属性 <code>value</code> ,<code>value</code> 的值为 "JavaScript")。<code>debug</code> 方法可以显示 <code>fireEvent</code> 执行前后显示的 HTML 结构的差别,然后你就能够看到 <code>input</code> 的字段值被重新渲染了。</p>
<p>另外,如果你的组件拥有一个异步的任务,例如我们的 APP 组件需要 fetches 一个 user,你可能会看到一条警告:<em>"Warning: An update to App inside a test was not wrapped in act(...).(翻译:警告:测试代码中,一个更新 APP 的操作没有被 act 函数包含)"</em>;这个警告的意思是,当存在一些异步任务执行时,我们需要去处理它。一般而言这个能够通过 RTL 的 <code>act</code> 函数解决,但在这个例子中,我们只需要等待 user 被 resolve:</p>
<pre><code class="language-javascript">describe('App', () =&gt; {
test('renders App component', async () =&gt; {
    render(&lt;App /&gt;);

    // wait for the user to resolve
    // needs only be used in our special case
    await screen.findByText(/Signed in as/);

    screen.debug();

    fireEvent.change(screen.getByRole('textbox'), {
      target: { value: 'JavaScript' },
    });

    screen.debug();
});
});
</code></pre>
<p>然后,我们可以在 <code>fireEvent</code> 发生前后进行断言:</p>
<pre><code class="language-javascript">describe('App', () =&gt; {
test('renders App component', async () =&gt; {
    render(&lt;App /&gt;);

    // wait for the user to resolve
    // needs only be used in our special case
    await screen.findByText(/Signed in as/);

    expect(screen.queryByText(/Searches for JavaScript/)).toBeNull();

    fireEvent.change(screen.getByRole('textbox'), {
      target: { value: 'JavaScript' },
    });

    expect(screen.getByText(/Searches for JavaScript/)).toBeInTheDocument();
});
});
</code></pre>
<p>我们在 <code>fireEvent</code> 执行前,通过 <code>queryBy</code> 定位变种去检查元素不显示,在 <code>fireEvent</code> 执行后,通过 <code>getBy</code> 定位变种去检查元素显示。 有时候你也会看到一些文章在最后一次断言会使用 <code>queryBy</code> ,因为 <code>queryBy</code> 与 <code>getBy</code> 在断言元素存在于页面上时,用法比较类似。</p>
<p>当然,<code>fireEvent</code> 除了可以解决测试中异步的行为,还可以直接使用并进行断言</p>
<h2 id="rtl用户事件user-event">RTL:用户事件(User Event)</h2>
<p>RTL 还拥有一个扩展用户行为的库,该库通过 fireEvent API 进行扩展。上面我们已经通过 <code>fireEvent</code> 去触发用户交互;下面我们会使用 <code>userEvent</code> 去替代它,因为 <code>userEvent</code> 的 API 相比于 <code>fireEvent</code> 的API 更真实地模仿了浏览器行为。例如: <code>fireEvent.change()</code> 函数触发了浏览器的 <code>change</code> 事件,但是 <code>userEvent.type</code> 触发了浏览器的 <code>chagnge</code>,<code>keyDown</code>,<code>keyPress</code>,<code>keyUp</code>事件。</p>
<pre><code class="language-javascript">import React from 'react';
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';

import App from './App';

describe('App', () =&gt; {
test('renders App component', async () =&gt; {
    render(&lt;App /&gt;);

    // wait for the user to resolve
    await screen.findByText(/Signed in as/);

    expect(screen.queryByText(/Searches for JavaScript/)).toBeNull();

    await userEvent.type(screen.getByRole('textbox'), 'JavaScript');

    expect(
      screen.getByText(/Searches for JavaScript/)
    ).toBeInTheDocument();
});
});
</code></pre>
<p>任何情况下,优先使用 <code>userEvent</code> 而不是 <code>fireEvent</code>。虽然写该文章的时候,<code>userEvent</code> 还不完全包含 <code>fireEvent</code> 的所有特性,但是,以后的事谁知道呢。</p>
<h2 id="rtl处理回调">RTL:处理回调</h2>
<p>有时候你会对 React 组件单独进行单元测试。这些组件一般不会拥有副作用及状态,只接收 <code>props</code> 和返回 <code>JSX</code> 或处理<code>callback handlers</code>。我们已经知道怎么测试接收 <code>props</code>和 <code>component</code> 的JSX 渲染了。下面我们会对 Search 组件的 callback hanlers 测试:</p>
<pre><code class="language-javascript">function Search({ value, onChange, children }) {
return (
    &lt;div&gt;
      &lt;label htmlFor="search"&gt;{children}&lt;/label&gt;
      &lt;input
      id="search"
      type="text"
      value={value}
      onChange={onChange}
      /&gt;
    &lt;/div&gt;
);
}
</code></pre>
<p>渲染及断言我们之前已经看过了。这次我们看一下通过 Jest 的工具去 <code>mock</code> 一个 <code>onChange</code> 函数并传递给组件。 然后通过 <code>input</code> 输入框触发用户交互,我们就可以去验证 <code>onChange</code> 方法被调用:</p>
<pre><code class="language-javascript">describe('Search', () =&gt; {
test('calls the onChange callback handler', () =&gt; {
   const onChange = jest.fn();

   render(
   &lt;Search value="" onChange={onChange}&gt;
       Search:
   &lt;/Search&gt;
   );

   fireEvent.change(screen.getByRole('textbox'), {
   target: { value: 'JavaScript' },
   });
        // 注意,这里断言触发回调了 1 次
   expect(onChange).toHaveBeenCalledTimes(1);
});
});
</code></pre>
<p>再一次说明,<code>userEvent</code> 相比于 <code>fireEvent</code> 更贴近用户在浏览器的表现,当 <code>fireEvent</code> 触发 <code>change</code> 事件只调用了一次回调函数,但 <code>userEvent</code> 对每个字母的输入都会触发回调:</p>
<pre><code class="language-javascript">describe('Search', () =&gt; {
test('calls the onChange callback handler', async () =&gt; {
    const onChange = jest.fn();

    render(
      &lt;Search value="" onChange={onChange}&gt;
      Search:
      &lt;/Search&gt;
    );

    await userEvent.type(screen.getByRole('textbox'), 'JavaScript');
        // 这里断言触发回调了 10 次
    expect(onChange).toHaveBeenCalledTimes(10);
});
});
</code></pre>
<p>但是,RTL 不建议你过多地孤立地测试你的组件(傻瓜组件),而是应该更多的与其他组件进行集成测试。只有这样你才能更好的测试出 <code>state</code> 改变对 DOM 的影响,以及是否有副作用发生。</p>
<h2 id="rtlasynchronous--async">RTL:Asynchronous / Async</h2>
<p>在前面的例子中,我们看到例子通过 <code>async</code> <code>await</code> 及 <code>findBy</code> 去等待定位一些开始不在,但最后一定会显示出来的元素来进行测试。现在我们通过一个小示例来测试 React 中 fetching 数据。以下是通过 axios 远程 fetching 数据的 React 组件:</p>
<pre><code class="language-javascript">import React from 'react';
import axios from 'axios';

const URL = 'http://hn.algolia.com/api/v1/search';

function App() {
const = React.useState([]);
const = React.useState(null);

async function handleFetch(event) {
    let result;

    try {
      result = await axios.get(`${URL}?query=React`);

      setStories(result.data.hits);
    } catch (error) {
      setError(error);
    }
}

return (
    &lt;div&gt;
      &lt;button type="button" onClick={handleFetch}&gt;
      Fetch Stories
      &lt;/button&gt;

      {error &amp;&amp; &lt;span&gt;Something went wrong ...&lt;/span&gt;}

      &lt;ul&gt;
      {stories.map((story) =&gt; (
          &lt;li key={story.objectID}&gt;
            &lt;a href={story.url}&gt;{story.title}&lt;/a&gt;
          &lt;/li&gt;
      ))}
      &lt;/ul&gt;
    &lt;/div&gt;
);
}

export default App;
</code></pre>
<p>点击按钮,我们从 Hacker News API 获取了 stories 数组。当顺利获取数据,React 会在页面渲染出一个 stories 列表,否则,我们将会看到一个异常提示。App 组件的测试会是以下这样的:</p>
<pre><code>import React from 'react';
import axios from 'axios';
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';

import App from './App';

jest.mock('axios');

describe('App', () =&gt; {
test('fetches stories from an API and displays them', async () =&gt; {
    const stories = [
      { objectID: '1', title: 'Hello' },
      { objectID: '2', title: 'React' },
    ];

    axios.get.mockImplementationOnce(() =&gt;
      Promise.resolve({ data: { hits: stories } })
    );

    render(&lt;App /&gt;);

    await userEvent.click(screen.getByRole('button'));

    const items = await screen.findAllByRole('listitem');

    expect(items).toHaveLength(2);
});
});
</code></pre>
<p>在 <code>render</code> App 组件之前,我们确保对 API 进行mocked。以上例子中,axios 通过 <code>get</code> 方法返回数据,所以我们对它进行了mocked。但是如果你使用其他库或者浏览器的 fetch API 获取数据,你也需要对此进行mocked。</p>
<p>对 API mock 并渲染组件后,我们使用 <code>userEvent</code> 点击按钮去触发 API 请求。但是由于请求是异步的,我们需要等待组件进行更新。之前我们使用 RTL 的 <code>findBy</code> 定位变种去等待终将出现的元素。</p>
<pre><code class="language-javascript">import React from 'react';
import axios from 'axios';
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';

import App from './App';

jest.mock('axios');

describe('App', () =&gt; {
test('fetches stories from an API and displays them', async () =&gt; {
    ...
});

test('fetches stories from an API and fails', async () =&gt; {
    axios.get.mockImplementationOnce(() =&gt;
      Promise.reject(new Error())
    );

    render(&lt;App /&gt;);

    await userEvent.click(screen.getByRole('button'));

    const message = await screen.findByText(/Something went wrong/);

    expect(message).toBeInTheDocument();
});
});
</code></pre>
<p>最后一段测试代码展示了:怎么进行测试 React 组件的 API 异常的场景。我们通过 <code>reject</code> promise 来代替<code>resolves</code> promise 来进行 mock。并在渲染组件后,进行模拟点击,最后我们看到页面展示一个异常消息:</p>
<pre><code>import React from 'react';
import axios from 'axios';
import { render, screen, act } from '@testing-library/react';
import userEvent from '@testing-library/user-event';

import App from './App';

jest.mock('axios');

describe('App', () =&gt; {
test('fetches stories from an API and displays them', async () =&gt; {
    const stories = [
      { objectID: '1', title: 'Hello' },
      { objectID: '2', title: 'React' },
    ];

    const promise = Promise.resolve({ data: { hits: stories } });

    axios.get.mockImplementationOnce(() =&gt; promise);

    render(&lt;App /&gt;);

    await userEvent.click(screen.getByRole('button'));

    await act(() =&gt; promise);

    expect(screen.getAllByRole('listitem')).toHaveLength(2);
});

test('fetches stories from an API and fails', async () =&gt; {
    ...
});
});
</code></pre>
<p>为了完整起见,最后的例子向你展示了怎么以更明确的方式 await promise,通过 <code>act</code> 函数而不是等待 HTML 出现在页面。</p>
<p>使用 RTL 去测试 React 的异步行为不是一件困难的事情。你已经在测试代码中通过使用 Jest 去 mock 了外部模块(remote API),并且 await 数据及重新渲染了 React 组件。</p>
<hr>
<p>React Testing Library 是我用于测试 React 组件的测试库。之前我在任何时候都会使用 Airbnb 的 Enzyme,不过我很喜欢 RTL 引导你去关注用户行为而不是实现细节。你可以通过编写类似于真实用户使用般的测试代码去测试你的程序可用性。</p><br><br>
来源:https://www.cnblogs.com/testopsfeng/p/14265218.html
頁: [1]
查看完整版本: React:React Testing Library(RTL)教程