End to End(E2E)测试,将系统作为一个整体测试
Cypress完全在浏览器中运行
在前端项目中安装Cypress

  1. npm install --save-dev cypress

在前端的npm-script添加cypress:open

  1. {
  2. // ...
  3. "scripts": {
  4. "start": "react-scripts start",
  5. // ...
  6. "cypress:open": "cypress open"
  7. },
  8. // ...
  9. }

在后端的npm-script中添加start:test
(没有用,应该还是用yarn dev启动后端项目,否则无法连接数据库)

  1. {
  2. // ...
  3. "scripts": {
  4. "start": "cross-env NODE_ENV=production node index.js",
  5. // ...
  6. "start:test": "cross-env NODE_ENV=test node index.js"
  7. },
  8. // ...
  9. }

后端和前端都在运行时,我们可以使用如下命令启动 Cypress

  1. npm run cypress:open

Cypress会在项目中创建一个Cypress目录,将integration中的example文件都删除,创建note_app.spec.js文件

  1. describe('Note app', function () {
  2. it('from page can be opened', function () {
  3. cy.visit('http://localhost:3000')
  4. cy.contains('Notes')
  5. cy.contains('Note app, Department of Computer Science, University of Helsinki 2021')
  6. })
  7. })

describe和it是Cypress从Mocha测试库中借用的部件,Mocha不建议使用箭头函数
cy.visit等是Cypress自己的命令
点击测试用例,查看测试结果
image.png
image.png

