关于javascript:Cypress.io如何处理异步代码

Cypress.io How to handle async code

由于我们的应用程序正在采用SPA方式,因此我正在将旧的水豚测试移至cypress.io。

在我们的案例中,我们有2000多个测试涵盖了许多功能。
因此,测试功能的常见模式是让用户拥有创建和发布的报价。

在开始的时候,我写了一个案例,其中柏树进入低谷页面并单击所有内容。它奏效了,但我看到要约创建发布花费了将近1.5分钟才能完成。有时我们需要多个报价。因此,我们进行了一个需要5分钟的测试,并且还有1999年可以重写。

我们提出了REST API来创建商品和用户,这基本上是准备测试环境的捷径。

我到了使用async/await一切正常的地步。所以这就是事情。如果我想在cypress上使用普通的异步JS代码,我会得到Error: Cypress detected that you returned a promise from a command while also invoking one or more cy commands in that promise.

这是它的样子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
    const faker = require('faker')
    import User from '../../support/User';

    describe('Toggle button for description offer', () => {
      const user = new User({
        first_name: faker.name.firstName(),
        last_name: faker.name.firstName(),
        email: `QA_${faker.internet.email()}`,
        password: 'xxx'
      })
      let offer = null

      before(async () => {
        await user.createOnServer()
        offer = await user.createOffer()
        await offer.publish()
      })

      beforeEach(() => {
        user.login()
        cy.visit(`/offers/${offer.details.id}`)
        cy.get('.offer-description__content button').as('showMoreButton')
      })

      it('XXX', function () {
        ...some test
      })
    })

此代码段符合预期。首先,它会在之前触发并创建整个环境,然后在完成后进一步进行操作,然后再开始测试。

现在我想合并之前和之前每个类似

1
2
3
4
5
6
7
8
  before(async () => {
    await user.createOnServer()
    offer = await user.createOffer()
    await offer.publish()
    user.login()
    cy.visit(`/offers/${offer.details.id}`)
    cy.get('.offer-description__content button').as('showMoreButton')
  })

这将由于async关键字而失败。
现在的问题是:如何将其重写以一起使用async / await和cypress命令?我试图用普通的Promise重写它,但是它也无法正常工作...

任何帮助表示赞赏。


您的问题源于赛普拉斯命令不是诺言,尽管其行为与诺言类似。

我可以想到两种选择:

  • 尝试重构测试代码以不使用异步/等待,因为在cypress上运行代码时,这些命令的行为不符合预期(请检查此bug)。赛普拉斯已经创建了处理异步代码的完整方法,因为它创建了始终按预期顺序顺序运行的命令队列。这意味着您可以在继续测试之前观察异步代码的效果,以验证它是否已发生。例如,如果User.createUserOnServer必须等待成功的API调用,则使用cy.server(),cy.route()和cy.wait()将代码添加到测试中以等待请求完成,如下所示:

    1
    2
    3
    4
    cy.server();
    cy.route('POST', '/users/').as('createUser');
    // do something to trigger your request here, like user.createOnServer()
    cy.wait('@createUser', { timeout: 10000});
  • 使用另一个第三方库来更改cypress与async / await一起工作的方式,例如cypress-promise。此lib可以帮助您将cypress命令视为可以在before代码中进行awaitPromise的Promise(在本文中了解有关此内容的更多信息)。


虽然@isotopeee的解决方案基本可行,但我确实遇到了问题,尤其是在使用wait(@alias)和紧随其后的await命令时。问题似乎在于,赛普拉斯函数返回的内部Chainable类型看起来像Promise,但不是一个。

不过,您可以利用它来发挥自己的优势,而不必编写

1
2
3
4
5
6
describe('Test Case', () => {
  (async () => {
     cy.visit('/')
     await something();
  })()
})

您可以写

1
2
3
describe('Test Case', () => {
  cy.visit('/').then(async () => await something())
})

这应与每个赛普拉斯命令一起使用


我在it / test块中存在关于异步/等待的类似问题。我通过将主体包裹在异步IIFE中来解决了我的问题:

