Cypress.io如何处理异步代码

时间:2018-04-23 11:43:53

标签: javascript testing async-await cypress

我正处于将旧的水豚测试移至cypress.io的过程中,因为我们的应用程序正在以SPA方式运行。

在我们的案例中,我们有超过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.

以下是它的样子:

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

此代码段按预期工作。首先它会在之前触发并创建整个环境然后当它完成时它会进一步发展到每个并开始测试。

现在我想在之前和之前合并像

一样
  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重写它,但它也不会起作用......

任何帮助表示感谢。

6 个答案:

答案 0 :(得分:6)

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

不过,您可以利用它来代替代码

describe('Test Case', () => {
  (async () => {
     cy.visit('/')
     await something();
  })()
})

你可以写

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

这应该适用于每一个赛普拉斯命令

答案 1 :(得分:3)

你的问题源于cypress commands are not promises,尽管表现得像承诺。

我可以想到两个选择:

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

    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命令视为await代码before中的rs = dbSendQuery(con, query) totRow = 0 resultDF = data.frame() while (!dbHasCompleted(rs)) { chunk <- dbFetch(rs, 4000) resultDF<-rbind(resultDF, chunk) totRow = totRow + nrow(chunk) #Status Print if(totRow %% 100000 == 0){ print(paste(OutFile, format(totRow, scientific=F))) } } dbClearResult(rs) 承诺(请参阅此article中的详细信息)。

答案 2 :(得分:1)

我在it / test块中有关于 async / await 的类似问题。我通过将主体包裹在 async IIFE 中来解决了我的问题:

describe('Test Case', () => {
  (async () => {
     // expressions here
  })()
})

答案 3 :(得分:1)

我将分享我的方法,因为我在编写涉及大量 AWS 开发工具包调用(所有承诺)的测试时遇到了大量麻烦。我提出的解决方案提供了良好的日志记录和错误处理,而且似乎解决了我遇到的所有问题。

以下是它提供的内容的摘要:

  • 一种包装惰性承诺并在赛普拉斯可链中调用承诺的方法
  • 提供给该方法的别名将出现在 UI 的 Cypress 命令面板中。当执行开始、完成或失败时,它也会被记录到控制台。错误将整齐地显示在 Cypress 命令面板中,而不是丢失(如果您在 before/after 挂钩中运行异步函数可能会发生)或仅出现在控制台中。
  • 使用 cypress-terminal-report,日志有望从浏览器复制到标准输出,这意味着您将拥有在 CI/CD 设置中调试测试所需的所有信息,在该设置之后浏览器日志将丢失运行
  • 作为一个不相关的奖励,我分享了我的 cylog 方法,它可以做两件事:
    • 登录赛普拉斯命令面板消息
    • 使用 Cypress 任务将消息记录到标准输出,该任务与 Node 一起执行,而不是在浏览器中执行。我可以登录浏览器并依靠 cypress-terminal-report 来记录它,但它 doesn't always log when errors occur in a before hook,所以我更喜欢在可能的情况下使用 Node。

希望这不会让您不知所措并且有用!

/**
 * 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 中需要一些额外的更改:

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

import installLogsCollector from 'cypress-terminal-report/src/installLogsCollector'

installLogsCollector({})

答案 4 :(得分:0)

您可以将 Promiseawait 关键字一起使用。并在 w3schools 上查找更多信息:https://www.w3schools.com/js/js_promise.asp

  • 这对我很有帮助
// {bidderCreationRequest} was declared earlier

function createBidderObject() {
  const bidderJson = {};
  await new Promise((generateBidderObject) => {
    cy.request(bidderCreationRequest).then((bidderCreationResp) => {
      bidderJson.id = bidderDMCreationResp.body.id;

      generateBidderObject(bidderJson);
    });
  });

  return bidderJson.id
}

createBidderObject(); // returns the id of the recently created bidder instead of undefined/null

您也可以使用 https://github.com/NicholasBoll/cypress-promise#readme,因为 cy 命令又不是 Promise。因此,如果您将 async/await 与使用本机 Promise 函数或提到的插件一起使用,您会很幸运

答案 5 :(得分:0)

我使用以下代码片段来确保在执行下一个 cypress 命令之前在 cypress 中执行异步函数:

cy.wrap(null).then(() => myAsyncFunction());

示例:

function sleep(milliseconds) {
    return new Promise((resolve) => setTimeout(resolve, milliseconds));
}

async function asyncFunction1() {
    console.log('started asyncFunction1');
    await sleep(3000);
    console.log('finalized asyncFunction1');
}

async function asyncFunction2() {
    console.log('started asyncFunction2');
    await sleep(3000);
    console.log('finalized asyncFunction2');
}

describe('Async functions', () => {
    it('should be executed in sequence', () => {
        cy.wrap(null).then(() => asyncFunction1());
        cy.wrap(null).then(() => asyncFunction2());
    });
});

导致以下输出:

started asyncFunction1
finalized asyncFunction1
started asyncFunction2
finalized asyncFunction2