跳到内容

d

端到端测试

到目前为止,我们已经使用集成测试在API层面上测试了整个后端,并使用单元测试测试了一些前端组件。

接下来我们将研究一种方法,使用端到端(E2E)测试系统整体

我们可以使用浏览器和测试库对网络应用进行E2E测试。有多种库可用,例如Selenium,几乎可以与任何浏览器一起使用。

另一个浏览器选项是所谓的无头浏览器,它是没有图形用户界面的浏览器。

例如,Chrome可以在无头模式下使用。

E2E测试有可能是最有用的测试类别,因为它们通过与真实用户使用的相同界面来测试系统。

它们也有一些缺点。配置E2E测试比单元或集成测试更具挑战性。他们也倾向于相当慢,对于一个大系统,他们的执行时间可能是几分钟,甚至几小时。这对开发是不利的,因为在编码过程中,如果出现代码回归,能够尽可能频繁地运行测试是有益的。

E2E测试也可能是不稳定

有些测试可能一次通过,另一次失败,即使代码根本没有变化。

Cypress

E2E库Cypress在去年开始流行。Cypress特别容易使用,与Selenium等相比,它需要的麻烦和头绪要少得多。

它的操作原理与大多数E2E测试库完全不同,因为Cypress测试完全在浏览器中运行。

其他库在一个Node进程中运行测试,该进程通过API与浏览器相连。

让我们为我们的笔记应用做一些端到端的测试。

我们首先将Cypress安装到前端作为开发依赖项

npm install --save-dev cypress

并添加一个npm-script来运行它。

{
  // ...
  "scripts": {
    "start": "react-scripts start",
    "build": "react-scripts build",
    "test": "react-scripts test",
    "eject": "react-scripts eject",
    "server": "json-server -p3001 db.json",
    "cypress:open": "cypress open"  },
  // ...
}

与前端的单元测试不同,Cypress的测试可以在前端或后端仓库中,甚至可以在它们自己的独立仓库中。

测试需要被测系统正在运行。与我们的后端集成测试不同,Cypress测试在运行时不会启动系统。

让我们给后端添加一个npm-script,在测试模式下启动它,或者让NODE_ENV成为测试

{
  // ...
  "scripts": {
    "start": "cross-env NODE_ENV=production node index.js",
    "dev": "cross-env NODE_ENV=development nodemon index.js",
    "build:ui": "rm -rf build && cd ../../../2/luento/notes && npm run build && cp -r build ../../../3/luento/notes-backend",
    "deploy": "git push heroku master",
    "deploy:full": "npm run build:ui && git add . && git commit -m uibuild && git push && npm run deploy",
    "logs:prod": "heroku logs --tail",
    "lint": "eslint .",
    "test": "cross-env NODE_ENV=test jest --verbose --runInBand",
    "start:test": "cross-env NODE_ENV=test node index.js"  },
  // ...
}

NB!为了让Cypress与WSL2一起工作,可能需要先做一些额外的配置。这两个链接开始的好地方。

NB!对于使用m1 CPU而不是intel CPU的macbook,cypress不能工作,因为它还不支持m1。要解决这个问题,安装Rosetta 2然后配置你的终端是必须的。关于一步一步的说明,请按照这里

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

npm run cypress:open

当我们第一次运行Cypress时,它会创建一个cypress目录。它包含一个integration子目录,我们将在那里放置我们的测试。Cypress在两个子目录中为我们创建了一堆测试示例:integration/1-getting-startedintegration/2-advanced-examples目录。我们可以删除这两个目录,在文件note_app.spec.js中做我们自己的测试。

describe('Note app', function() {
  it('front page can be opened', function() {
    cy.visit('http://localhost:3000')
    cy.contains('Notes')
    cy.contains('Note app, Department of Computer Science, University of Helsinki 2022')
  })
})

我们从打开的窗口开始进行测试。

fullstack content

注意:删除示例测试后,你可能需要重新启动Cypress。

运行测试会打开你的浏览器,并显示应用在运行测试时的表现。

fullstack content

测试的结构应该看起来很熟悉。他们使用describe块来分组不同的测试用例,像Jest那样。测试用例已经用it方法进行了定义。

Cypress从它在引擎盖下使用的Mocha测试库中借用了这些部分。

cy.visitcy.contains是Cypress的命令,它们的目的非常明显。

cy.visit在测试所使用的浏览器中打开作为参数给它的网页地址。cy.contains搜索它从网页上收到的作为参数的字符串。

