E2E(end to end)端到端测试是最直观可以理解的测试类型。在前端应用程序中,端到端测试可以从用户的视角通过真实浏览器自动检查应用程序是否正常工作。
E2E 把整个系统当作一个黑盒,测试人员模拟真实用户在浏览器中操作 UI,测试在真实浏览器环境运行测试,测试出的问题可能是前端也可能是后端导致的,比如:
E2E 测试一般是由 QA 测试工程师来做。稍小的项目可能根据测试用例(excel)操作一遍就完了,稍大一点的会些一些自动化测试的代码。
前端可能会为核心的、主要的或稳定的业务流程写 E2E,不过占据的测试比例要小很多,主要目的是:
本文主要通过 Cypress 学习 E2E 测试。
官方文档:Installing Cypress
简单使用没必要特意安装到某个前端项目,可以单独安装,运行真实浏览器环境,测试某个网站,这个网站可以是本地的,也可以是在线的。
mkdir cypress-demo
cd cypress-demo
npm init -y
npm i -D cypress
配置启动脚本:
"scripts": {
"cypress:open": "cypress open"
},
运行命令: npm run cypress:open
,会打开测试运行器(一个浏览器窗口),INTEGRATION TESTS
显示初始生成的测试用例文件,默认不运行
可以点击任意测试模块,它会打开一个浏览器窗口(Chrome)运行测试,左边是当前的测试概况,右边是一个真实的浏览器(地址栏+页面)
当 cypress 启动后,默认会在项目根目录下创建一个 cypress
目录,其中包含 4 个子目录:
├─ fixtures # 存放测试之前准备的测试数据
│ └─ example.json
├─ integration # 存放测试代码文件,可以通过子目录的方式进行分类
│ ├─ 1-getting-started
│ │ └─ todo.spec.js
│ └─ 2-advanced-examples
│ ├─ ...
│ └─ window.spec.js
├─ plugins # 配置 cypress 插件
│ └─ index.js
└─ support # 相关支持配置
├─ commands.js
└─ index.js
官方文档:Writing Your First Test
在 integration
目录下创建测试文件 sample_spec.js
。
测试运行器会实时加载,创建完成后会显示这个测试文件。
可以打开运行这个测试文件,测试窗口也会实时加载。
// describe it 使用的 Mocha
describe('My First Test', () => {
it('Does not do much!', () => {
// 断言使用的 chai
expect(true).to.equal(true)
})
})
一些 API 看上去和 Jest 很像,但其实 Cypress 中默认 describe
和 it
使用的 Mocha,expect
使用的 chai,如果想要使用 Jest 可以进行配置。
查看测试窗口:
如果编写一个失败的错误 expect(true).to.equal(false)
:
会显示具体哪里导致的错误和文件地址,点击文件还可以用配置的编辑器打开测试用例文件,跳转到对应的代码位置。
describe('My First Test', () => {
it('Does not do much!', () => {
// 访问一个页面
cy.visit('http://npmjs.com/')
// 查询一个元素
cy.contains('Build amazing things')
})
})
保存后观察测试窗口更新,可以看到右侧加载了页面。
即使没有添加断言,测试用例仍然成功,这是因为 Cypress 的许多命令如果没有找到预期的结果,在运行就会失败,这被称为默认断言。
如果编写一个错误测试,如查询页面上没有的内容 cy.contains('123')
,测试将失败,但是会等待大于 4 秒后才给出结果,因为内容显示可能需要时间或异步加载,所以 Cypress 默认会等待 4 秒。
丰富用例:
describe('My First Test', () => {
it('Does not do much!', () => {
// 访问一个页面
cy.visit('http://npmjs.com/')
// 查询一个元素
cy.contains('Build amazing things')
// 找到输入框,输入内容并按下回车
// .get(选择器) 类似于 jQuery 的 $(选择器)
cy.get('[placeholder="Search packages"]').type('cypress{enter}')
// 断言内容
cy.contains('Cypress.io end to end testing tool')
})
})
Cypress 会自动识别系统中安装的浏览器,并允许切换:
可以使用这个案例代码 gothinkster/vue-realworld-example-app,它是 realworld 案例的 Vue 版本。
代码中样式表
https://demo.productionready.io/main.css
从外部站点引入,如果有跨域限制,可手动拷贝到本地。
clone 或下载 zip 源码,npm i
安装依赖,npm run serve
运行应用。
可以使用 Vue CLI 在创建应用的时候选择安装 Cypress。
已有的项目,也可以手动安装配置 Cypress。
或者使用官方提供的插件:e2e-cypress
# 在项目中安装插件
npx @vue/cli add e2e-cypress
# 如果全局安装了 @vue/cli 可以直接使用 vue 命令安装
# vue add e2e-cypress
安装完成后,查看项目:
1、package.json
中增加了插件依赖和启动命令:
{
...
"scripts": {
...
"test:e2e": "vue-cli-service test:e2e"
},
"devDependencies": {
...
"@vue/cli-plugin-e2e-cypress": "^5.0.4"
}
}
2、项目中新增了 Cypress 的配置文件 cypress.json
,里面指定了一个插件文件,在运行测试后的时候会加载这个插件:
{
"pluginsFile": "tests/e2e/plugins/index.js"
}
3、还在 tests
目录下创建了 e2e
目录,存放Cypress 的代码,其中测试文件存放在 specs
中,其中包含一个示例文件。
describe('My First Test', () => {
it('Visits the app root url', () => {
cy.visit('/')
// cy.contains('h1', 'Welcome to Your Vue.js App')
})
})
4、运行测试命令 npm run test:e2e
该命令会将应用进行打包构建,Cypress 测试的就是打包后的应用,构建完成后,就会启动测试打开测试运行器。
官方介绍了如何配置 IDE 的代码智能提示功能:Intelligent Code Completion
在文件顶部添加注释(三斜杠):
///
对基于 DOM 的命令,Cypress 默认等待 4 秒,案例中有些操作需要等待接口响应,而响应时间可能要久一些,可以修改默认等待时间:
// cypress.json
{
"pluginsFile": "tests/e2e/plugins/index.js",
"defaultCommandTimeout": 10000
}
src\views\Login.vue
:
<ul v-if="errors" class="error-messages">
<li data-testid="error-message-item" v-for="(v, k) in errors" :key="k">{{ k }} {{ v | error }}li>
ul>
<form @submit.prevent="onSubmit(email, password)">
<fieldset class="form-group">
<input
class="form-control form-control-lg"
type="text"
v-model="email"
placeholder="Email"
data-testid="email"
/>
fieldset>
<fieldset class="form-group">
<input
class="form-control form-control-lg"
type="password"
v-model="password"
placeholder="Password"
data-testid="password"
/>
fieldset>
<button
data-testid="submit"
class="btn btn-lg btn-primary pull-xs-right"
>
Sign in
button>
form>
// tests\e2e\specs\test.js
///
describe('用户登录', () => {
it('登录成功,跳转到首页', () => {
// 打开登录页
cy.visit('#/login')
// 输入邮箱
cy.get('input[data-testid="email"]')
.type('[email protected]')
// 输入密码
cy.get('input[data-testid="password"]')
.type('123456')
// 按下登录按钮
cy.get('button[data-testid="submit"]')
.click()
// 断言:当前 url 包含 `#/`
cy.url().should('contain','#/')
// 断言:页面包含指定元素
cy.contains('h1','conduit')
})
it('登录失败,展示错误提示信息', () => {
cy.visit('#/login')
cy.get('input[data-testid="email"]')
.type('[email protected]')
cy.get('input[data-testid="password"]')
.type('@123456')
cy.get('button[data-testid="submit"]')
.click()
// 断言:页面包含指定元素
cy.get('[data-testid="error-message-item"]')
.should('exist')
.should('contain', 'email or password is invalid')
})
})
src\views\ArticleEdit.vue
:
<form @submit.prevent="onPublish(article.slug)">
<fieldset :disabled="inProgress">
<fieldset class="form-group">
<input
data-testid="article-title"
type="text"
class="form-control form-control-lg"
v-model="article.title"
placeholder="Article Title"
/>
fieldset>
<fieldset class="form-group">
<input
data-testid="article-description"
type="text"
class="form-control"
v-model="article.description"
placeholder="What's this article about?"
/>
fieldset>
<fieldset class="form-group">
<textarea
data-testid="article-body"
class="form-control"
rows="8"
v-model="article.body"
placeholder="Write your article (in markdown)"
/>
fieldset>
<fieldset class="form-group">
<input
data-testid="article-tag"
type="text"
class="form-control"
placeholder="Enter tags"
v-model="tagInput"
@keypress.enter.prevent="addTag(tagInput)"
/>
<div class="tag-list">
<span
class="tag-default tag-pill"
v-for="(tag, index) of article.tagList"
:key="tag + index"
>
<i class="ion-close-round" @click="removeTag(tag)"/>
{{ tag }}
span>
div>
fieldset>
fieldset>
<button
data-testid="article-publish"
:disabled="inProgress"
class="btn btn-lg pull-xs-right btn-primary"
type="submit"
>
Publish Article
button>
form>
describe.only('发布文章', () => {
it('发布成功,跳转到文章详情页', () => {
// 准备登录状态
const token = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJlbWFpbCI6InpieUByZW5zaGlqaWFuLmNvbSIsInVzZXJuYW1lIjoi5ZGo56eJ5LmJIiwiaWF0IjoxNjQ4NTM0NTc5LCJleHAiOjE2NTM3MTg1Nzl9.cjPX0Rq4O2jjFNZULL93DwJ8aP3wXig89QZYqL91_CE'
window.localStorage.setItem('id_token', token)
cy.visit('#/editor')
const articleTitle = '这是一篇文章'
cy.get('[data-testid="article-title"]').type(articleTitle)
cy.get('[data-testid="article-description"]').type('文章描述')
cy.get('[data-testid="article-body"]').type('文章内容')
cy.get('[data-testid="article-tag"]').type('aa{enter}').type('bb{enter}').type('cc{enter}')
cy.get('[data-testid="article-publish"]').click()
cy.contains('h1', articleTitle)
})
})