什么是测试驱动开发?
测试驱动开发或简称为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,然后跳到本文的结尾,即单元测试和集成测试部分。
应用程序组件文件
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集成测试自动填写表格,然后点击保存按钮。在页面的底部会输出创建的对象的字符串版本。
所以让我们做一个快速的总结,你现在知道如何创建。
- 单元测试
- 集成测试
- 端到端测试
这只是一个简单的介绍,看看Jest、React测试库和Cypress的官方文档以了解更多。
构建可组合的Web应用
不要建立网络单体。使用 比特来创建和组成解耦的软件组件--在你喜欢的框架中,如React或Node。构建可扩展和模块化的应用程序,提供强大和愉快的开发体验。
把你的团队带到 比特云来托管并共同协作开发组件,并作为一个团队加快、扩大和规范开发。尝试用设计系统 或微前端的可组合前端,或探索用服务器端组件的可组合后端。

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