Nightwatch 2.4 版本改进了组件测试,并显著提高了对测试 React 组件(通过 @nightwatch/react
插件)的支持。我们还发布了一个用于在 Nightwatch 中使用流行的 Testing Library 的新插件 - @nightwatch/testing-library
,该插件自 Nightwatch v2.6 开始提供。
现在,我们将构建一个详细的示例,说明如何使用 Nightwatch 和 Testing Library 测试 React 组件。我们将使用 复杂示例,该示例在 React Testing Library 文档中提供,并使用 Jest 编写。
本教程将涵盖如何
- 使用 Vite 设置新的 React 项目,Nightwatch 在内部也使用 Vite 进行组件测试;
- 安装和配置 Nightwatch 和 Testing Library;
- 使用
@nightwatch/api-testing
插件模拟 API 请求; - 使用 Nightwatch 和 Testing Library 编写复杂的 React 组件测试。
步骤 0. 创建新项目
首先,我们将使用 Vite 创建一个新项目
npm init vite@latest
出现提示时选择 React
和 JavaScript
。这将创建一个使用 React 和 JavaScript 的新项目。
步骤 1. 安装 Nightwatch 和 Testing Library
Testing Library for React 可以使用 @testing-library/react
包进行安装
npm i @testing-library/react --save-dev
要安装 Nightwatch,请运行 init 命令
npm init nightwatch@latest
出现提示时选择 组件测试
和 React
。这将安装 nightwatch
和 @nightwatch/react
插件。选择一个浏览器以安装其驱动程序。在本例中,我们将使用 Chrome。
1.1. 安装 @nightwatch/testing-library 插件
自 v2.6 版本以来,Nightwatch 提供了自己的插件,可以直接使用 Testing Library 查询作为命令。我们将在稍后编写测试时需要它,因此现在让我们安装它
npm i @nightwatch/testing-library --save-dev
1.2 安装 @nightwatch/apitesting 插件
此示例包含一个用于测试组件的模拟服务器。我们将使用 @nightwatch/apitesting
插件提供的集成模拟服务器。使用以下命令安装它
npm i @nightwatch/apitesting --save-dev
步骤 2. 创建 Login 组件
我们将使用与 React Testing Library 文档中相同的组件。创建一个新文件 src/Login.jsx
并添加以下代码
// login.jsx
import * as React from 'react'
function Login() {
const [state, setState] = React.useReducer((s, a) => ({...s, ...a}), {
resolved: false,
loading: false,
error: null,
})
function handleSubmit(event) {
event.preventDefault()
const {usernameInput, passwordInput} = event.target.elements
setState({loading: true, resolved: false, error: null})
window
.fetch('http://localhost:3000/api/login', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({
username: usernameInput.value,
password: passwordInput.value,
}),
})
.then(r => r.json().then(data => (r.ok ? data : Promise.reject(data))))
.then(
user => {
setState({loading: false, resolved: true, error: null})
window.localStorage.setItem('token', user.token)
},
error => {
setState({loading: false, resolved: false, error: error.message})
},
)
}
return (
<div>
<form onSubmit={handleSubmit}>
<div>
<label htmlFor="usernameInput">Username</label>
<input id="usernameInput" />
</div>
<div>
<label htmlFor="passwordInput">Password</label>
<input id="passwordInput" type="password" />
</div>
<button type="submit">Submit{state.loading ? '...' : null}</button>
</form>
{state.error ? <div role="alert">{state.error}</div> : null}
{state.resolved ? (
<div role="alert">Congrats! You're signed in!</div>
) : null}
</div>
)
}
export default Login
步骤 3. 创建组件测试
Testing Library 的基本原则之一是,测试应尽可能地模仿用户与应用程序的交互方式。在使用 JSX 在 Nightwatch 中编写组件测试时,我们需要使用 组件故事格式(由 Storybook 引入的声明性格式)将测试编写为组件故事。
这使我们能够编写专注于组件使用方式的测试,而不是其实现方式,这符合 Testing Library 的理念。您可以在 Nightwatch 文档 中了解更多关于这方面的信息。
使用此格式编写测试的一大好处是,我们可以使用相同的代码为组件编写故事,这些故事可用于在 Storybook 中记录和展示它们。
3.1 使用有效凭据登录测试
创建一个新文件 src/Login.spec.jsx
并添加以下代码,它与使用 Jest 编写的 复杂示例 相同
要在 Nightwatch 中使用 JSX 渲染组件,我们只需为渲染的组件创建一个导出,可选地设置一组属性。play
和 test
函数用于与组件交互并验证结果。
play
用于与组件交互。它在浏览器上下文中执行,因此我们可以使用 Testing Library 的screen
对象来查询 DOM 并触发事件;test
用于验证结果。它在 Node.js 上下文中执行,因此我们可以使用 Nightwatch 的browser
对象来查询 DOM 并验证结果。
// login.spec.jsx
import {render, fireEvent, screen} from '@testing-library/react'
import Login from '../src/login'
export default {
title: 'Login',
component: Login
}
export const LoginWithValidCredentials = () => <Login />;
LoginWithValidCredentials.play = async ({canvasElement}) => {
//fill out the form
};
LoginWithValidCredentials.test = async (browser) => {
// verify the results
};
添加模拟服务器
此示例使用模拟服务器来模拟登录请求。我们将使用 @nightwatch/apitesting
插件提供的集成模拟服务器。
为此,我们将使用 setup
和 teardown
钩子,我们可以直接在测试文件中编写它们。这两个钩子都在 Node.js 上下文中执行。
我们还需要在 Login
组件中将登录端点设置为 http://localhost:3000/api/login
,这是模拟服务器的 URL。
完整的测试文件
完整的测试文件将如下所示
// login.spec.jsx
import {render, fireEvent, screen} from '@testing-library/react'
import Login from '../src/Login'
let server;
const token = 'fake_user_token';
let serverResponse = {
status: 200,
body: {token}
};
export default {
title: 'Login',
component: Login,
setup: async ({mockserver}) => {
server = await mockserver.create();
server.setup((app) => {
app.post('/api/login', function (req, res) {
res.status(serverResponse.status).json(serverResponse.body);
});
});
await server.start(mockServerPort);
},
teardown: async (browser) => {
await browser.execute(function() {
window.localStorage.removeItem('token')
});
await server.close();
}
}
export const LoginWithValidCredentials = () => <Login />;
LoginWithValidCredentials.play = async ({canvasElement}) => {
//fill out the form
fireEvent.change(screen.getByLabelText(/username/i), {
target: {value: 'chuck'},
});
fireEvent.change(screen.getByLabelText(/password/i), {
target: {value: 'norris'},
});
fireEvent.click(screen.getByText(/submit/i))
};
LoginWithValidCredentials.test = async (browser) => {
const alert = await browser.getByRole('alert')
await expect(alert).text.to.match(/congrats/i)
const localStorage = await browser.execute(function() {
return window.localStorage.getItem('token');
});
await expect(localStorage).to.equal(fakeUserResponse.token)
};
调试
使用 Nightwatch 进行组件测试的主要优势之一是,除了可以使用相同的 API 进行端到端测试之外,我们还可以让测试在真正的浏览器中运行,而不是在虚拟 DOM 环境(例如 JSDOM)中运行。
这使我们能够使用 Chrome 开发工具调试测试。
例如,让我们在 LoginWithValidCredentials.play
函数中添加一个 debugger
语句
LoginWithValidCredentials.play = async ({canvasElement}) => {
//fill out the form
fireEvent.change(screen.getByLabelText(/username/i), {
target: {value: 'chuck'},
});
fireEvent.change(screen.getByLabelText(/password/i), {
target: {value: 'norris'},
});
debugger;
fireEvent.click(screen.getByText(/submit/i))
};
现在,让我们使用 --debug
和 --devtools
标志运行测试
npx nightwatch test/login.spec.jsx --debug --devtools
这将打开一个新的 Chrome 窗口,并打开开发工具。我们现在可以在开发工具中设置断点并逐步执行代码。