我们可以用一个箭头函数来声明这个测试

describe('Note app', () => {  it('front page can be opened', () => {    cy.visit('http://localhost:3000')
    cy.contains('Notes')
    cy.contains('Note app, Department of Computer Science, University of Helsinki 2022')
  })
})

然而,Mocha建议不要使用箭头函数,因为它们在某些情况下可能导致一些问题。

如果cy.contains没有找到它要搜索的文本,测试就不会通过。 因此,如果我们像这样扩展我们的测试

describe('Note app', function() {
  it('front page can be opened',  function() {
    cy.visit('http://localhost:3000')
    cy.contains('Notes')
    cy.contains('Note app, Department of Computer Science, University of Helsinki 2022')
  })

  it('front page contains random text', function() {    cy.visit('http://localhost:3000')    cy.contains('wtf is this app?')  })})

测试失败

fullstack content

让我们从测试中删除失败的代码。

Writing to a form

让我们扩展我们的测试,使测试尝试登录到我们的应用。

我们假设我们的后端包含一个用户名mluukkai和密码salainen的用户。

测试从打开登录表单开始。

describe('Note app',  function() {
  // ...

  it('login form can be opened', function() {
    cy.visit('http://localhost:3000')
    cy.contains('login').click()
  })
})

测试首先通过文本搜索登录按钮,并通过命令cy.click点击按钮。

我们的两个测试都是以同样的方式开始的,打开http://localhost:3000页面,所以我们应该

将共享部分分离成一个beforeEach块,在每个测试前运行。

describe('Note app', function() {
  beforeEach(function() {    cy.visit('http://localhost:3000')  })
  it('front page can be opened', function() {
    cy.contains('Notes')
    cy.contains('Note app, Department of Computer Science, University of Helsinki 2022')
  })

  it('login form can be opened', function() {
    cy.contains('login').click()
  })
})

登录字段包含两个输入字段,测试应该把它们写进。

cy.get命令允许通过CSS选择器搜索元素。

我们可以访问页面上的第一个和最后一个输入字段,并通过cy.type命令写入它们,就像这样。

it('user can login', function () {
  cy.contains('login').click()
  cy.get('input:first').type('mluukkai')
  cy.get('input:last').type('salainen')
})

这个测试是有效的。问题是,如果我们以后添加更多的输入字段,测试就会中断,因为它期望它需要的字段是页面上的第一个和最后一个。

最好是给我们的输入以唯一的ids,并使用这些来找到它们。

我们像这样改变我们的登录表格。

const LoginForm = ({ ... }) => {
  return (
    <div>
      <h2>Login</h2>
      <form onSubmit={handleSubmit}>
        <div>
          username
          <input
            id='username'            value={username}
            onChange={handleUsernameChange}
          />
        </div>
        <div>
          password
          <input
            id='password'            type="password"
            value={password}
            onChange={handlePasswordChange}
          />
        </div>
        <button id="login-button" type="submit">          login
        </button>
      </form>
    </div>
  )
}

我们还为我们的提交按钮添加了一个ID,这样我们就可以在测试中访问它。

测试变成了。

describe('Note app',  function() {
  // ..
  it('user can log in', function() {
    cy.contains('login').click()
    cy.get('#username').type('mluukkai')    cy.get('#password').type('salainen')    cy.get('#login-button').click()
    cy.contains('Matti Luukkainen logged in')  })
})

最后一行确保登录成功。

注意CSS的id-selector是#,所以如果我们想搜索一个id为username的元素,CSS选择器是#username

Some things to note

测试首先点击按钮,打开登录表单,就像这样

cy.contains('login').click()

当表格填写完毕后,点击提交按钮,表格被提交。

cy.get('#login-button').click()

两个按钮都有文本login,但它们是两个独立的按钮。

实际上两个按钮一直都在应用的DOM中,但由于其中一个按钮的display:none样式,每次只有一个是可见的。

如果我们通过文本搜索一个按钮,cy.contains将返回其中的第一个,或打开登录表单的那个。

即使该按钮不可见,也会发生这种情况。

为了避免名称冲突,我们给了提交按钮一个id login-button,我们可以用它来访问。

现在我们注意到,我们测试使用的变量cy给了我们一个讨厌的Eslint错误

fullstack content

我们可以通过安装eslint-plugin-cypress作为开发依赖来摆脱它。

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

然后改变.eslintrc.js中的配置,像这样。

