End to End(E2E)测试,将系统作为一个整体测试
Cypress完全在浏览器中运行
在前端项目中安装Cypress
npm install --save-dev cypress
在前端的npm-script添加cypress:open
{
// ...
"scripts": {
"start": "react-scripts start",
// ...
"cypress:open": "cypress open"
},
// ...
}
在后端的npm-script中添加start:test
(没有用,应该还是用yarn dev启动后端项目,否则无法连接数据库)
{
// ...
"scripts": {
"start": "cross-env NODE_ENV=production node index.js",
// ...
"start:test": "cross-env NODE_ENV=test node index.js"
},
// ...
}
当后端和前端都在运行时,我们可以使用如下命令启动 Cypress
npm run cypress:open
Cypress会在项目中创建一个Cypress目录,将integration中的example文件都删除,创建note_app.spec.js文件
describe('Note app', function () {
it('from page can be opened', function () {
cy.visit('http://localhost:3000')
cy.contains('Notes')
cy.contains('Note app, Department of Computer Science, University of Helsinki 2021')
})
})
describe和it是Cypress从Mocha测试库中借用的部件,Mocha不建议使用箭头函数
cy.visit等是Cypress自己的命令
点击测试用例,查看测试结果
writing to a form
describe('Note app', function () {
beforeEach(function () {
cy.visit('http://localhost:3000')
})
it('from page can be opened', function () {
cy.contains('Notes')
cy.contains('Note app, Department of Computer Science, University of Helsinki 2021')
})
// it('front page contains random text', function () {
// cy.visit('http://localhost:3000')
// cy.contains('wtf is this app?')
// })
it('login form can be opened', function () {
cy.contains('login').click()
cy.get('#username').type('root')
cy.get('#password').type('salainen')
cy.get('#login-button').click()
cy.contains('Superuser logged-in')
})
})
- 将所有测试通用的部分都写到beforeEach里
- cy.get通过CSS选择器获取元素
- cy.type向input组件内输入内容
- cy.click点击按钮
-
消除ESLint错误
安装eslint-plugin-cypressnpm install eslint-plugin-cypress --save-dev
在eslintrc.js中添加配置
module.exports = {
"env": {
// ...
"cypress/globals": true
},
"plugins": [
// ...
"cypress"
],
}
Testing new note form
describe('Note app', function () {
beforeEach(function () {
cy.visit('http://localhost:3000')
})
// ...
describe('when logged in', function () {
beforeEach(function () {
cy.contains('login').click()
cy.get('#username').type('root')
cy.get('#password').type('salainen')
cy.get('#login-button').click()
})
it('a new note can be created', function () {
cy.contains('new note').click()
cy.get('input').type('a note created by cypress')
cy.contains('save').click()
cy.contains('a note created by cypress')
})
})
})
Controlling the state of the database
实现每次测试时都清空数据库
在后端的controllers目录添加testing.js路由 ```javascript const router = require(‘express’).Router() const Note = require(‘../models/note’) const User = require(‘../models/user’)
router.post(‘/reset’, async (request, response) => { await Note.deleteMany({}) await User.deleteMany({})
response.status(204).end() })
module.exports = router
在app.js中添加test模式时运行
```javascript
// ...
app.use('/api/login', loginRouter)
app.use('/api/users', usersRouter)
app.use('/api/notes', notesRouter)
if (process.env.NODE_ENV === 'test') {
const testingRouter = require('./controllers/testing')
app.use('/api/testing', testingRouter)
}
app.use(middleware.unknownEndpoint)
app.use(middleware.errorHandler)
module.exports = app
用命令**npm run start:test**
启动后端项目
在前端测试的beforeEach中添加用户
describe('Note app', function() {
beforeEach(function() {
cy.request('POST', 'http://localhost:3001/api/testing/reset')
const user = {
name: 'Matti Luukkainen',
username: 'mluukkai',
password: 'salainen'
}
cy.request('POST', 'http://localhost:3001/api/users/', user)
cy.visit('http://localhost:3000')
})
it('front page can be opened', function() {
// ...
})
it('user can login', function() {
// ...
})
describe('when logged in', function() {
// ...
})
})
测试新增note并改变重要性
describe('Note app', function() {
// ...
describe('when logged in', function() {
// ...
describe('and a note exists', function () {
beforeEach(function () {
cy.contains('new note').click()
cy.get('input').type('another note cypress')
cy.contains('save').click()
})
it('it can be made important', function () {
cy.contains('another note cypress')
.contains('make important')
.click()
cy.contains('another note cypress')
.contains('make not important')
})
})
})
})
Failed login test
如果只要运行一个测试,可以使用it.only
这里测试登录失败的例子
it.only('loggin fails with wrong password', function () {
cy.contains('login').click()
cy.get('#username').type('mluukkai')
cy.get('#password').type('wrong')
cy.get('#login-button').click()
cy.contains('Wrong credentials')
})
使用should语法替代contains
it('login fails with wrong password', function() {
// ...
cy.get('.error').should('contain', 'wrong credentials')
})
参考文档:https://docs.cypress.io/guides/references/assertions#Common-Assertions
判断消息是否是红色边框
it('login fails with wrong password', function() {
// ...
cy.get('.error').should('contain', 'wrong credentials')
cy.get('.error').should('have.css', 'color', 'rgb(255, 0, 0)')
cy.get('.error').should('have.css', 'border-style', 'solid')
})
cypress的颜色要用rgb
cy.get同一个组件可以用.and连接
it('login fails with wrong password', function() {
// ...
cy.get('.error')
.should('contain', 'wrong credentials')
.and('have.css', 'color', 'rgb(255, 0, 0)')
.and('have.css', 'border-style', 'solid')
})
Should 应当总是与get 链接(或其他某个可链接命令)
css属性访问在Firefox浏览器中会有异常
Bypassing the UI
Cypress 建议我们不要使用beforeEach 块中的表单登录用户,而是绕过 UI ,对后端执行 HTTP 请求以登录。 原因是,使用 HTTP 请求登录要比填写表单快得多。
实现后端登录并保存到localStorage
describe('when logged in', function() {
beforeEach(function() {
cy.request('POST', 'http://localhost:3001/api/login', {
username: 'mluukkai', password: 'salainen'
}).then(response => {
localStorage.setItem('loggedNoteappUser', JSON.stringify(response.body))
cy.visit('http://localhost:3000')
})
})
it('a new note can be created', function() {
// ...
})
// ...
})
登录代码需要在多个地方使用,可以将它写成自定义命令custom command
自定义命令在cypress/support/commands.js. 中声明
Cypress.Commands.add('login', ({ username, password }) => {
cy.request('POST', 'http://localhost:3001/api/login', {
username, password
}).then(({ body }) => {
localStorage.setItem('loggedNoteappUser', JSON.stringify(body))
cy.visit('http://localhost:3000')
})
})
使用自定义命令
describe('when logged in', function() {
beforeEach(function() {
cy.login({ username: 'mluukkai', password: 'salainen' })
})
it('a new note can be created', function() {
// ...
})
// ...
})
自定义创建便签命令
Cypress.Commands.add('createNote', ({ content, important }) => {
cy.request({
url: 'http://localhost:3001/api/notes',
method: 'POST',
body: { content, important },
headers: {
Authorization: `bearer ${JSON.parse(localStorage.getItem('loggedNoteappUser')).token}`,
},
})
cy.visit('http://localhost:3000')
})
使用命令
describe('Note app', function() {
// ...
describe('when logged in', function() {
it('a new note can be created', function() {
// ...
})
describe('and a note exists', function () {
beforeEach(function () {
cy.createNote({
content: 'another note cypress',
important: false
})
})
it('it can be made important', function () {
// ...
})
})
})
})
Changing the importance of a note
it('other of those can be made important', function () {
cy.contains('second note').parent().find('button').click()
cy.contains('second note').parent().find('button')
.should('contain', 'make not important')
})
.parent()获取父元素,.find查找子元素, cy.get是查找整个页面
为了减少重复代码,使用.as将查找的元素保存为别名,再用.get获取
it('it can be made important', function () {
cy.contains('second note').parent().find('button').as('theButton')
cy.get('@theButton').click()
cy.get('@theButton').should('contain', 'make not important')
})
Running and debugging the tests
const button = cy.contains('log in')
button.click()
debugger()
cy.contains('logout').click()
cypress命令总是返回undefined, 所以上面这段代码不起作用
cypress命令类似promise,可以使用.then
it('then example', function() {
cy.get('button').then( buttons => {
console.log('number of buttons', buttons.length)
cy.wrap(buttons[0]).click()
})
})
使用debug让控制台暂停
https://docs.cypress.io/api/commands/debug
使用命令行运行cypress, 在npm-scripts中添加test:e2e
"scripts": {
// ...
"cypress:open": "cypress open",
"test:e2e": "cypress run"
},
测试执行的视频将被保存到cypress/videos/中,应该用gitignore忽略这个目录。