React Native 应用程序中的单元测试
已发表: 2022-03-10React Native 是用于构建移动应用程序的最广泛使用的框架之一。 本教程面向希望开始测试他们构建的 React Native 应用程序的开发人员。 我们将使用 Jest 测试框架和 Enzyme。
在本文中,我们将学习测试的核心原则,探索用于测试应用程序的各种库,并了解如何测试 React Native 应用程序的单元(或组件)。 通过使用 React Native 应用程序,我们将巩固我们的测试知识。
注意:在学习本教程时,JavaScript 和 React Native 的基本知识会大有裨益。
什么是单元测试?
单元测试是测试软件各个组件的测试级别。 我们这样做是为了确保每个组件都按预期工作。 组件是软件中最小的可测试部分。
为了说明,让我们创建一个Button
组件并模拟一个单元测试:
import React from 'react'; import { StyleSheet, Text, TouchableOpacity } from 'react-native'; function AppButton({ onPress }) { return ( <TouchableOpacity style={[styles.button, { backgroundColor: colors[color] }]} onPress={onPress} > <Text style={styles.text}>Register</Text> </TouchableOpacity> ); } const styles = StyleSheet.create({ button: { backgroundColor: red; borderRadius: 25, justifyContent: 'center', alignItems: 'center', }, text: { color: #fff } }) export default AppButton;
此Button
组件具有文本和onPress
功能。 让我们测试这个组件,看看单元测试是关于什么的。
首先,让我们创建一个名为Button.test.js
的测试文件:
it('renders correctly across screens', () => { const tree = renderer.create(<Button />).toJSON(); expect(tree).toMatchSnapshot(); });
在这里,我们正在测试我们的Button
组件是否在应用程序的所有屏幕上都按应有的方式呈现。 这就是单元测试的全部内容:测试应用程序的组件以确保它们按应有的方式工作。
React Native 应用程序中的单元测试
可以使用多种工具测试 React Native 应用程序,其中一些工具如下:
- 网络驱动程序
这个用于 Node.js 应用程序的开源测试工具也用于测试 React Native 应用程序。 - 恶梦
这会自动执行浏览器中的测试操作。 根据文档,“目标是公开一些模仿用户操作的简单方法(如goto
、type
和click
),并使用一个对每个脚本块感觉同步的 API,而不是深度嵌套的回调。” - 笑话
这是目前最流行的测试库之一,也是我们今天要关注的。 与 React 一样,它由 Facebook 维护,旨在提供“零配置”设置以实现最佳性能。 - 摩卡
Mocha 是一个流行的库,用于测试 React 和 React Native 应用程序。 它已成为开发人员首选的测试工具,因为它易于设置和使用以及速度有多快。 - 茉莉花
根据其文档,Jasmine 是一个用于测试 JavaScript 代码的行为驱动开发框架。
Jest 和 Enzyme 简介
根据其文档,“Jest 是一个令人愉悦的 JavaScript 测试框架,专注于简单性”。 它适用于零配置。 安装后(使用 npm 或 Yarn 等包管理器),Jest 即可使用,无需其他安装。
Enzyme 是一个用于 React Native 应用程序的 JavaScript 测试框架。 (如果您使用的是 React 而不是 React Native,则可以使用指南。)我们将使用 Enzyme 来测试我们应用程序输出的单元。 有了它,我们可以模拟应用程序的运行时。
让我们开始设置我们的项目。 我们将使用 GitHub 上的 Done With It 应用程序。 这是一个 React Native 应用市场。 首先克隆它,导航到文件夹,然后通过为 npm 运行以下命令来安装包...
npm install
……或者这个对于纱线:
yarn install
此命令将安装我们应用程序中的所有包。 完成后,我们将使用快照测试应用程序的 UI 一致性,如下所述。
快照和 Jest 配置
在本节中,我们将通过使用 Jest 测试快照来测试用户触摸和应用程序组件的 UI。
在此之前,我们需要安装 Jest 及其依赖项。 要为 Expo React Native 安装 Jest,请运行以下命令:
yarn add jest-expo --dev
这会在我们的应用程序目录中安装jest-expo
。 接下来,我们需要更新我们的package.json
文件以拥有一个测试脚本:
"scripts": { "test" "jest" }, "jest": { "preset": "jest-expo" }
通过添加这个命令,我们告诉 Jest 在我们的应用程序中注册哪个包以及在哪里注册。
接下来是向我们的应用程序添加其他包,以帮助 Jest 进行全面测试。 对于 npm,运行这个…
npm i react-test-renderer --save-dev
……对于 Yarn,这个:
yarn add react-test-renderer --dev
我们在package.json
文件中还有一些配置要做。 根据 Expo React Native 的文档,我们需要添加一个transformIgnorePattern
配置,以防止在源文件与测试匹配时在 Jest 中运行测试(即,如果进行了测试并且在项目的node modules
中找到了类似的文件)。
"jest": { "preset": "jest-expo", "transformIgnorePatterns": [ "node_modules/(?!(jest-)?react-native|react-clone-referenced-element|@react-native-community|expo(nent)?|@expo(nent)?/.*|react-navigation|@react-navigation/.*|@unimodules/.*|unimodules|sentry-expo|native-base|@sentry/.*)" ] }
现在,让我们创建一个名为App.test.js
的新文件来编写我们的第一个测试。 我们将测试我们的App
在其树中是否有一个子元素:
import React from "react"; import renderer from "react-test-renderer"; import App from "./App.js" describe("<App />", () => { it('has 1 child', () => { const tree = renderer.create(<App />).toJSON(); expect(tree.children.length).toBe(1); }); });
现在,运行yarn test
或其 npm 等效项。 如果App.js
有一个子元素,我们的测试应该通过,这将在命令行界面中得到确认。
在上面的代码中,我们导入了React
和react-test-renderer
,它们会渲染我们对Expo
的测试。 我们已经将<App />
组件树转换为 JSON,然后让 Jest 查看 JSON 中返回的子组件数量是否等于我们的预期。
更多快照测试
正如 David Adeneye 所说:
“快照测试确保 Web 应用程序的用户界面 (UI) 不会意外更改。 它在某个时刻捕获组件的代码,以便我们可以将处于一种状态的组件与它可能采用的任何其他可能状态进行比较。”
尤其是当项目涉及在许多组件中使用的全局样式时,会这样做。 让我们为App.js
写一个快照测试来测试它的 UI 一致性:
it('renders correctly across screens', () => { const tree = renderer.create( ).toJSON(); expect(tree).toMatchSnapshot(); });
it('renders correctly across screens', () => { const tree = renderer.create( ).toJSON(); expect(tree).toMatchSnapshot(); });
将其添加到我们已经编写的测试中,然后运行yarn test
(或其 npm 等效项)。 如果我们的测试通过,我们应该看到:
PASS src/App.test.js √ has 1 child (16ms) √ renders correctly (16ms) Test Suites: 1 passed, 1 total Tests: 2 passed, 2 total Snapshots: 1 total Time: 24s
这告诉我们我们的测试通过了以及他们花费的时间。 如果测试通过,您的结果将看起来相似。
让我们继续在 Jest 中模拟一些函数。
模拟 API 调用
根据 Jest 的文档:
模拟函数允许您通过擦除函数的实际实现、捕获对函数的调用(以及在这些调用中传递的参数)、在使用 `new` 实例化时捕获构造函数的实例以及允许测试来测试代码之间的链接。返回值的时间配置。
简单地说,mock 是一个对象或函数的副本,没有该函数的实际工作。 它模仿了那个功能。
Mocks 以多种方式帮助我们测试应用程序,但主要好处是它们减少了我们对依赖项的需求。
模拟通常可以通过以下两种方式之一进行。 一种是创建一个模拟函数,注入到要测试的代码中。 另一种是编写一个模拟函数来覆盖附加到组件的包或依赖项。
大多数组织和开发人员更喜欢编写模仿功能并使用假数据测试某些组件的手动模拟。
React Native 在全局对象中包含fetch
。 为了避免在我们的单元测试中进行真正的 API 调用,我们模拟了它们。 下面是一种在 React Native 中模拟所有(如果不是大多数)API 调用的方法,并且不需要依赖项:
global.fetch = jest.fn(); // mocking an API success response once fetch.mockResponseIsSuccess = (body) => { fetch.mockImplementationForOnce ( () => Promise.resolve({json: () => Promise.resolve(JSON.parse(body))}) ); }; // mocking an API failure response for once fetch.mockResponseIsFailure = (error) => { fetch.mockImplementationForOnce( () => Promise.reject(error) ); };
在这里,我们编写了一个尝试获取 API 一次的函数。 完成此操作后,它返回一个 Promise,当它被解析时,它以 JSON 格式返回正文。 它类似于失败的获取事务的模拟响应——它返回一个错误。
下面是我们应用程序的product
组件,包含一个product
对象并将信息作为props
返回。
import React from 'react'; const Product = () => { const product = { name: 'Pizza', quantity: 5, price: '$50' } return ( <> <h1>Name: {product.name}</h1> <h1>Quantity: {product.quantity}</h1> <h1>Price: {product.price}</h1> </> ); } export default Product;
假设我们正在尝试测试我们产品的所有组件。 直接访问我们的数据库不是一个可行的解决方案。 这就是模拟发挥作用的地方。 在下面的代码中,我们试图通过使用 Jest 描述组件中的对象来模拟产品的组件。
describe("", () => { it("accepts products props", () => { const wrapper = mount(<Customer product={product} />); expect(wrapper.props().product).toEqual(product); }); it("contains products quantity", () => { expect(value).toBe(3); }); });
我们使用 Jest 中的describe
来指示我们想要完成的测试。 在第一个测试中,我们正在检查我们传递的对象是否等于我们模拟的道具。
在第二个测试中,我们通过customer
道具来确保它是一个产品并且它与我们的模型相匹配。 这样做,我们不必测试我们产品的所有组件,而且我们还可以防止代码中的错误。
模拟外部 API 请求
到目前为止,我们一直在使用应用程序中的其他元素对 API 调用进行测试。 现在让我们模拟一个外部 API 调用。 我们将使用 Axios。 要测试对 API 的外部调用,我们必须模拟我们的请求并管理我们得到的响应。 我们将使用axios-mock-adapter
来模拟 Axios。 首先,我们需要通过运行以下命令来安装axios-mock-adapter
:
yarn add axios-mock-adapter
接下来要做的是创建我们的模拟:
import MockAdapter from 'axios-mock-adapter'; import Faker from 'faker' import ApiClient from '../constants/api-client'; import userDetails from 'jest/mockResponseObjects/user-objects'; let mockApi = new MockAdapter(ApiClient.getAxiosInstance()); let validAuthentication = { name: Faker.internet.email(), password: Faker.internet.password() mockApi.onPost('requests').reply(config) => { if (config.data === validAuthentication) { return [200, userDetails]; } return [400, 'Incorrect username and password']; });
在这里,我们调用ApiClient
并将 Axios 实例传递给它以模拟用户的凭据。 我们正在使用一个名为 faker.js 的包来生成虚假的用户数据,例如电子邮件地址和密码。
模拟的行为与我们期望的 API 一样。 如果请求成功,我们将收到一个状态码为 200 的响应,表示 OK。 我们会得到一个 400 的状态码,表示对服务器的错误请求,这将与 JSON 一起发送,并带有“用户名和密码不正确”的消息。
现在我们的 mock 已经准备好了,让我们为外部 API 请求编写一个测试。 和以前一样,我们将使用快照。
it('successful sign in with correct credentials', async () => { await store.dispatch(authenticateUser('[email protected]', 'password')); expect(getActions()).toMatchSnapshot(); }); it('unsuccessful sign in with wrong credentials', async () => { await store.dispatch(authenticateUser('[email protected]', 'wrong credential')) .catch((error) => { expect(errorObject).toMatchSnapshot(); });
在这里,我们使用本机 JavaScript async await
来保存我们的输入,以测试是否使用正确的凭据成功登录。 同时,来自 Jest 的authenticateUser
函数对请求进行身份验证,并确保它与我们之前的快照相匹配。 接下来,我们会测试登录不成功的凭据(例如电子邮件地址或密码)是否存在错误,并发送错误作为响应。
现在,运行yarn test
或npm test
。 我相信你所有的测试都会通过。
让我们看看如何在状态管理库 Redux 中测试组件。
使用快照测试 Redux Actions 和 Reducers
不可否认,Redux 是 React 应用程序中使用最广泛的状态管理器之一。 Redux 中的大部分功能都涉及dispatch
,它是 Redux 存储的一个功能,用于触发应用程序状态的更改。 测试 Redux 可能会很棘手,因为 Redux 的actions
在规模和复杂性上迅速增长。 使用 Jest 快照,这变得更容易。 大多数使用 Redux 的测试归结为两件事:
- 为了测试
actions
,我们创建了redux-mock-store
并分发动作。 - 为了测试 reducer,我们导入
reducer
并向其传递状态和操作对象。
下面是一个带有快照的 Redux 测试。 我们将通过在SIGN-IN
验证用户并查看user
reducer 如何处理LOGOUT
操作来测试调度的操作。
import mockStore from 'redux-mock-store'; import { LOGOUT } from '../actions/logout'; import User from '../reducers/user'; import { testUser } from 'jest/mock-objects'; describe('Testing the sign in authentication', () => { const store = mockStore(); it('user attempts with correct password and succeeds', async () => { await store.dispatch(authenticateUser('[email protected]', 'password')); expect(store.getActions()).toMatchSnapshot(); }); }); describe('Testing reducers after user LOGS OUT', () => { it('user is returned back to initial app state', () => { expect(user(testUser, { type: LOGOUT })).toMatchSnapshot(); }); });
在第一个测试中,我们描述了登录身份验证并创建了一个模拟商店。 我们首先从 Redux 导入一个mockStore
,然后从 Jest 导入一个名为testUser
的方法来帮助我们模拟用户。 接下来,我们测试用户何时使用与快照存储中的电子邮件地址和密码匹配的电子邮件地址和密码成功登录应用程序。 因此,快照确保每次运行测试时用户输入的对象都匹配。
在第二个测试中,我们正在测试用户何时注销。 一旦我们的 reducer 快照确认用户已注销,它就会返回到应用程序的初始状态。
接下来,我们通过运行yarn test
。 如果测试通过,我们应该看到以下结果:
PASS src/redux/actions.test.js √ user attempts with correct password and succeeds (23ms) √ user is returned back to initial app state (19ms) Test Suites: 1 passed, 1 total Tests: 2 passed, 2 total Snapshots: 2 total Time: 31s
结论
使用 Jest,测试 React Native 应用程序从未如此简单,尤其是使用快照,它确保 UI 保持一致,无论全局样式如何。 此外,Jest 允许我们在应用程序中模拟某些 API 调用和模块。 我们可以通过测试 React Native 应用程序的组件来更进一步。
更多资源
- “使用 Jest 测试 React Native 应用程序的实用指南”,David Adeneye,Smashing Magazine
- 笑话文档
- “使用 Jest 进行测试”,Expo React Native 文档
- “学习用 Jest 测试 React Native”,Jason Gaare