如何使用React测试库、Jest和Cypress在React中进行测试驱动开发

184 阅读8分钟

什么是测试驱动开发?

测试驱动开发或简称为TDD,本质上是开发人员和团队在测试他们的代码时经历的一个过程。编码、设计和测试结合在一起,并创建测试用例,以确保代码经过稳健的测试,并在达到生产水平之前,在开发阶段解决任何bug或错误。

这被认为是一种良好的做法,也是所有开发人员在处理代码库时应该遵循的方法。通过这个过程,代码会随着时间的推移而得到改善,从而使应用程序更加稳定。在这篇文章中,我们将讨论单元测试、集成测试和端到端测试。

什么是单元测试?

基本上,单元测试是一种测试应用程序中小部分代码样本的方法。这可以包括运行代码块的函数或返回数据的API。其目的是找出代码是否正常工作,以及在发生错误时是否能捕捉到。例如,在一个表格中返回的数据不正确。

什么是集成测试?

集成测试实际上就是将多个单元测试组合在一起。所以,一个单元测试可以测试一个功能,而集成测试则更像是一个测试套件。所以从某种意义上说,你现在是在同时测试多个代码块,比如整个旋转木马组件。如果是单元测试,那么你将只测试图片是否加载,而在集成测试中,你现在要测试标题是否加载,图片是否加载,以及正确的数据是否显示等。

什么是端到端测试?

端到端测试是一种测试应用程序前端工作流程的方法。它是一种测试整个应用程序的方法,这样你就知道它的行为是你所期望的那样。端到端测试与其他两种测试的区别在于,端到端测试是对软件和系统的测试,而其他两种测试更多的是用于系统测试。

如何进行测试?

在命令行中进行单元和集成测试时,Jest和React测试库非常流行。Cypress是一个流行的工具,用于在浏览器中进行端到端测试。Jest甚至可以在后端使用,所以你可以覆盖所有的基础,使用相同的库进行后端和前端的测试工作。

单元测试/集成测试库

端到端测试库

项目设置

让我们来设置我们的项目。导航到你电脑上的一个目录,打开命令行,运行下面的命令。

npx create-react-app tdd-react-cypress-app

现在运行这个命令来启动Cypress,你应该看到你的电脑上打开了一个Cypress窗口。

# To start Cypress

有很多集成测试的例子,如果你愿意,你可以运行它们,看看它们做什么。当你准备好后,在你的代码编辑器中打开项目,在你的项目中找到位于my-app/cypress/integration的Cypress集成文件夹,并删除其中的文件夹,这样我们就有了一块干净的空白。

然后创建一个名为user.spec.js的文件,并将其放在集成文件夹中,代码如下。这将是第一个端到端测试,但它还不能工作,因为我们的应用程序还没有代码!