module.exports = {
    "env": {
        "browser": true,
        "es6": true,
        "jest/globals": true,
        "cypress/globals": true    },
    "extends": [
      // ...
    ],
    "parserOptions": {
      // ...
    },
    "plugins": [
        "react", "jest", "cypress"    ],
    "rules": {
      // ...
    }
}

Testing new note form

接下来我们添加测试 "新笔记 "功能的测试。

describe('Note app', function() {
  // ..
  describe('when logged in', function() {    beforeEach(function() {      cy.contains('login').click()      cy.get('input:first').type('mluukkai')      cy.get('input:last').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')    })  })})

这个测试已经被定义在它自己的describe块中。

只有登录的用户才能创建新的笔记,所以我们在beforeEach块中加入了登录应用。

测试相信当创建一个新的笔记时,页面只包含一个输入,所以它像这样搜索它。

cy.get('input')

如果该页面包含更多的输入,测试就会中断。

fullstack content

由于这个原因,最好还是给输入一个id,并通过它的id搜索元素。

测试的结构看起来是这样的。

describe('Note app', function() {
  // ...

  it('user can log in', function() {
    cy.contains('login').click()
    cy.get('#username').type('mluukkai')
    cy.get('#password').type('salainen')
    cy.get('#login-button').click()

    cy.contains('Matti Luukkainen logged in')
  })

  describe('when logged in', function() {
    beforeEach(function() {
      cy.contains('login').click()
      cy.get('input:first').type('mluukkai')
      cy.get('input:last').type('salainen')
      cy.get('#login-button').click()
    })

    it('a new note can be created', function() {
      // ...
    })
  })
})

Cypress按照代码中的顺序运行测试。因此,首先它运行user can log in,其中用户登录。然后,Cypress将运行一个新的笔记可以被创建,为此,一个beforeEach块也会登录。

为什么这样做?用户在第一次测试后不是已经登录了吗?

不是,因为就浏览器而言,每个测试都是从零开始的。

每次测试后,对浏览器状态的所有改变都是相反的。

Controlling the state of the database

如果测试需要能够修改服务器的数据库,情况会立即变得更加复杂。理想情况下,每次我们运行测试时,服务器的数据库应该是相同的,所以我们的测试可以可靠地、容易地重复。

与单元和集成测试一样,对于E2E测试,最好是在测试运行前清空数据库,并可能将其格式化。E2E测试的挑战是他们不能访问数据库。

解决方法是为测试的后端创建API端点。

我们可以使用这些端点清空数据库。

让我们为测试创建一个新的路由器

const testingRouter = require('express').Router()
const Note = require('../models/note')
const User = require('../models/user')

testingRouter.post('/reset', async (request, response) => {
  await Note.deleteMany({})
  await User.deleteMany({})

  response.status(204).end()
})

module.exports = testingRouter

并将其添加到后端 如果应用在测试模式下运行

// ...

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

更改后,对/api/testing/reset端点的HTTP POST请求会清空数据库。确保你的后端在测试模式下运行,用这个命令启动它(之前在package.json文件中配置)。

  npm run start:test

修改后的后端代码可以在GitHub分支part5-1找到。

接下来我们将修改beforeEach块,以便在运行测试之前清空服务器的数据库。

目前不可能通过前端的用户界面添加新用户,所以我们从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() {
    // ...
  })
})

在格式化过程中,测试用cy.request向后端做HTTP请求。

与先前不同,现在测试开始时,后端每次都处于相同的状态。后端将包含一个用户,没有注释。

让我们再增加一个测试,检查我们是否可以改变笔记的重要性。

首先我们改变前端,使新的笔记默认为不重要,或者重要字段为false

const NoteForm = ({ createNote }) => {
  // ...

  const addNote = (event) => {
    event.preventDefault()
    createNote({
      content: newNote,
      important: false    })

    setNewNote('')
  }
  // ...
}

有多种方法来测试。在下面的例子中,我们首先搜索一个笔记,并点击其重要按钮。然后我们检查该笔记现在是否包含一个使之不重要按钮。

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')
      })
    })
  })
})

第一条命令搜索一个包含另一个笔记cypress文本的组件,然后搜索其中的make important按钮。然后它就点击这个按钮。

第二个命令检查按钮上的文字是否已经变成了使之不重要

测试和当前的前端代码可以从GitHub分支part5-9找到。

Failed login test

让我们做一个测试,确保在密码错误的情况下,登录尝试失败。

Cypress默认每次都会运行所有测试,随着测试数量的增加,它开始变得相当耗时。