writing to a form

  1. describe('Note app', function () {
  2. beforeEach(function () {
  3. cy.visit('http://localhost:3000')
  4. })
  5. it('from page can be opened', function () {
  6. cy.contains('Notes')
  7. cy.contains('Note app, Department of Computer Science, University of Helsinki 2021')
  8. })
  9. // it('front page contains random text', function () {
  10. // cy.visit('http://localhost:3000')
  11. // cy.contains('wtf is this app?')
  12. // })
  13. it('login form can be opened', function () {
  14. cy.contains('login').click()
  15. cy.get('#username').type('root')
  16. cy.get('#password').type('salainen')
  17. cy.get('#login-button').click()
  18. cy.contains('Superuser logged-in')
  19. })
  20. })
  • 将所有测试通用的部分都写到beforeEach里
  • cy.get通过CSS选择器获取元素
  • cy.type向input组件内输入内容
  • cy.click点击按钮
  • cy.contains查看页面是否包含指定内容

    消除ESLint错误

    image.png
    安装eslint-plugin-cypress

    1. npm install eslint-plugin-cypress --save-dev

    在eslintrc.js中添加配置

    1. module.exports = {
    2. "env": {
    3. // ...
    4. "cypress/globals": true
    5. },
    6. "plugins": [
    7. // ...
    8. "cypress"
    9. ],
    10. }

    Testing new note form

    1. describe('Note app', function () {
    2. beforeEach(function () {
    3. cy.visit('http://localhost:3000')
    4. })
    5. // ...
    6. describe('when logged in', function () {
    7. beforeEach(function () {
    8. cy.contains('login').click()
    9. cy.get('#username').type('root')
    10. cy.get('#password').type('salainen')
    11. cy.get('#login-button').click()
    12. })
    13. it('a new note can be created', function () {
    14. cy.contains('new note').click()
    15. cy.get('input').type('a note created by cypress')
    16. cy.contains('save').click()
    17. cy.contains('a note created by cypress')
    18. })
    19. })
    20. })

    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

  1. app.js中添加test模式时运行
  2. ```javascript
  3. // ...
  4. app.use('/api/login', loginRouter)
  5. app.use('/api/users', usersRouter)
  6. app.use('/api/notes', notesRouter)
  7. if (process.env.NODE_ENV === 'test') {
  8. const testingRouter = require('./controllers/testing')
  9. app.use('/api/testing', testingRouter)
  10. }
  11. app.use(middleware.unknownEndpoint)
  12. app.use(middleware.errorHandler)
  13. module.exports = app

用命令**npm run start:test**启动后端项目
在前端测试的beforeEach中添加用户

  1. describe('Note app', function() {
  2. beforeEach(function() {
  3. cy.request('POST', 'http://localhost:3001/api/testing/reset')
  4. const user = {
  5. name: 'Matti Luukkainen',
  6. username: 'mluukkai',
  7. password: 'salainen'
  8. }
  9. cy.request('POST', 'http://localhost:3001/api/users/', user)
  10. cy.visit('http://localhost:3000')
  11. })
  12. it('front page can be opened', function() {
  13. // ...
  14. })
  15. it('user can login', function() {
  16. // ...
  17. })
  18. describe('when logged in', function() {
  19. // ...
  20. })
  21. })

测试新增note并改变重要性

  1. describe('Note app', function() {
  2. // ...
  3. describe('when logged in', function() {
  4. // ...
  5. describe('and a note exists', function () {
  6. beforeEach(function () {
  7. cy.contains('new note').click()
  8. cy.get('input').type('another note cypress')
  9. cy.contains('save').click()
  10. })
  11. it('it can be made important', function () {
  12. cy.contains('another note cypress')
  13. .contains('make important')
  14. .click()
  15. cy.contains('another note cypress')
  16. .contains('make not important')
  17. })
  18. })
  19. })
  20. })

Failed login test

如果只要运行一个测试,可以使用it.only
这里测试登录失败的例子

  1. it.only('loggin fails with wrong password', function () {
  2. cy.contains('login').click()
  3. cy.get('#username').type('mluukkai')
  4. cy.get('#password').type('wrong')
  5. cy.get('#login-button').click()
  6. cy.contains('Wrong credentials')
  7. })

使用should语法替代contains

  1. it('login fails with wrong password', function() {
  2. // ...
  3. cy.get('.error').should('contain', 'wrong credentials')
  4. })

参考文档:https://docs.cypress.io/guides/references/assertions#Common-Assertions
判断消息是否是红色边框

  1. it('login fails with wrong password', function() {
  2. // ...
  3. cy.get('.error').should('contain', 'wrong credentials')
  4. cy.get('.error').should('have.css', 'color', 'rgb(255, 0, 0)')
  5. cy.get('.error').should('have.css', 'border-style', 'solid')
  6. })

cypress的颜色要用rgb
cy.get同一个组件可以用.and连接

  1. it('login fails with wrong password', function() {
  2. // ...
  3. cy.get('.error')
  4. .should('contain', 'wrong credentials')
  5. .and('have.css', 'color', 'rgb(255, 0, 0)')
  6. .and('have.css', 'border-style', 'solid')
  7. })

Should 应当总是与get 链接(或其他某个可链接命令)
css属性访问在Firefox浏览器中会有异常

Bypassing the UI

Cypress 建议我们不要使用beforeEach 块中的表单登录用户,而是绕过 UI ,对后端执行 HTTP 请求以登录。 原因是,使用 HTTP 请求登录要比填写表单快得多。
实现后端登录并保存到localStorage

  1. describe('when logged in', function() {
  2. beforeEach(function() {
  3. cy.request('POST', 'http://localhost:3001/api/login', {
  4. username: 'mluukkai', password: 'salainen'
  5. }).then(response => {
  6. localStorage.setItem('loggedNoteappUser', JSON.stringify(response.body))
  7. cy.visit('http://localhost:3000')
  8. })
  9. })
  10. it('a new note can be created', function() {
  11. // ...
  12. })
  13. // ...
  14. })

登录代码需要在多个地方使用,可以将它写成自定义命令custom command
自定义命令在cypress/support/commands.js. 中声明

  1. Cypress.Commands.add('login', ({ username, password }) => {
  2. cy.request('POST', 'http://localhost:3001/api/login', {
  3. username, password
  4. }).then(({ body }) => {
  5. localStorage.setItem('loggedNoteappUser', JSON.stringify(body))
  6. cy.visit('http://localhost:3000')
  7. })
  8. })

使用自定义命令

  1. describe('when logged in', function() {
  2. beforeEach(function() {
  3. cy.login({ username: 'mluukkai', password: 'salainen' })
  4. })
  5. it('a new note can be created', function() {
  6. // ...
  7. })
  8. // ...
  9. })

自定义创建便签命令

  1. Cypress.Commands.add('createNote', ({ content, important }) => {
  2. cy.request({
  3. url: 'http://localhost:3001/api/notes',
  4. method: 'POST',
  5. body: { content, important },
  6. headers: {
  7. Authorization: `bearer ${JSON.parse(localStorage.getItem('loggedNoteappUser')).token}`,
  8. },
  9. })
  10. cy.visit('http://localhost:3000')
  11. })

使用命令

  1. describe('Note app', function() {
  2. // ...
  3. describe('when logged in', function() {
  4. it('a new note can be created', function() {
  5. // ...
  6. })
  7. describe('and a note exists', function () {
  8. beforeEach(function () {
  9. cy.createNote({
  10. content: 'another note cypress',
  11. important: false
  12. })
  13. })
  14. it('it can be made important', function () {
  15. // ...
  16. })
  17. })
  18. })
  19. })

Changing the importance of a note

  1. it('other of those can be made important', function () {
  2. cy.contains('second note').parent().find('button').click()
  3. cy.contains('second note').parent().find('button')
  4. .should('contain', 'make not important')
  5. })

.parent()获取父元素,.find查找子元素, cy.get是查找整个页面
为了减少重复代码,使用.as将查找的元素保存为别名,再用.get获取

  1. it('it can be made important', function () {
  2. cy.contains('second note').parent().find('button').as('theButton')
  3. cy.get('@theButton').click()
  4. cy.get('@theButton').should('contain', 'make not important')
  5. })

Running and debugging the tests

  1. const button = cy.contains('log in')
  2. button.click()
  3. debugger()
  4. cy.contains('logout').click()

cypress命令总是返回undefined, 所以上面这段代码不起作用
cypress命令类似promise,可以使用.then

  1. it('then example', function() {
  2. cy.get('button').then( buttons => {
  3. console.log('number of buttons', buttons.length)
  4. cy.wrap(buttons[0]).click()
  5. })
  6. })

使用debug让控制台暂停
https://docs.cypress.io/api/commands/debug
使用命令行运行cypress, 在npm-scripts中添加test:e2e

  1. "scripts": {
  2. // ...
  3. "cypress:open": "cypress open",
  4. "test:e2e": "cypress run"
  5. },

测试执行的视频将被保存到cypress/videos/中,应该用gitignore忽略这个目录。

cypress文档