3.2 使用服务器异常登录测试
Testing Library 文档中提供的原始 示例 还包括一个测试,用于处理服务器抛出异常的情况。
让我们尝试在 Nightwatch 中编写相同的测试。这次,我们只使用 test
函数,因为我们也可以通过这种方式与组件交互。如前所述,test
函数在 Node.js 上下文中执行,并接收 Nightwatch 的 browser
对象作为参数。
我们还需要更新模拟服务器响应,使其返回 500 状态代码和错误消息。我们可以通过在 LoginWithServerException
组件故事上编写一个 preRender
测试钩子来轻松实现这一点。
export const LoginWithServerException = () => <Login />;
LoginWithServerException.preRender = async (browser) => {
serverResponse = {
status: 500,
body: {message: 'Internal server error'}
};
};
LoginWithServerException.test = async (browser) => {
const username = await browser.getByLabelText(/username/i);
await username.sendKeys('chuck');
const password = await browser.getByLabelText(/password/i);
await password.sendKeys('norris');
const submit = await browser.getByText(/submit/i);
await submit.click();
const alert = await browser.getByRole('alert');
await expect(alert).text.to.match(/internal server error/i);
const localStorage = await browser.execute(function() {
return window.localStorage.getItem('token');
});
await expect(localStorage).to.equal(token)
};
4. 运行测试
最后,让我们运行测试。这将在 Chrome 中运行 LoginWithValidCredentials
和 LoginWithServerException
组件故事。
npx nightwatch test/login.spec.jsx
要运行测试而无需打开浏览器,我们可以传递 --headless
标志。
如果一切顺利,您应该看到以下输出
[Login] Test Suite
────────────────────────────────────
ℹ Connected to ChromeDriver on port 9515 (1134ms).
Using: chrome (108.0.5359.124) on MAC OS X.
Mock server listening on port 3000
Running <LoginWithValidCredentials> component:
──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
[browser] [vite] connecting...
[browser] [vite] connected.
✔ Expected element <LoginWithValidCredentials> to be visible (15ms)
✔ Expected element <DIV[id='app'] > DIV > DIV> text to match: "/congrats/i" (14ms)
✔ Expected 'fake_user_token' to equal('fake_user_token'):
✨ PASSED. 3 assertions. (1.495s)
Running <LoginWithServerException> component:
──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
[browser] [vite] connecting...
[browser] [vite] connected.
✔ Expected element <LoginWithServerException> to be visible (8ms)
✔ Expected element <DIV[id='app'] > DIV > DIV> text to match: "/internal server error/i" (8ms)
✔ Expected 'fake_user_token' to equal('fake_user_token'):
✨ PASSED. 3 assertions. (1.267s)
✨ PASSED. 6 total assertions (4.673s)
5. 结论
就是这样!您可以在 GitHub 存储库 中找到此示例的完整代码。欢迎提交 PR。
如果您有任何问题或反馈,请随时访问 Nightwatch Discord。