当开发一个新的测试或调试一个损坏的测试时,我们可以用it.only代替it来定义测试,这样Cypress将只运行所需的测试。

当测试正常时,我们可以删除.only

我们测试的第一个版本如下。

describe('Note app', function() {
  // ...

  it.only('login 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')
  })

  // ...
)}

测试使用cy.contains来确保应用打印出错误信息。

应用将错误信息渲染到一个具有CSS类error的组件。

const Notification = ({ message }) => {
  if (message === null) {
    return null
  }

  return (
    <div className="error">      {message}
    </div>
  )
}

我们可以让测试确保错误信息被渲染到正确的组件,也就是具有CSS类error的组件。

it('login fails with wrong password', function() {
  // ...

  cy.get('.error').contains('wrong credentials')})

首先我们使用cy.get来搜索一个具有CSS类error的组件。然后我们检查是否可以从这个组件中找到错误信息。

注意CSS类选择器以句号开头,所以error类的选择器是.error

我们可以用should的语法做同样的事情。

it('login fails with wrong password', function() {
  // ...

  cy.get('.error').should('contain', 'wrong credentials')})

使用should比使用contains要麻烦一些,但它允许比contains更多样化的测试,后者只基于文本内容工作。

可以和should一起使用的最常见的断言列表可以在这里找到。

例如,我们可以确保错误信息是红色的,并且有一个边框。

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')
})

让我们完成这个测试,以便它也能检查应用是否渲染成功信息 "Matti Luukkainen logged in"

it('login 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.get('.error')
    .should('contain', 'wrong credentials')
    .and('have.css', 'color', 'rgb(255, 0, 0)')
    .and('have.css', 'border-style', 'solid')

  cy.get('html').should('not.contain', 'Matti Luukkainen logged in')})

Should should always be chained with get (or another chainable command).

我们使用cy.get("html")来访问应用的整个可见内容。

注意:一些CSS属性在Firefox上的表现不同。如果你用Firefox运行测试。

running

那么涉及到 "border-style"、"border-radius "和 "padding "的测试,在Chrome或Electron上会通过,但在Firefox上会失败。

borderstyle

Bypassing the UI

目前我们有以下的测试。

describe('Note app', function() {
  it('user can login', function() {
    cy.contains('login').click()
    cy.get('#username').type('mluukkai')
    cy.get('#password').type('salainen')
    cy.get('#login-button').click()

    cy.contains('Matti Luukkainen logged in')
  })

  it('login fails with wrong password', function() {
    // ...
  })

  describe('when logged in', function() {
    beforeEach(function() {
      cy.contains('login').click()
      cy.get('input:first').type('mluukkai')
      cy.get('input:last').type('salainen')
      cy.get('#login-button').click()
    })

    it('a new note can be created', function() {
      // ...
    })

  })
})

首先我们测试登录。然后,在他们自己的描述块中,我们有一系列的测试,期望用户能够登录。用户在beforeEach块中被登录。

正如我们上面所说的,每个测试都是从零开始的!测试不会从之前测试结束的状态开始。

Cypress文档给了我们以下建议。完全测试登录流程--但只测试一次!

因此,Cypress建议我们不要在beforeEach块中使用表单来登录用户,而是绕过UI,向后端发出HTTP请求来登录。这样做的原因是,用HTTP请求登录要比填表快得多。

我们的情况比Cypress文档中的例子要复杂一些,因为当用户登录时,我们的应用会将他们的详细信息保存到localStorage中。

然而,Cypress也可以处理这个问题。

代码如下

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() {
    // ...
  })

  // ...
})

我们可以用then方法访问cy.request的响应。 在引擎盖下cy.request,像所有的Cypress命令一样,是promises

回调函数将登录用户的详细信息保存到localStorage,并重新加载页面。

现在与用户用登录表格登录没有区别。

如果我们给我们的应用写新的测试,我们必须在多个地方使用登录代码。

我们应该把它变成一个自定义命令

自定义命令在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() {
    // ...
  })

  // ...
})

现在我们想想,这同样适用于创建一个新的笔记。我们有一个测试,使用表单制作一个新的笔记。我们也在测试改变笔记的重要性的beforeEach块中制作一个新的笔记。

describe('Note app', function() {
  // ...

  describe('when logged in', function() {
    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')
    })

    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 () {
        // ...
      })
    })
  })
})

让我们制作一个新的自定义命令来制作一个新的注释。该命令将通过HTTP POST请求制作一个新的笔记。

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')
})

