Cypress.io 如何处理异步代码

Cypress.io How to handle async code

我正在将我们的旧水豚测试转移到 cypress.io,因为我们的应用程序正在采用 SPA 方式。

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

一开始我写了柏树浏览页面并点击所有内容的案例。它有效,但我看到报价创建 + 发布花了将近 1.5 分钟才能完成。有时我们需要多个报价。所以我们有一个测试需要 5 分钟,我们还有 1999 年要重写。

我们想出了 REST API 来创建报价和用户,基本上是测试环境准备的捷径。

我到了使用 async/await 一切正常的地步。所以这就是事情。如果我想在柏树上使用普通的异步 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
      })
    })

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

现在我想合并before和beforeEach like

  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 commands are not promises,虽然表现得像承诺。

我能想到两个方案:

  • 尝试重构您的测试代码以不使用 async/await,因为当 运行 您的代码在赛普拉斯上时,这些命令的行为不如预期(检查此 bug). Cypress already has a whole way of dealing with async code as it creates a command queue that always run sequentially and in the expected order. That means you could observe the effects of your async code to validate that it happened before moving forward on your test. For instance, if User.createUserOnServer must wait a successful API call, add code to your test that will wait for the request to complete, using 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. This lib may help you to treat cypress commands as promises that you can await in your before code (read more about it in this article).

关于 it / test 块内的 async/await 我遇到了类似的问题。我通过将主体包裹在 async IIFE:

中解决了我的问题
describe('Test Case', () => {
  (async () => {
     // expressions here
  })()
})

虽然 @isotopeee 的解决方案基本上有效,但我确实 运行 遇到了问题,尤其是在使用 wait(@alias) 和之后的 await 命令时。问题似乎是,Cypress 函数 return 一个看起来像 Promise 但不是 Promise 的内部 Chainable 类型。

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

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

你可以写

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

这应该适用于每个 Cypress 命令

我将分享我的方法,因为我在编写涉及大量 AWS SDK 调用(所有承诺)的测试时非常头疼。我想出的解决方案提供了良好的日志记录、错误处理,似乎解决了我遇到的所有问题。

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

  • 一种包装惰性 promise 并在 Cypress chainable 中调用该 promise 的方法
  • 提供给该方法的别名将出现在 UI 中的赛普拉斯命令面板中。当执行开始、完成或失败时,它也会被记录到控制台。错误将整齐地显示在 Cypress 命令面板中,而不是丢失(如果您 运行 在 before/after 挂钩中使用异步功能,可能会发生错误)或仅出现在控制台中。
  • 使用 cypress-terminal-report,日志应该从浏览器复制到标准输出,这意味着您将在浏览器所在的 CI/CD 设置中获得调试测试所需的所有信息运行
  • 后日志丢失
  • 作为一个不相关的奖励,我分享了我的 cylog 方法,它做了两件事:
    • 在消息中记录 Cypress 命令面板
    • 使用 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({})

您可以将 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命令又不是Promises。因此,如果您将 async/await 与本机 Promise 函数或提到的插件

一起使用,您会很幸运

我正在使用以下代码片段来确保在执行下一个 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

这是另一个更简洁的解决方法:

// an modified version of `it` that doesn't produce promise warning
function itAsync(name, callback) {
  it(name, wrap(callback))
}

function wrap(asyncCallback) {
  const result = () => {
    // don't return the promise produced by async callback
    asyncCallback()
  }
  return result
}

itAsync('foo', async () => {
  await foo()
  assert.equal('hello', 'hello')
})

我遇到了与 OP 完全相同的问题,我想我会分享我正在使用的 Timation 答案的简化版本。我在赛普拉斯 8.0.0 版中对此进行了测试。

在我的例子中,我在 before() 挂钩中调用了一个异步函数,但是 cypress 不断抛出相同的警告 OP。

赛普拉斯抱怨这段代码:

// Bad Code
const setupTests = async () => {
  await myAsyncLibraryCall();
}

before(async () => {
  await setupTests();
  cy.login();
});

为了修复它,我只是 cy.wrap() 了异步函数,现在 cypress 与其他 Cypress 命令同步运行异步函数并且没有抱怨。

// Good Code
before(() => {
  cy.wrap(setupTests());
  cy.login();
});

将异步代码放入cy.then():

  before(() => {
    cy.then(async () => {
      await user.createOnServer()
      offer = await user.createOffer()
      await offer.publish()
      user.login()
      cy.visit(`/offers/${offer.details.id}`)
    })

    // This line can be outside `cy.then` because it does not
    // use any variables created or set inside `cy.then`.
    cy.get('.offer-description__content button').as('showMoreButton')
  })