Cypress.io 如何处理异步代码

Posted

技术标签:

【中文标题】Cypress.io 如何处理异步代码【英文标题】:Cypress.io How to handle async code 【发布时间】:2018-10-03 10:53:14 【问题描述】:

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

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

在开始时,我写了 cypress 进入低谷页面并单击所有内容的案例。它有效,但我看到提供创建 + 发布需要将近 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
      )
    )

这个 sn-p 按预期工作。首先它在 beforeEach 之前触发并创建整个 env,然后当它完成后,它会进一步到 beforeEach 并开始测试。

现在我想合并 before 和 beforeEach 喜欢

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

任何帮助表示赞赏。

【问题讨论】:

【参考方案1】:

你的问题源于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。这个库可以帮助您将 cypress 命令视为可以在您的 before 代码中 await 的承诺(在此 article 中了解更多信息)。

【讨论】:

完美!非常感谢,明天测试一下! 不客气!这个主题github.com/cypress-io/cypress/issues/595 也可以帮助您了解如何使用 cy.server 和 cy.route。 @GuilhermeLemmi 我有同样的问题,发现 cy.route() 仅适用于从应用程序触发的请求。是否也有任何 cypress 工具来等待测试中触发的请求?【参考方案2】:

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

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

【讨论】:

async 可能有问题!赛普拉斯可能会stop at JS promises,并且测试看起来他们成功了,但实际上被忽略了。我建议检查测试是否真的得到处理,或者只是跳过。它也可能表现不同,例如在“交互”模式和“无头”模式下。 我已经做了一段时间这样的事情(有点不同,因为我将异步块传递给之前/之后的钩子)并且它工作了很多时间,但我经常遇到难以理解的错误花了很多时间来调试。您可能会看到奇怪和不一致的行为,以及糟糕的错误处理(例如丢失错误消息等)。如果您有兴趣,我发布了我的最终解决方案作为答案【参考方案3】:

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

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

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

你可以写

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

这应该适用于所有 Cypress 命令

【讨论】:

谢谢!真可惜,图书馆的其他一切都很好用。【参考方案4】:

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

以下是它提供的摘要:

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

【讨论】:

【参考方案5】:

您可以将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 函数或提到的插件,那么您会很幸运

【讨论】:

【参考方案6】:

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

【讨论】:

你的意思是.then(async () =&gt; await myAsyncFunction()) @Sarah 不,我刚刚编写了一个测试并将结果添加到这篇文章中,如果您使用cy.wrap(null).then(() =&gt; asyncFunction1());,它似乎确实按顺序执行代码【参考方案7】:

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

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

【讨论】:

您的答案可以通过额外的支持信息得到改进。请edit 添加更多详细信息,例如引用或文档,以便其他人可以确认您的答案是正确的。你可以找到更多关于如何写好答案的信息in the help center。【参考方案8】:

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

在我的例子中,我在 before() 钩子中调用了一个异步函数,但赛普拉斯不断抛出 OP 得到的相同警告。

赛普拉斯抱怨这段代码:

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


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

为了解决这个问题,我只是 cy.wrap()'d 异步函数和赛普拉斯现在与其他赛普拉斯命令同步运行异步函数并且没有抱怨。

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

【讨论】:

您好,感谢您的解决方案。想补充一点,文档说如果您需要同步性保证,您应该使用cy.wrap(p).then(() =&gt; ),因为默认情况下它们都是异步的,并且不能保证顺序:docs.cypress.io/api/commands/wrap#Promises

以上是关于Cypress.io 如何处理异步代码的主要内容,如果未能解决你的问题,请参考以下文章

如何处理一对多关系中的嵌套 mongoose 查询和异步问题?

如何处理异步函数 UWP App GetFileFromPathAsync(path) 中的异常;

前端知识体系:JavaScript基础-作用域和闭包-如何处理循环的异步操作

等待/异步如何处理未解决的承诺

当循环中调用了异步函数时,Node.JS 将如何处理循环控制?

Angular:如何处理异步问题?