发布于 2026-01-05 2 阅读
0

2020 年如何编写 React 测试 - 第一部分

2020 年如何编写 React 测试 - 第一部分

使用 React 推荐库编写 React 测试 —  JestReact 测试库,适合完全初学者。

React 文档截图
来自https://reactjs.org/docs/test-utils.html#overview

本文旨在帮助那些刚开始学习 React 并想知道如何为 React 应用编写简单测试的人。就像大多数人开始使用 .js 开发 React 应用一样create-react-app,我也会从这里入手。

首先,让我们从默认示例开始。

默认依赖项create-react-app(2020/05/22)

"dependencies": {
    "@testing-library/jest-dom": "^4.2.4",
    "@testing-library/react": "^9.3.2",
    "@testing-library/user-event": "^7.1.2",
    "react": "^16.13.1",
    "react-dom": "^16.13.1",
    "react-scripts": "3.4.1"
  }

我们已经编写了一个测试题来帮助你入门。

// src/App.test.js
import React from 'react';
import { render } from '@testing-library/react';
import App from './App';

test('renders learn react link', () => {
  const { getByText } = render(<App />); //render is from @testing-library/react
  const linkElement = getByText(/learn react/i);
  expect(linkElement).toBeInTheDocument(); //expect assertion is from Jest
});

运行该命令后$ yarn test App,您将看到类似如下的结果:

首次测试结果

默认create-react-app设置下,无需安装或配置任何内容即可开始编写测试。

从上面的例子我们可以学到——

  • 我应该把测试文件放在哪里?如何放置? ——正如你所看到的,App.test.js文件放在App.js同一个文件夹的相邻位置,文件名在组件名称.test.js后加上了后缀。这是团队建议的默认约定(链接在此)。Appcreate-react-app

  • JestReact Testing Library是测试背后的工具链。它们默认都包含在 create-react-app 中。

// setupTests.js
// Jest is importing from a global setup file if you wonder
import '@testing-library/jest-dom/extend-expect';

其次,为 NavBar 组件编写测试。

我正在创建一个NavBar包含链接和徽标的组件。

首先,我会先编写测试,而不编写实际的组件(测试驱动开发)。

// navBar.test.js
import React from 'react'; 
// use "screen" - a newer way to utilize query in 2020 
import { render, screen } from '@testing-library/react'; 
import NavBar from './navBar'; // component to test

test('render about link', () => {
  render(<NavBar />);
  expect(screen.getByText(/about/)).toBeInTheDocument();
})

navBar.js由于我还没有在组件中编写任何代码,因此测试会首先失败。

第二次测试结果——失败

使用以下代码后navBar.js,测试应该可以通过了。

// navBar.js
import React from 'react';

const NavBar = () => (
  <div className="navbar">
    <a href="#">
      about
    </a>
  </div>
);

export default NavBar;

现在,你应该学习:

  • expect( ... ).toBeInTheDocument()断言来自 Jest。
  • render(<NavBar />);screen.getByText(/about/)来自测试库。
  • Jest 和 React Testing Library 协同工作,使在 React 中编写测试变得容易。
  • screen.getByText(/about/)使用“getByText”而不是按类名选择,是因为 React Testing Library 秉持着以用户体验而非实现细节为中心的理念。

  • 如需了解更多扩展和修改测试的内容,您可以查看以下资源:

现在让我们扩展测试和组件,使其更贴近实际应用——

// navBar.test.js
import React from 'react';
import { render, screen } from '@testing-library/react';
import NavBar from './navBar';

// include as many test cases as you want here
const links = [
  { text: 'Home', location: "/" },
  { text: 'Contact', location: "/contact" },
  { text: 'About', location: "/about" },
  { text: 'Search', location: "/search" },
];

// I use test.each to iterate the test cases above
test.each(links)(
  "Check if Nav Bar have %s link.",
  (link) => {
    render(<NavBar />);
//Ensure the text is in the dom, will throw error it can't find
    const linkDom = screen.getByText(link.text); 

//use jest assertion to verify the link property
    expect(linkDom).toHaveAttribute("href", link.location);
  }
);

test('Check if have logo and link to home page', () => {
  render(<NavBar />);
        // get by TestId define in the navBar
    const logoDom = screen.getByTestId(/company-logo/); 
        // check the link location
    expect(logoDom).toHaveAttribute("href", "/"); 
    //check the logo image
  expect(screen.getByAltText(/Company Logo/)).toBeInTheDocument(); 
});

