赛普拉斯 - 向页面对象添加重试

Cypress - adding retries to page object

为了方便以后的修改,我们将登录脚本放在了一个页面对象中。

//Login.js

export class Login {

username_input=         () => cy.get('#LoginForm_username');
password_input=         () => cy.get('#LoginForm_password');
login_button=           () => cy.contains('Login');
profile=                () => cy.get('.profile-content');

login(email, password){
    this.username_input().type(email);
    this.password_input().type(password);
    this.login_button().click();
    this.profile().should('exist')
        return this;
    }
}

所以稍后我们可以在任何规范文件中重用它

//actual.spec.js

import {Login} from "../../pages/login/Login";

it('logs in', () => {
login.login(Cypress.env('userEmail'), Cypress.env('userPass'))
}

现在我们的登录页面出现了有时没有响应的奇怪行为。因此我们添加了重试。

我们找到了两种可行的方法,但都不理想:

解决方案一:

将重试参数放入配置文件

"retries": 2

为什么这不理想?

这会为每一个测试启用重试,这是我们不想要的。我们只需要它用于登录脚本。

方案二:

将重试参数放入 'it'

import {Login} from "../../pages/login/Login";

it('logs in', {retries:2} () => {
login.login(Cypress.env('userEmail'), Cypress.env('userPass'))
}

为什么这不理想?

我们必须将参数放入每个规范文件中,如果我们想要更改重试次数或完全取消重试,我们需要在每个规范文件中进行更改。

解法三???

我现在正在寻找的是一种将重试参数放在 login.js 的登录功能中某处的方法,但我找不到这样做的方法。

(编辑 post 以包含第二个解决方法...)

cypress 的整个想法是避免不稳定的测试。如果您的登录有时会失败,有时不会,那么您的环境甚至在您开始之前就已经不稳定了。

  1. 如果您所有的测试都依赖于登录,那么在套件级别(在描述中)包含重试可能不是一个坏主意,但如果您只有一些测试依赖于登录,那么您可以将它们全部分组仅在一个套件中,然后在套件级别添加重试。

有点像..


describe('Your suite name', {
  retries: {
    runMode: 2,
    openMode: 2,
  }
}, () => {
 
//number of retries will be applied per test
  it('logs in', () => {
    login.login(Cypress.env('userEmail'), Cypress.env('userPass'))
  })
  
  it('other test', () => {
})

})
  1. 通过 API 进行登录,这使它成为依赖于登录的测试的一部分(因此它不必通过 UI 并且您不会得到问题)。当测试登录本身时,仅在那里,重试该测试。

当你写:

Now our login page has the strange behavior of sometimes not responding.

你放心,你的测试没有问题。

更多想法:

即时重试更改

TLDR
优点:简单
缺点:使用内部命令

有一个内部命令可以让您在测试中更改重试次数。

我强调内部这个词是为了警告它可能会在某些新版本中停止工作。

用你的代码

export class Login {
  ...

  login(email, password) {

    const originalRetries = cy.state('runnable')._retries

    cy.state('runnable')._retries = 5    // WARNING - internal command
    
    this.username_input().type(email);
    ...

    cy.state('runnable')._retries = originalRetries  // restore configured retries value
  }
}

递归重试

TLDR
好处:适用于不稳定的情况
缺点:需要显式等待

如果你想要更长但更主流的解决方案,请使用递归。

您将需要一个非失败检查来处理重试或完成逻辑,这实际上意味着将 cy.get('.profile-content').should('exist') 更改为 jQuery 等效项并使用显式等待(缺点是这个模式)。

export class Login {
  ...

  loginAttempt(attempt = 0) {       
    
    if (attempt === 3) throw 'Unable to login after 3 attempts'

    this.login_button().click();
    
    // wait for login actions to change the page
    cy.wait(200).then(() => {  
      
      // test for success
      if (Cypress.$('.profile-content').length > 0) {
        return // found it, so finish
      }

      // retry
      loginAttempt(email, password, ++attempt)  
    })
  }

  login(email, password) {
    this.username_input().type(email);  
    this.password_input().type(password);
    loginAttempt()
    return this
  }
}

你可以把cy.wait(200)降低一点,提高最大尝试次数,比如说cy.wait(20)if (attempt === 300)

缺点是每次重试都会占用更多的堆内存,所以做得太过火是有害的 - 您需要进行试验。


直接设置登录状态

TLDR
好处:绕过测试的不稳定部分
缺点:需要了解登录状态

您可能会考虑的另一个方面是完全删除不稳定的登录。为此,您需要找出应用认为已登录的内容。是cookie、localstorage值等吗

对于每个需要处于登录状态但实际上并未测试登录过程的测试,直接设置该状态。

这里给出了一个例子recipes - logging-in__using-app-code

describe('logs in', () => {
  it('by using application service', () => {
    cy.log('user service login')

    // see https://on.cypress.io/wrap
    // cy.wrap(user promise) forces the test commands to wait until
    // the user promise resolves. We also don't want to log empty "wrap {}"
    // to the command log, since we already logged a good message right above
    cy.wrap(userService.login(Cypress.env('username'), Cypress.env('password')), {
      log: false,
    }).then((user) => {
    // the userService.login resolves with "user" object
    // and we can assert its values inside .then()

      // confirm general shape of the object
      expect(user).to.be.an('object')
      expect(user).to.have.keys([
        'firstName',
        'lastName',
        'username',
        'id',
        'token',
      ])

      // we don't know the token or id, but we know the expected names
      expect(user).to.contain({
        username: 'test',
        firstName: 'Test',
        lastName: 'User',
      })
    })
function login (username, password) {
  const requestOptions = {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ username, password }),
  }

  return fetch(`${config.apiUrl}/users/authenticate`, requestOptions)
  .then(handleResponse)
  .then((user) => {
    // login successful if there's a jwt token in the response
    if (user.token) {
      // store user details and jwt token in local storage to keep user logged in between page refreshes
      localStorage.setItem('user', JSON.stringify(user))
    }

    return user
  })
}