本文概述
集成测试是测试成本和测试价值之间的最佳结合点。代替组件单元测试或除组件单元测试之外, 借助react-testing-library为React应用编写集成测试可以提高代码的可维护性, 而不会影响开发速度。
如果你想在开始之前先入手, 请在此处查看如何使用react-testing-library进行React应用集成测试的示例。
为什么要投资集成测试?
“集成测试在信心和速度/费用之间取得了很好的平衡。这就是为什么建议在此花费大部分(而不是全部, 请注意)的原因。” –写测试中的Kent C. Dodds。不是很多。主要是整合。
编写React组件的单元测试是一种惯例, 通常使用流行的库来测试React”酶”。特别是其”浅”方法。这种方法使我们能够与应用程序的其余部分隔离地测试组件。但是, 由于编写React应用程序只涉及组成组件, 因此仅单元测试并不能确保该应用程序没有错误。
例如, 更改组件的接受道具并更新其关联的单元测试可能会导致所有测试通过, 而如果未相应更新另一个组件, 则该应用程序仍可能会损坏。
集成测试可在更改React应用程序时帮助你省心, 因为它们可确保组件的组合产生所需的UX。
React App集成测试的要求
这是React开发人员在编写集成测试时想要做的一些事情:
- 从用户的角度测试应用程序用例。用户访问网页上的信息并与可用控件进行交互。
- 模拟API调用不依赖于API的可用性和通过/失败测试的状态。
- 模拟浏览器API(例如本地存储), 因为它们在测试环境中根本不存在。
- 声明React DOM状态(浏览器DOM或本机移动环境)。
现在, 对于某些事情, 我们在编写React应用程序集成测试时应尽量避免:
- 测试实施细节。如果实施更改确实引入了错误, 则仅应打破测试。
- 模拟太多。我们想测试应用程序的所有部分如何协同工作。
- 浅呈现。我们要测试应用程序中所有组件的组成, 直到最小的组件。
为什么选择反应测试库?
前述要求使React-Test-Library是一个不错的选择, 因为它的主要指导原则是允许以类似于实际人员的方式来测试React组件。
该库及其可选的伴随库使我们能够编写与DOM交互并声明其状态的测试。
示例应用设置
我们将为其编写示例集成测试的应用程序实现了一个简单的场景:
- 用户输入GitHub用户名。
- 该应用程序显示与输入的用户名关联的公共存储库列表。
从集成测试的角度来看, 如何实现上述功能应该是无关紧要的。但是, 为了与实际应用程序保持紧密联系, 该应用程序遵循常见的React模式, 因此该应用程序:
- 是单页应用程序(SPA)。
- 发出API请求。
- 具有全球状态管理。
- 支持国际化。
- 使用React组件库。
可在此处找到该应用实现的源代码。
编写集成测试
安装依赖
含纱线:
yarn add --dev jest @testing-library/react @testing-library/user-event jest-dom nock
或使用npm:
npm i -D jest @testing-library/react @testing-library/user-event jest-dom nock
创建集成测试套件文件
我们将在应用程序的./test文件夹中创建一个名为viewGitHubRepositoriesByUsername.spec.js的文件。笑话会自动将其捡起。
在测试文件中导入依赖项
import React from 'react'; // so that we can use JSX syntax
import {
render, cleanup, waitForElement
} from '@testing-library/react'; // testing helpers
import userEvent from '@testing-library/user-event' // testing helpers for imitating user events
import 'jest-dom/extend-expect'; // to extend Jest's expect with DOM assertions
import nock from 'nock'; // to mock github API
import {
FAKE_USERNAME_WITH_REPOS, FAKE_USERNAME_WITHOUT_REPOS, FAKE_BAD_USERNAME, REPOS_LIST
} from './fixtures/github'; // test data to use in a mock API
import './helpers/initTestLocalization'; // to configure i18n for tests
import App from '../App'; // the app that we are going to test
设置测试套件
describe('view GitHub repositories by username', () => {
beforeAll(() => {
nock('https://api.github.com')
.persist()
.get(`/users/${FAKE_USERNAME_WITH_REPOS}/repos`)
.query(true)
.reply(200, REPOS_LIST);
});
afterEach(cleanup);
describe('when GitHub user has public repositories', () => {
it('user can view the list of public repositories for entered GitHub username', async () => {
// arrange
// act
// assert
});
});
describe('when GitHub user has no public repositories', () => {
it('user is presented with a message that there are no public repositories for entered GitHub username', async () => {
// arrange
// act
// assert
});
});
describe('when GitHub user does not exist', () => {
it('user is presented with an error message', async () => {
// arrange
// act
// assert
});
});
});
笔记:
- 在进行所有测试之前, 请模拟GitHub API以在使用特定用户名调用时返回存储库列表。
- 每次测试后, 请清洁测试React DOM, 以便每个测试均从干净的地方开始。
- 描述块指定了集成测试用例和流程变化。
- 我们正在测试的流量变化为:
- 用户输入具有关联的公共GitHub存储库的有效用户名。
- 用户输入没有关联的公共GitHub存储库的有效用户名。
- 用户输入的用户名在GitHub上不存在。
- 它阻止使用异步回调, 因为他们正在测试的用例中包含异步步骤。
编写第一个流程测试
首先, 需要渲染应用程序。
const { getByText, getByPlaceholderText, queryByText } = render(<App />);
从@ testing-library / react模块导入的render方法在测试React DOM中渲染应用程序, 并返回绑定到渲染应用程序容器的DOM查询。这些查询用于定位DOM元素以进行交互并声明。
现在, 作为测试流程的第一步, 向用户显示一个用户名字段, 并在其中输入用户名字符串。
userEvent.type(getByPlaceholderText('userSelection.usernamePlaceholder'), FAKE_USERNAME_WITH_REPOS);
导入的@ testing-library / user-event模块中的userEvent帮助器具有类型方法, 该方法可模仿用户在文本字段中键入文本时的行为。它接受两个参数:接受输入的DOM元素和用户键入的字符串。
用户通常通过与它们关联的文本找到DOM元素。在输入的情况下, 它可以是标签文本或占位符文本。较早地从render返回的getByPlaceholderText查询方法使我们可以通过占位符文本查找DOM元素。
请注意, 由于文本本身通常可能会更改, 因此最好不要依赖于实际的本地化值, 而应配置本地化模块以将本地化项关键字作为其值来返回。
例如, 当” zh-cn”本地化通常返回Enter GitHub username作为userSelection.usernamePlaceholder键的值时, 在测试中, 我们希望它返回userSelection.usernamePlaceholder。
当用户在字段中键入文本时, 他们应该看到文本字段值已更新。
expect(getByPlaceholderText('userSelection.usernamePlaceholder')).toHaveAttribute('value', FAKE_USERNAME_WITH_REPOS);
在流程的下一步, 用户单击”提交”按钮, 并希望看到存储库列表。
userEvent.click(getByText('userSelection.submitButtonText').closest('button'));
getByText('repositories.header');
userEvent.click方法模仿用户单击DOM元素, 而getByText查询通过其包含的文本查找DOM元素。最接近的修饰符可确保我们选择正确种类的元素。
注意:在集成测试中, 步骤通常同时充当行动和断言角色。例如, 我们断言用户可以通过单击按钮来单击它。
在上一步中, 我们断言用户看到了应用程序的存储库列表部分。现在, 我们需要断言, 由于从GitHub获取存储库列表可能需要一些时间, 因此用户会看到指示正在进行获取的指示。我们还想确保该应用程序不会告诉用户在仍在获取存储库列表的同时, 没有与输入的用户名相关联的存储库。
getByText('repositories.loadingText');
expect(queryByText('repositories.empty')).toBeNull();
请注意, getBy查询前缀用于断言可以找到DOM元素, 而queryBy查询前缀对于相反的断言很有用。另外, 如果找不到任何元素, queryBy也不会返回错误。
接下来, 我们要确保该应用最终完成了对存储库的提取并将其显示给用户。
await waitForElement(() => REPOS_LIST.reduce((elementsToWaitFor, repository) => {
elementsToWaitFor.push(getByText(repository.name));
elementsToWaitFor.push(getByText(repository.description));
return elementsToWaitFor;
}, []));
waitForElement异步方法用于等待DOM更新, 该更新将使作为方法参数提供的断言呈现为true。在这种情况下, 我们断言该应用程序会显示模拟的GitHub API返回的每个存储库的名称和描述。
最后, 该应用程序不应再显示指示正在获取存储库的指示符, 并且它不应显示错误消息。
expect(queryByText('repositories.loadingText')).toBeNull();
expect(queryByText('repositories.error')).toBeNull();
我们产生的React集成测试如下所示:
it('user can view the list of public repositories for entered GitHub username', async () => {
const { getByText, getByPlaceholderText, queryByText } = render(<App />);
userEvent.type(getByPlaceholderText('userSelection.usernamePlaceholder'), FAKE_USERNAME_WITH_REPOS); expect(getByPlaceholderText('userSelection.usernamePlaceholder')).toHaveAttribute('value', FAKE_USERNAME_WITH_REPOS);
userEvent.click(getByText('userSelection.submitButtonText').closest('button'));
getByText('repositories.header');
getByText('repositories.loadingText');
expect(queryByText('repositories.empty')).toBeNull();
await waitForElement(() => REPOS_LIST.reduce((elementsToWaitFor, repository) => {
elementsToWaitFor.push(getByText(repository.name));
elementsToWaitFor.push(getByText(repository.description));
return elementsToWaitFor;
}, []));
expect(queryByText('repositories.loadingText')).toBeNull();
expect(queryByText('repositories.error')).toBeNull();
});
替代流量测试
当用户输入没有关联公共存储库的GitHub用户名时, 该应用会显示一条适当的消息。
describe('when GitHub user has no public repositories', () => {
it('user is presented with a message that there are no public repositories for entered GitHub username', async () => {
const { getByText, getByPlaceholderText, queryByText } = render(<App />);
userEvent.type(getByPlaceholderText('userSelection.usernamePlaceholder'), FAKE_USERNAME_WITHOUT_REPOS); expect(getByPlaceholderText('userSelection.usernamePlaceholder')).toHaveAttribute('value', FAKE_USERNAME_WITHOUT_REPOS);
userEvent.click(getByText('userSelection.submitButtonText').closest('button'));
getByText('repositories.header');
getByText('repositories.loadingText');
expect(queryByText('repositories.empty')).toBeNull();
await waitForElement(() => getByText('repositories.empty'));
expect(queryByText('repositories.error')).toBeNull();
});
});
当用户输入不存在的GitHub用户名时, 应用程序将显示错误消息。
describe('when GitHub user does not exist', () => {
it('user is presented with an error message', async () => {
const { getByText, getByPlaceholderText, queryByText } = render(<App />);
userEvent.type(getByPlaceholderText('userSelection.usernamePlaceholder'), FAKE_BAD_USERNAME); expect(getByPlaceholderText('userSelection.usernamePlaceholder')).toHaveAttribute('value', FAKE_BAD_USERNAME);
userEvent.click(getByText('userSelection.submitButtonText').closest('button'));
getByText('repositories.header');
getByText('repositories.loadingText');
expect(queryByText('repositories.empty')).toBeNull();
await waitForElement(() => getByText('repositories.error'));
expect(queryByText('repositories.empty')).toBeNull();
});
});
为什么React集成测试会失败
集成测试确实为React应用程序提供了一个最佳选择。这些测试有助于捕获错误并使用TDD方法, 同时, 当实现更改时, 它们不需要维护。
本文展示的React-testing-library是编写React集成测试的好工具, 因为它使你可以在用户进行操作时与应用进行交互, 并从用户的角度验证应用的状态和行为。
希望这里提供的示例将帮助你开始在新的和现有的React项目上编写集成测试。包含应用程序实现的完整示例代码可在我的GitHub上找到。