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忽略这个目录。
