端到端测试用于断言应用程序从头到尾的流程。无需手动处理测试(例如,在整个应用程序中手动点击),您可以编写一个在构建应用程序时运行的测试。这就是我们所说的持续集成,它是一件很棒的事情。编写一些代码,保存它,然后让工具完成确保它不会破坏任何内容的繁重工作。
Cypress 只是一个端到端测试框架,它可以为我们完成所有点击工作,这就是我们将在本文中探讨的内容。它实际上适用于任何现代 JavaScript 库,但我们将在示例中将其与 React 集成。
让我们设置一个要测试的应用程序
在本教程中,我们将编写测试以涵盖我构建的待办事项应用程序。您可以克隆存储库,并在我们将它与 Cypress 集成时一起学习。
git clone [email protected]:kinsomicrote/cypress-react-tutorial.git
导航到应用程序,并安装依赖项
cd cypress-react-tutorial
yarn install
Cypress 不是依赖项的一部分,但您可以通过运行以下命令来安装它
yarn add cypress --dev
现在,运行此命令以打开 Cypress
node_modules/.bin/cypress open

一遍又一遍地向终端键入该命令可能会让人感到厌烦,但是您可以将此脚本添加到项目根目录中的package.json
文件中
"cypress": "cypress open"
现在,您只需执行一次npm run cypress
,Cypress 就会一直处于待命状态。为了了解我们将要测试的应用程序的外观,您可以通过运行yarn start
来启动 React 应用程序。
我们将首先编写一个测试来确认 Cypress 是否正常工作。在cypress/integration
文件夹中,创建一个名为init.spec.js
的新文件。该测试断言true
等于true
。我们只需要它来确认它是否正常工作,以确保 Cypress 能够为整个应用程序正常运行。
describe('Cypress', () => {
it('is working', () => {
expect(true).to.equal(true)
})
})
您应该会看到一个打开的测试列表。转到那里并选择init.spec.js
。

这应该会运行测试并弹出一个窗口,显示测试通过。

在我们仍在init.spec.js
中时,让我们添加一个测试来断言我们可以通过在浏览器中点击http://localhost:3000
来访问应用程序。这将确保应用程序本身正在运行。
it('visits the app', () => {
cy.visit('http://localhost:3000')
})
我们调用visit()
方法并传递应用程序的 URL。我们可以访问一个名为cy
的全局对象,用于调用 Cypress 上可用的方法。

为了避免每次都必须编写 URL,我们可以设置一个基本 URL,可以在我们编写的测试中使用。打开应用程序主目录中的cypress.json
文件,并在其中添加 URL 定义
{
"baseUrl": "http://localhost:3000"
}
您可以将测试块更改为如下所示
it('visits the app', () => {
cy.visit('/')
})
…并且测试应该继续通过。🤞
测试表单控件和输入
我们将要编写的测试将涵盖用户如何与待办事项应用程序交互。例如,我们希望确保在应用程序加载时输入处于焦点状态,以便用户可以立即开始输入任务。我们还希望确保其中有一个默认任务,以便列表默认情况下不为空。当没有任务时,我们希望显示文本来告知用户。
首先,请在 integration 文件夹中创建一个名为 form.spec.js
的新文件。文件名称并不重要。我们以“form”作为前缀,因为我们最终要测试的是表单输入。您可以根据您计划如何组织测试来将其命名为其他名称。
我们将向文件中添加一个describe
块
describe('Form', () => {
beforeEach(() => {
cy.visit('/')
})
it('it focuses the input', () => {
cy.focused().should('have.class', 'form-control')
})
})
beforeEach
块用于避免不必要的重复。对于每个测试块,我们需要访问应用程序。每次重复该行都是多余的,beforeEach
确保在每种情况下 Cypress 都访问应用程序。
对于测试,让我们检查应用程序首次加载时处于焦点的 DOM 元素是否具有form-control
类。如果您检查源文件,您将看到输入元素具有名为form-control
的类,并且我们将其autoFocus
作为元素属性之一
<input
type="text"
autoFocus
value={this.state.item}
onChange={this.handleInputChange}
placeholder="Enter a task"
className="form-control"
/>
保存后,返回测试屏幕并选择form.spec.js
以运行测试。

接下来,我们将测试用户是否可以成功地将值输入到输入字段中。
it('accepts input', () => {
const input = "Learn about Cypress"
cy.get('.form-control')
.type(input)
.should('have.value', input)
})

我们已向输入中添加了一些文本(“了解 Cypress”)。然后,我们使用cy.get
获取具有form-control
类名的 DOM 元素。我们也可以执行类似于cy.get('input')
的操作并获得相同的结果。获取元素后,cy.type()
用于输入我们分配给input
的值,然后我们断言具有form-control
类的 DOM 元素的值与input
的值匹配。
换句话说

我们的应用程序还应该有两个在应用程序运行时默认创建的待办事项。重要的是我们有一个测试来检查它们是否确实已列出。
我们想要什么?在我们的代码中,我们使用列表项(<li>
)元素将任务显示为列表中的项目。由于默认情况下我们列出了两个项目,这意味着列表在开始时应该有两个长度。因此,测试将如下所示
it('displays list of todo', () => {
cy.get('li')
.should('have.length', 2)
})