该命令希望用户已经登录,并且用户的详细信息被保存到localStorage。

现在格式化块变成。

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 () {
        // ...
      })
    })
  })
})

测试和前端代码可以从GitHub分支part5-10找到。

Changing the importance of a note

最后让我们来看看我们为改变笔记的重要性所做的测试。

首先,我们要改变格式化块,使其创建三个注释而不是一个。

describe('when logged in', function() {
  describe('and several notes exist', function () {
    beforeEach(function () {
      cy.createNote({ content: 'first note', important: false })      cy.createNote({ content: 'second note', important: false })      cy.createNote({ content: 'third note', important: false })    })

    it('one of those can be made important', function () {
      cy.contains('second note')
        .contains('make important')
        .click()

      cy.contains('second note')
        .contains('make not important')
    })
  })
})

cy.contains命令实际上是如何工作的?

当我们在Cypress Test Runner中点击cy.contains("second note")命令时,我们看到该命令在搜索包含文本second note的元素。

fullstack content

通过点击下一行.contains("make important")我们看到该测试使用了

对应于第二个笔记的"使重要"按钮。

fullstack content

当连锁时,第二个contains命令继续从第一个命令找到的组件中进行搜索。

如果我们没有将这些命令连接起来,而是写成:

cy.contains('second note')
cy.contains('make important').click()

结果就会完全不同。测试的第二行会点击一个错误的笔记的按钮。

fullstack content

在编写测试代码时,你应该在测试运行器中检查测试是否使用了正确的组件!

让我们改变Note组件,使笔记的文本被渲染成span

const Note = ({ note, toggleImportance }) => {
  const label = note.important
    ? 'make not important' : 'make important'

  return (
    <li className='note'>
      <span>{note.content}</span>      <button onClick={toggleImportance}>{label}</button>
    </li>
  )
}

我们的测试失败了!正如测试运行器所显示的,cy.contains("second note")现在返回包含文本的组件,而按钮不在其中。

fullstack content

解决这个问题的一个方法是如下。

it('one 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命令来访问包含second note的元素的父元素,并从其中找到按钮。

然后我们点击按钮,并检查上面的文字是否改变。

注意,我们使用命令find来搜索按钮。我们不能在这里使用cy.get,因为它总是从整个页面搜索,并且会返回页面上的所有5个按钮。

不幸的是,我们现在有一些复制粘贴的测试,因为搜索右边按钮的代码总是相同的。

在这种情况下,可以使用as命令。

it('one of those 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')
})

现在第一行找到了右边的按钮,并使用as将其保存为theButton。下面几行可以用cy.get("@theButton")来使用这个命名的元素。

Running and debugging the tests

最后,关于Cypress如何工作和调试你的测试的一些说明。

Cypress测试的形式给人的印象是测试是正常的JavaScript代码,例如我们可以这样尝试。

const button = cy.contains('login')
button.click()
debugger()
cy.contains('logout').click()

但这并不可行。当Cypress运行一个测试时,它将每个cy命令添加到一个执行队列中。

当测试方法的代码被执行后,Cypress将逐一执行队列中的每个命令。

Cypress命令总是返回undefined,所以上述代码中的button.click()会导致一个错误。试图启动调试器不会在执行命令之间停止代码,而是在任何命令被执行之前。

Cypress命令类似于 promise ,所以如果我们想访问它们的返回值,我们必须使用then命令来完成。

例如,下面的测试将打印应用中的按钮数量,并点击第一个按钮。

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

用调试器停止测试的执行是可能的。只有当Cypress test runner's developer console打开时,调试器才会启动。

当调试你的测试时,开发者控制台是各种有用的。

你可以在网络标签上看到测试所做的HTTP请求,控制台标签将显示你的测试信息。

fullstack content

到目前为止,我们使用图形化的测试运行器运行我们的Cypress测试。

也可以从命令行运行它们。我们只需要为它添加一个npm脚本。

  "scripts": {
    "start": "react-scripts start",
    "build": "react-scripts build",
    "test": "react-scripts test",
    "eject": "react-scripts eject",
    "server": "json-server -p3001 --watch db.json",
    "cypress:open": "cypress open",
    "test:e2e": "cypress run"  },

现在我们可以用命令npm run test:e2e从命令行运行我们的测试。

fullstack content

注意,测试执行的视频将被保存到cypress/videos/,所以你可能应该git忽略这个目录。

前端和测试代码可以从GitHub分支part5-11找到。