Nightwatch 2.4 版本改进了组件测试,并显著提高了对测试 React 组件(通过 @nightwatch/react 插件)的支持。我们还发布了一个用于在 Nightwatch 中使用流行的 Testing Library 的新插件 - @nightwatch/testing-library,该插件自 Nightwatch v2.6 开始提供。

现在,我们将构建一个详细的示例,说明如何使用 Nightwatch 和 Testing Library 测试 React 组件。我们将使用 复杂示例,该示例在 React Testing Library 文档中提供,并使用 Jest 编写。

本教程将涵盖如何

  1. 使用 Vite 设置新的 React 项目,Nightwatch 在内部也使用 Vite 进行组件测试;
  2. 安装和配置 Nightwatch 和 Testing Library;
  3. 使用 @nightwatch/api-testing 插件模拟 API 请求;
  4. 使用 Nightwatch 和 Testing Library 编写复杂的 React 组件测试。

步骤 0. 创建新项目

首先,我们将使用 Vite 创建一个新项目

npm init vite@latest

出现提示时选择 ReactJavaScript。这将创建一个使用 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 渲染组件,我们只需为渲染的组件创建一个导出,可选地设置一组属性。playtest 函数用于与组件交互并验证结果。

  • 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 插件提供的集成模拟服务器。

为此,我们将使用 setupteardown 钩子,我们可以直接在测试文件中编写它们。这两个钩子都在 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 窗口,并打开开发工具。我们现在可以在开发工具中设置断点并逐步执行代码。

Debugging

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 中运行 LoginWithValidCredentialsLoginWithServerException 组件故事。

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