哦!如果用户无法向列表中添加新任务,这个应用程序将是什么样子?我们最好也对此进行测试。
it('adds a new todo', () => {
const input = "Learn about cypress"
cy.get('.form-control')
.type(input)
.type('{enter}')
.get('li')
.should('have.length', 3)
})
这看起来类似于我们在前两个测试中编写的内容。我们获取输入并模拟向其中键入值。然后,我们模拟提交一个任务,该任务应该更新应用程序的状态,从而将长度从 2 增加到 3。因此,实际上,我们可以基于我们已经拥有的内容进行构建!

将值从 3 更改为 2 将导致测试失败——这是我们期望的,因为列表默认情况下应该有两个任务,提交一次应该产生总共三个任务。
您可能想知道如果用户在尝试提交新任务之前删除了默认任务中的一个(或两个),会发生什么。好吧,我们也可以为此编写一个测试,但在本示例中我们没有做出这样的假设,因为我们只想确认可以提交任务。这是一种简单的方法,可以在我们开发过程中测试基本提交功能,并且我们以后可以考虑高级/边缘情况。
我们需要测试的最后一个功能是删除任务。首先,我们希望删除默认任务项之一,然后查看删除发生后是否还剩下一个。这与之前的情况相同,但我们应该期望列表中只剩下一个项目,而不是在向列表中添加新任务时预期的三个项目。
it('deletes a todo', () => {
cy.get('li')
.first()
.find('.btn-danger')
.click()
.get('li')
.should('have.length', 1)
})
好的,那么如果我们删除列表中的所有默认任务并且列表完全为空会发生什么?假设当列表中没有更多项目时,我们希望显示此文本:“您已完成所有任务。做得很好!”
这与我们之前所做的没有太大区别。您可以先尝试一下,然后回来查看它的代码。
it.only('deletes all todo', () => {
cy.get('li')
.first()
.find('.btn-danger')
.click()
.get('li')
.first()
.find('.btn-danger')
.click()
.get('.no-task')
.should('have.text', 'All of your tasks are complete. Nicely done!')
})
这两个测试看起来很相似:我们获取列表项元素,定位第一个元素,并使用cy.find()
查找具有btn-danger
类名的 DOM 元素(再次强调,这是本示例应用程序中删除按钮的完全任意类名)。我们模拟元素上的点击事件以删除任务项。

测试网络请求
网络请求非常重要,因为这通常是应用程序中使用的数据来源。假设我们的应用程序中有一个组件,它向服务器发出请求以获取将显示给用户的数据。假设组件标记如下所示
class App extends React.Component {
state = {
isLoading: true,
users: [],
error: null
};
fetchUsers() {
fetch(`https://jsonplaceholder.typicode.com/users`)
.then(response => response.json())
.then(data =>
this.setState({
users: data,
isLoading: false,
})
)
.catch(error => this.setState({ error, isLoading: false }));
}
componentDidMount() {
this.fetchUsers();
}
render() {
const { isLoading, users, error } = this.state;
return (
<React.Fragment>
<h1>Random User</h1>
{error ? <p>{error.message}</p> : null}
{!isLoading ? (
users.map(user => {
const { username, name, email } = user;
return (
<div key={username}>
<p>Name: {name}</p>
<p>Email Address: {email}</p>
<hr />
</div>
);
})
) : (
<h3>Loading...</h3>
)}
</React.Fragment>
);
}
}
在这里,我们使用JSON Placeholder API作为示例。我们可以进行如下测试以测试我们从服务器获得的响应
describe('Request', () => {
it('displays random users from API', () => {
cy.request('https://jsonplaceholder.typicode.com/users')
.should((response) => {
expect(response.status).to.eq(200)
expect(response.body).to.have.length(10)
expect(response).to.have.property('headers')
expect(response).to.have.property('duration')
})
})
})

测试服务器(而不是存根它)的好处在于,我们可以确定我们获得的响应与用户获得的响应相同。要了解有关网络请求以及如何存根网络请求的更多信息,请参阅 Cypress 文档中的此页面。
从命令行运行测试
Cypress 测试可以在没有提供的UI的情况下从终端运行
./node_modules/.bin/cypress run
…或者
npx cypress run
让我们运行我们编写的表单测试
npx cypress run --record --spec "cypress/integration/form.spec.js"
终端应该在那里输出结果,并提供测试内容的摘要。

在文档中,您可以了解有关使用命令行与 Cypress 结合使用的更多信息。
总结
测试对于不同的人来说,要么让人兴奋,要么让人害怕。希望我们在本文中探讨的内容能激发大家在应用程序中实施测试的热情,并展示其实现起来相对简单。Cypress 是一个优秀的工具,我个人在工作中也经常用到,但还有其他工具可供选择。无论你使用什么工具(以及你对测试的感受如何),希望你都能看到测试的好处,并更有动力去尝试一下。
没有提到为你的选择器使用 data-test-id 吗?你将把人们带入未来充满痛苦的世界。
使用
fetch
你遇到过什么问题吗?据我所知,fetch
仍然不受 Cypress 支持,你需要使用这个 技巧cy.get() 并不总是适用于 React 应用程序,因为它会在 React 渲染之前尝试查找 DOM 元素。