1
2
3
4
5
describe('Test Case', () => {
  (async () => {
     // expressions here
  })()
})


我将分享我的方法,因为我在编写涉及大量AWS开发工具包调用(全部Promise)的测试时会感到头疼。我想出的解决方案提供了良好的日志记录,错误处理,似乎可以解决我遇到的所有问题。

以下是其提供的摘要:

  • 一种package懒惰Promise并在Cypress可链接的内部调用promise的方法
  • 提供给该方法的别名将出现在UI的Cypress命令面板中。执行开始,完成或失败时,也会将其记录到控制台。错误将在Cypress命令面板中整齐地显示,而不是丢失(如果在before/after挂钩中运行异步功能,则可能会发生)或仅出现在控制台中。
  • 使用cypress-terminal-report,有望将日志从浏览器复制到stdout,这意味着您将拥有在CI / CD设置中调试测试所需的所有信息,该配置会使运行后浏览器日志丢失
  • 作为无关的奖励,我分享了我的cylog方法,该方法有两件事:

    • 登录消息赛普拉斯命令面板
    • 使用Cypress任务将消息记录到stdout,该任务使用Node而不是在浏览器中执行。我可以登录浏览器并依靠cypress-terminal-report进行记录,但是当在before挂钩中发生错误时,它并不总是记录日志,因此我更喜欢在可能的情况下使用Node。

希望这不会让您不知所措,而且很有用!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
/**
 * Work around for making some asynchronous operations look synchronous, or using their output in a proper Cypress
 * {@link Chainable}. Use sparingly, only suitable for things that have to be asynchronous, like AWS SDK call.
 */

export function cyasync< T >(alias: string, promise: () => Promise< T >, timeout?: Duration): Chainable< T > {
    const options = timeout ? { timeout: timeout.toMillis() } : {}
    return cy
        .wrap(null)
        .as(alias)
        .then(options, async () => {
            try {
                asyncLog(`Running async task"${alias}"`)
               
                const start = Instant.now()
                const result = await promise()
                const duration = Duration.between(start, Instant.now())
               
                asyncLog(`Successfully executed task"${alias}" in ${duration}`)
                return result
            } catch (e) {
                const message = `Failed"${alias}" due to ${Logger.formatError(e)}`
                asyncLog(message, Level.ERROR)
                throw new Error(message)
            }
        })
}

/**
 * Logs both to the console (in Node mode, so appears in the CLI/Hydra logs) and as a Cypress message
 * (appears in Cypress UI) for easy debugging. WARNING: do not call this method from an async piece of code.
 * Use {@link asyncLog} instead.
 */

export function cylog(message: string, level: Level = Level.INFO) {
    const formatted = formatMessage(message, level)
    cy.log(formatted)
    cy.task('log', { level, message: formatted }, { log: false })
}

/**
 * When calling from an async method (which you should reconsider anyway, and avoid most of the time),
 * use this method to perform a simple console log, since Cypress operations behave badly in promises.
 */

export function asyncLog(message: string, level: Level = Level.INFO) {
    getLogger(level)(formatMessage(message, level))
}

对于日志记录,plugins/index.js中需要一些其他更改:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
modules.export = (on, config) => {
    setUpLogging(on)
    // rest of your setup...
}

function setUpLogging(on) {
    // this task executes Node code as opposed to running in the browser. This thus allows writing out to the console/Hydra
    // logs as opposed to inside of the browser.
    on('task', {
        log(event) {
            getLogger(event.level)(event.message);
            return null;
        },
    });

    // best-effort attempt at logging Cypress commands and browser logs
    // https://www.npmjs.com/package/cypress-terminal-report
    require('cypress-terminal-report/src/installLogsPrinter')(on, {
        printLogsToConsole: 'always'
    })
}

function getLogger(level) {
    switch (level) {
        case 'info':
            return console.log
        case 'error':
            return console.error
        case 'warn':
            return console.warn
        default:
            throw Error('Unrecognized log level: ' + level)
    }
}

support/index.ts

1
2
3
import installLogsCollector from 'cypress-terminal-report/src/installLogsCollector'

installLogsCollector({})