这就是导航栏组件通常的样子(可能需要添加一些样式)。

// navBar.js
import React from 'react';

const NavBar = () => (
  <div className="navbar">
    <a href="/" data-testid="company-logo">
      <img src="/logo.png" alt="Company Logo" />
    </a>

    <ul>
      <li>
        <a href="/"> Home </a>
      </li>
      <li>
        <a href="/about"> About </a>
      </li>
      <li>
        <a href="/contact"> Contact </a>
      </li>
      <li>
        <a href="/search"> Search </a>
      </li>
    </ul>
  </div>
);

export default NavBar;

第三,编写注册表单组件测试。

写完静态内容的测试之后,让我们来写一个动态内容(例如注册表单)的测试。

首先,让我们用测试驱动开发(TDD)的方式思考——这个注册表单需要包含哪些内容(无论它看起来如何):

  • 姓名输入框,只允许输入长度为 3 到 30 个字符的字符串。
  • 一个用于输入电子邮件地址的字段,可以检查该电子邮件地址是否有效。
  • 密码输入框,可以检查密码的复杂性(至少包含 1 个数字、1 个小写字母、1 个大写字母和 1 个特殊字符)。
  • 提交按钮。
  • 以上3个输入项均为必填项,不能为空。

现在,我们来编写测试。

/*  Prepare some test cases, ensure 90% edge cases are covered.
    You can always change your test cases to fit your standard
*/
const entries = [
  { name: 'John', email: 'john_doe@yahoo', password: 'helloworld' },
  { name: 'Jo', email: 'jo.msn.com', password: 'pa$$W0rd' },
  { name: '', email: 'marry123@test.com', password: '123WX&abcd' },
  { name: 'kent'.repeat(10), email: 'kent@testing.com', password: 'w%oRD123yes' },
  { name: 'Robert', email: 'robert_bell@example.com', password: 'r&bsEc234E' },
]

接下来,构建测试的框架。

// signupForm.test.js
// this mostly a input validate test
describe('Input validate', () => {
  /* 
   I use test.each to iterate every case again
   I need use 'async' here because wait for 
   validation is await function 
  */    
  test.each(entries)('test with %s entry', async (entry) => { 
    ...

    })
})

现在,让我们在测试中构建模块。

// signupForm.test.js
...
test.each(entries)('test with %s entry', async (entry) => { 
//render the component first (it will clean up for every iteration    
    render(<SignupForm />); 

/*  grab all the input elements. 
I use 2 queries here because sometimes you can choose
how your UI look (with or without Label text) without
breaking the tests
*/   
    const nameInput = screen.queryByLabelText(/name/i)
      || screen.queryByPlaceholderText(/name/i);
    const emailInput = screen.getByLabelText(/email/i)
      || screen.queryByPlaceholderText(/email/i);
    const passwordInput = screen.getByLabelText(/password/i)
      || screen.queryByPlaceholderText(/password/i);

/* use fireEvent.change and fireEvent.blur to change name input value
and trigger the validation
*/
    fireEvent.change(nameInput, { target: { value: entry.name } }); 
    fireEvent.blur(nameInput); 

/* first if-statement to check whether the name is input.
second if-statement to check whether the name is valid.
'checkName' is a utility function you can define by yourself.
I use console.log here to show what is being checked.  
*/
if (entry.name.length === 0) {
      expect(await screen.findByText(/name is required/i)).not.toBeNull();
      console.log('name is required.');
    }
    else if (!checkName(entry.name)) {
      // if the name is invalid, error msg will showup somewhere in the form
      expect(await screen.findByText(/invalid name/i)).not.toBeNull();
      console.log(entry.name + ' is invalid name.');
    };

// With a similar structure, you can continue building the rest of the test.
        ...

/*  Remember to add this line at the end of your test to 
avoid act wrapping warning.
More detail please checkout Kent C.Dodds's post:
(He is the creator of Testing Library)
https://kentcdodds.com/blog/fix-the-not-wrapped-in-act-warning
*/
     await act(() => Promise.resolve()); 
})
...

完整的测试代码请点击此处查看。

好了,现在测试完成了(也许我们会回来做一些调整,但现在让我们继续),让我们编写组件。