describe('user form flow', () => {
    it('user can save form', () => {
        cy.get('input[name="firstName"]').type('Eren');
        cy.get('input[name="lastName"]').type('Yeager');
        cy.get('input[name="email"]').type('erenyeager@gmail.com');
        cy.get('input[name="career"]').type('Attack Titan');
        cy.get('textarea[name="bio"]').type('Hello there my name is Eren Yeager!');
        cy.get('input[name="save"]').click();

现在终于到了将代码添加到我们先前创建的文件中的时候了。把下面的代码复制并粘贴到相应的文件中。这是一个相当繁琐的过程,因为它们被分成了不同的组件,但最终会是值得的。

另外,你也可以直接克隆/下载 repo,然后跳到本文的结尾,即单元测试和集成测试部分。

github.com/andrewbaisd…

应用程序组件文件

App.css

@import url('https://fonts.googleapis.com/css2?family=Quicksand:wght@400;500;700&display=swap');
*,
    margin: 0;
    box-sizing: 0;
html {
body {
    font-size: 1.6rem;
    color: #2d2d2d;
    background: #b3b3b3
.container {
    display: flex;
    flex-flow: row nowrap;
    width: 100%;
    height: 50rem;
    max-width: 100rem;
main {
    max-width: 60rem;

App.js

import Sidebar from './components/Sidebar/Sidebar';
import Header from './components/Header/Header';
import Profile from './components/Profile/Profile';
import './App.css';
const App = () => {
                <main>
                    <Profile />
export default App;

App.test.js

import { render, screen } from '@testing-library/react';
import App from './App';
describe('<App />', () => {
        const el = screen.getByTestId('container');
        expect(el.className).toBe('container');

表单组件文件

表单.css

.profile-details-form-container {
    display: flex;
    flex-flow: column nowrap;
.profile-details-form-container input {
    height: 2rem;
    padding: 0.5rem;
    font-size: 1.3rem;
.profile-details-form-container label {
.profile-details-form-container textarea {
    height: 5rem;
    resize: none;
    padding: 0.5rem;
    font-size: 1.3rem;
input[type='submit'] {
    background: #7e7dd6;
    color: #ffffff;
    font-weight: 600;
    width: 8rem;
    border-radius: 0.2rem;
    cursor: pointer;
    font-size: 1rem;
.form-output {
    width: 40rem;
    font-weight: 600;
    font-size: 0.8rem;

Form.js

import { useState } from 'react';
import './Form.css';
const Form = () => {
const [firstName, setFirstName] = useState('');
const [lastName, setLastName] = useState('');
const [email, setEmail] = useState('');
const [career, setCareer] = useState('');
const [bio, setBio] = useState('');
const [data, setData] = useState('');
const formSubmit = (e) => {
e.preventDefault();
const user = {
firstName: firstName,
lastName: lastName,
email: email,
career: career,
bio: bio,
};
const formData = JSON.stringify(user);
console.log(formData);
setData(formData);
clearForm();
};
const clearForm = () => {
setFirstName('');
setLastName('');
setEmail('');
setCareer('');
setBio('');
};
return (
<>
<div>
<form onSubmit={formSubmit} className="profile-details-form-container">
<div>
<label data-testid="firstname">First Name</label>
<input
type="text"
name="firstName"
value={firstName}
onChange={(e) => setFirstName(e.target.value)}
placeholder="First Name"
/>
</div>
<div>
<label data-testid="lastname">Last Name</label>
<input
type="text"
name="lastName"
value={lastName}
onChange={(e) => setLastName(e.target.value)}
placeholder="Last Name"
/>
</div>
<div>
<label data-testid="email">Email</label>
<input
type="text"
name="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
placeholder="Email"
/>
</div>
<div>
<label data-testid="career">Career</label>
<input
type="text"
name="career"
value={career}
onChange={(e) => setCareer(e.target.value)}
placeholder="Career"
/>
</div>
<div>
<label data-testid="bio">Bio</label>
<textarea name="bio" value={bio} onChange={(e) => setBio(e.target.value)} placeholder="Bio"></textarea>
</div>
<div>
<input name="save" type="submit" value="Save" />
</div>
</form>
<div className="form-output">
<p>Output</p>
<div>{data}</div>
</div>
</div>
</>
);
};
export default Form;

表单.测试.js

import { render, screen } from '@testing-library/react';
import Form from './Form';
describe('<Form />', () => {
it('has a first name label', () => {
render(<Form />);
const el = screen.getByTestId('firstname');
expect(el.innerHTML).toBe('First Name');
});
it('has a last name label', () => {
render(<Form />);
const el = screen.getByTestId('lastname');
expect(el.innerHTML).toBe('Last Name');
});
it('has a email label', () => {
render(<Form />);
const el = screen.getByTestId('email');
expect(el.innerHTML).toBe('Email');
});
it('has a career label', () => {
render(<Form />);
const el = screen.getByTestId('career');
expect(el.innerHTML).toBe('Career');
});
it('has a bio label', () => {
render(<Form />);
const el = screen.getByTestId('bio');
expect(el.innerHTML).toBe('Bio');
});
});

Header.css

header {
    display: flex;
    flex-flow: row nowrap;
    justify-content: space-between;
    padding: 1rem;
    border-bottom: 0.1rem solid rgb(234, 234, 234);
.page-title,
    flex-flow: row nowrap;
    justify-content: center;
    align-items: center;
.page-title h1 {
.page-info {
    flex-flow: row nowrap;
    justify-content: space-around;
    max-width: 15rem;
    width: 100%;
.page-info button {
    background: #7e7dd6;
    color: #ffffff;
    padding: 1rem;
    border-radius: 0.5rem;
    font-weight: 600;
    cursor: pointer;
.secure,
    flex-flow: row nowrap;
    justify-content: center;
    align-items: center;
    border: 0.2rem solid rgb(233, 233, 233);
    padding: 0.5rem;
    height: 2rem;
    border-radius: 0.5rem;

Header.js

import './Header.css';
const Header = () => {
                    <div>📝</div>
                <div className="page-info">
                    <div role="alert" className="notifications">
                    <button data-testid="confirm-btn">Confirm</button>
export default Header;

Header.test.js

import { screen, render } from '@testing-library/react';
import Header from './Header';
describe('<Header />', () => {
        const el = screen.getByTestId('info');
        expect(el.innerHTML).toBe('Information');
    it('has a notification div', () => {
        const el = screen.getByRole('alert');
    it('has a confirm button', () => {
        const el = screen.getByTestId('confirm-btn');
        expect(el.innerHTML).toBe('Confirm');

Profile.css

.profile-container {
    flex-flow: row nowrap;
    justify-content: space-between;
    padding: 1rem;
    background: #ffffff;
.profile-container section {
.profile-container h1 {
.profile-container p {

剖面图.js

import Form from '../Form/Form';
import ProfileDetails from '../ProfileDetails/ProfileDetails';
import './Profile.css';
const Profile = () => {
                        <p>Fill in your user details in the form below.</p>
                    <Form />
                <section>
export default Profile;

Profile.test.js

import { screen, render } from '@testing-library/react';
import Profile from './Profile';
describe('<Profile />', () => {
        const el = screen.getByText(/User Profile/i);
        expect(el).toBeTruthy();

ProfileDetails.css

.profile-details-container {
.profile-details-container p {
    font-weight: 600;
    margin-top: 1rem;
.profile-details-container form label {
    margin-left: 1rem;
.profile-details-image {
    flex-flow: column nowrap;
    align-items: flex-start;
.profile-details-image h1 {
    margin-bottom: 1rem;
.profile-details-image div {
    border-radius: 100%;
    height: 5rem;
    width: 5rem;
    display: flex;
    flex-flow: row nowrap;
    justify-content: center;
    align-items: center;

ProfileDetails.js

import './ProfileDetails.css';
const ProfileDetails = () => {
                    <div>😎</div>
                <p>Select your gender</p>
                <form>
                        <label htmlFor="male">Male</label>
                        <br />
                    <div>
                        <label htmlFor="female">Female</label>
                        <br />
                    <div>
                        <label htmlFor="nonBinary">Non-binary</label>
                        <br />
export default ProfileDetails;

剖析细节.测试.js

import { screen, render } from '@testing-library/react';
import ProfileDetails from './ProfileDetails';
describe('<ProfileDetails />', () => {
        const el = screen.getByText(/Select your gender/i);
        expect(el).toBeTruthy();

侧边栏.css

aside {
    backdrop-filter: blur(10px);
    padding: 2rem;
    width: 100%;
    max-width: 16rem;
    border-top-left-radius: 8px;
    border-bottom-left-radius: 8px;
    height: 43.4rem;
.profile-sidebar-container {
    flex-flow: row nowrap;
    justify-content: space-between;
.profile-image {
    border-radius: 100%;
    padding: 1rem;
    height: 2rem;
.profile-user p {
.profile-user h1 {
.settings {
    flex-flow: row nowrap;
    justify-content: center;
    align-items: center;
    background: #ffffff;
    padding: 0.5rem;
    height: 2rem;
    width: 2rem;
    border-radius: 0.5rem;
    border: none;
    cursor: pointer;
aside {
    flex-flow: column nowrap;
    justify-content: space-between;
aside nav,
    flex-flow: column nowrap;
aside nav a,
    text-decoration: none;
    font-weight: 600;
    padding: 0.4rem;
    border-radius: 0.2rem;
aside nav a:hover,

侧边栏.js

import './Sidebar.css';
const Sidebar = () => {
                    <div className="profile-user">
                        <h1>Eren Yeager</h1>
                    <button className="settings">⚙️</button>
                <nav>
                    <a href="/" data-testid="dashboard">
                    <a href="/" data-testid="assets">
                    <a href="/" data-testid="business">
                    <a href="/" data-testid="data">
                    <a href="/" data-testid="backups">
                <div className="support-log-out">
                    <a href="/" data-testid="log-out">
export default Sidebar;

Sidebar.test.js

import { screen, render } from '@testing-library/react';
import Sidebar from './Sidebar';
describe('<Sidebar />', () => {
        const el = screen.getByTestId('search');
    it('has a dashboard link', () => {
        const el = screen.getByTestId('dashboard');
    it('has a assets link', () => {
        const el = screen.getByTestId('assets');
    it('has a business link', () => {
        const el = screen.getByTestId('business');
    it('has a data link', () => {
        const el = screen.getByTestId('data');
    it('has a backups link', () => {
        const el = screen.getByTestId('backups');
    it('has a support link', () => {
        const el = screen.getByTestId('support');
    it('has a log-out link', () => {
        const el = screen.getByTestId('log-out');

接下来在你的命令行应用程序中运行下面的命令,但在不同的标签/窗口中。所以现在你应该有React、Jest和Cypress同时运行。你可能需要按a回车键来运行所有Jest测试。

# To start React
# To start Jest
# To start Cypress

单元测试和集成测试

你可以在组件文件夹中找到所有单元和集成测试的例子。所有的测试都应该是通过的,你可以玩玩这些文件,看看测试的失败和通过。

端到端测试

端到端测试在my-app/cypress/integration/user.spec.js中。要运行这些测试,请进入Cypress应用程序窗口,点击按钮运行测试。如果你点击那个有Electron作为选项的下拉菜单,你将能够选择不同的网络浏览器。

user.spec.js集成测试自动填写表格,然后点击保存按钮。在页面的底部会输出创建的对象的字符串版本。

所以让我们做一个快速的总结,你现在知道如何创建。

  • 单元测试
  • 集成测试
  • 端到端测试

这只是一个简单的介绍,看看JestReact测试库Cypress的官方文档以了解更多。

构建可组合的Web应用

不要建立网络单体。使用 比特来创建和组成解耦的软件组件--在你喜欢的框架中,如React或Node。构建可扩展和模块化的应用程序,提供强大和愉快的开发体验。

把你的团队带到 比特云来托管并共同协作开发组件,并作为一个团队加快、扩大和规范开发。尝试用设计系统 或微前端可组合前端,或探索用服务器端组件可组合后端

试一试吧→

了解更多


如何使用React测试库、Jest和Cypress在React中进行测试驱动开发》最初发表于《Bits and Pieceson Medium》,人们在这里通过强调和回应这个故事继续对话。