// signupForm.js
import React from 'react';
/* 
I borrow the sample code from formik library with some adjustments
https://jaredpalmer.com/formik/docs/overview#the-gist
*/
import { Formik } from 'formik';
/* 
For validation check, I wrote 3 custom functions.
(I use the same functions in test)
*/
import {
  checkName,
  checkEmail,
  checkPassword,
} from '../utilities/check';

const SignupForm = () => (
  <div>
    <h1>Anywhere in your app!</h1>
    <Formik
      initialValues={{ name: '', email: '', password: '' }}
      validate={values => {
        const errors = {};
        if (!values.name) {
          errors.name = 'Name is Required'
        } else if (!checkName(values.name)) {
          errors.name = `invalid name`;
        }

        if (!values.email) {
          errors.email = 'Email is Required';
        }
        else if (!checkEmail(values.email)) {
          errors.email = 'Invalid email address';
        }

        if (!values.password) {
          errors.password = 'Password is Required';
        } else if (!checkPassword(values.password)) {
          errors.password = 'Password is too simple';
        }

        return errors;
      }}
      onSubmit={(values, { setSubmitting }) => {
        setTimeout(() => {
          alert(JSON.stringify(values, null, 2));
          setSubmitting(false);
        }, 400);
      }}
    >
      {({
        values,
        errors,
        touched,
        handleChange,
        handleBlur,
        handleSubmit,
        isSubmitting,
        /* and other goodies */
      }) => (
          <form onSubmit={handleSubmit}>
            <label>
              Name:
            <input
                type="text"
                name="name"
                placeholder="Enter your name here"
                onChange={handleChange}
                onBlur={handleBlur}
                value={values.name}
              />
            </label>

            <p style={{ 'color': 'red' }}>
              {errors.name && touched.name && errors.name}
            </p>

            <label>
              Email:
            <input
                type="email"
                name="email"
                placeholder="Your Email Address"
                onChange={handleChange}
                onBlur={handleBlur}
                value={values.email}
              />
            </label>
            <p style={{ 'color': 'red' }}>
              {errors.email && touched.email && errors.email}
            </p>

            <label>
              Password:
            <input
                type="password"
                name="password"
                placeholder="password here"
                onChange={handleChange}
                onBlur={handleBlur}
                value={values.password}
              />
            </label>

            <p style={{ 'color': 'red' }}>
              {errors.password && touched.password && errors.password}
            </p>

            <button type="submit" disabled={isSubmitting}>
              Submit
          </button>
          </form>
        )}
    </Formik>
  </div>
);

export default SignupForm;

表单看起来会和下面类似(样式不多,但足以满足我们的需求),如果输入错误,错误信息会显示在输入框下方:

表单截图及验证

如果你完成了上面的测试,现在所有测试都应该通过了。yarn test --verbose在命令行中运行,你应该会看到类似这样的输出。使用详细模式和 console.log 信息,你可以看到每个测试用例是如何被测试的,以及哪个用例是正确的,哪个用例是错误的。

最终形式测试结果

如需更多测试代码示例和不同案例,请查看我的仓库(链接在此)

最终图像

结语。

对于初学者来说,一下子掌握所有内容是很难的,所以如果觉得压力太大,就放慢速度。我花了至少一周的时间才学会基础知识,而这仅仅是编写 React 应用测试的开始。

这是一个很难掌握的话题,但如果你想成为一名专业的前端开发人员,我认为值得花些时间学习它。

好消息是,你已经有了一个良好的开端,现在你应该知道如何利用JestReact Testing Library为你的 React 组件编写测试,并且有了这个良好的基础,你可以开始探索其他库和解决方案。

如果这篇文章反响不错,我计划再写一篇文章,介绍一些更高级的例子。再次感谢您抽出时间阅读。

本文参考的资源:

特别感谢ooloo.ioJohannes Kettmann

对于想要成为一名合格的前端开发人员的人来说,我推荐ooloo.io的课程。它介绍了一些概念,例如:创建像素级完美设计、规划和实现复杂的 UI 组件、在 IDE 中调试以及编写集成测试,这些内容在大多数在线教程或课程中并不常见。是的,我从这门课程中获得了许多灵感,最终帮助我完成了这篇文章。

文章来源:https://dev.to/kelvin9877/how-to-write-tests-for-react-in-2020-4oai