Cypress最佳实践2

本文共--字 阅读约--分钟 | 浏览: -- Last Updated: 2022-07-07

PageObject模式

PageObject 模式是自动化测试中的一个最佳实践,具备以下特征:

  • 将每个页面(或者待测试对象)封装成一个类(class),类里包括了页面上所有的元素及它们的操作方法。(单步操作或功能集合)
  • 测试代码和被测代码解耦,使用PageObject对象后,当页面发生改变,无需改变测试代码。

PageObject 减少了代码冗余,使业务流程变得清晰可读,降低了测试维护成本。

举例:Cypress 中的 PageObject 模式

// commonPage.js
export default class CommonPage {
  constructor() {
    // ...
  }

  isTargetPage() {
    cy.url().should('eq', this.url);
  }
}

// login.js
import CommonPage from './commonPage';

export default class LoginPage extends CommonPage {
  constructor() {
    super();
    this.userNameLocator = 'input[name=username]';
    this.passwordLocator = 'input[name=password]';
    this.formLocator = 'form';
    this.url = 'http://localhost:7077/login';
  }

  get username() {
    return cy.get(this.userNameLocator);
  }

  get password() {
    return cy.get(this.passwordLocator);
  }

  get form() {
    return cy.get(this.formLocator);
  }

  login(username, password) {
    this.userName.type(username);
    this.password.type(password);
    this.form.submit();
  }
}

// mainPage.js
import CommonPage from './commonPage';

export default class mainPage extends CommonPage {
  constructor() {
    super();
    this.h1Locator = 'h1';
    this.url = 'http://localhost:7077/dashboard';
  }

  get welcomeText() {
    return cy.get(this.h1Locator);
  }
}

在测试中使用:

import LoginPage from '../pages/login';
import MainPage from '../pages/main';


descrbe('登录测试', () => {
  const username = 'jane.lane';
  const password = 'password123';

  it('登录成功', () => {
    cy.visit('/login');
    const loginInstance = new LoginPage();
    loginInstance.isTargetPage();
    loginInstance.login(username, password);
    cy.url().should('include', '/dashboard');

    const mainInstance = new MainPage();
    mainInstance.isTargetPage();
    mainInstance.welcomeText.should('contain', 'jane.lane');
  })
})

这就是 PageObject 的一个典型场景,需要注意的是,PageObject模式存在这样一个问题,如果一个测试需要访问多个页面对象,就意味着测试要初始化多个页面对象实例,如果这个页面对象需要登录才能访问(大部分是这样),则每次初始化都需要先登录再访问,无形增加了测试运行的时间。

这种场景下 Cypress 不认为 PageObject 是一个好的模式,Cypress 认为跨页面共享逻辑是一个反模式(Anti-Pattern),因为 Cypress 的实现原理与其他工具完全不同,Cypress 提供了很多方式允许用户通过“捷径”直接设置被测应用达到测试状态,而无须在不同页面一遍又一遍执行相同的操作。这个捷径就是 Custom Commands。

使用自定义命令(Custom Commands)

自定义命令默认存放在 cypress/support/commands.js 文件中,它会在任何测试文件被导入之前加载。

// cypress/support/commands.js
Cypress.Commands.add('login', (username, password) => {
  cy.get('input[name=username]').type(username);
  cy.get('input[name=password]').type(`${password}${enter}`);
})

在测试代码中这样使用:

it('登录成功', () => {
  cy.visit('/login');
  cy.login(username, password);

  const mainInstance = new MainPage();
  mainInstance.isTargetPage();
  mainInstance.welcomeText.should('contain', 'jane.lane');
})

自定义命令的好处体现在:

  • 可以像 Cypress 内置命令那样直接使用,无须像 PageObject 那样引入。
  • 自定义命令可以比 PageObject 模式运行更快。
  • 自定义命令允许你重写 Cypress 内置命令,这意味着你可以自定义测试框架并立刻全局应用。

比如,重写 visit 命令:

Cypress.Commands.overwrite('visit', (originalFn, url) => {
  // 比如 重写 visit 命令,使每个visit命令都打印一行+++符号在 console里
  console.log('++++++++++++');

  // originalFn 代表传入进来的原 'visit' 命令
  // url 是 visit() 接受到的 url 地址
  return originalFn(url);
})

数据驱动策略

数据驱动是测试框架中的一个必要功能,使用数据驱动,可以在不增加代码量的前提下根据数据生成不同的测试策略。

数据保存在前置条件中

describe('测试数据放在前置条件里', () => {
  let testData;

  beforeEach(() => {
    testData = [
      { "name": 'iTesting', "password": "helloqa"}, 
      { "name": 'kevin', "password": "helloqa"}, 
    ]
  })

  for (const data in testData) {
    it('demo1', () => {
      cy.login(data.name, data.password);
    })
  }
})

使用 fixtures

describe('测试外部数据', () => {
  it('测试外部数据$', () => {
    // example.json 存放在 cypress/fixtures 下
    cy.fixture('example.json').as('testData');
    cy.get('@testData').each((data) => {
      cy.log(data.name);
      cy.log(data.password);
    })
  })
})

数据保存在自定义文件中

import testData from '../../settings/user.json';

describe('数据保存在自定义文件中', () => {
  for (const data in testData) {
    it('demo1', () => {
      cy.login(data.name, data.password);
    })
  }
})

环境变量设置

除了设置 cypress.json 中的 env 字段外,还可以使用专门的 cypress.env.json,例如在 cypress/config 下建立两个文件 cypress.dev.jsoncypress.qa.json

cypress.env.json

// cypress.dev.json
{
  "baseUrl": "http://localhost:7077/login",
  "env": {
    "uesrname": "jane.lane",
    "password": "password123"
  }
}

// cypress.qa.json
{
  "baseUrl": "http://localhost:7077/login",
  "env": {
    "uesrname": "wrongUser",
    "password": "wrongPassword"
  }
}

plugins/index.js 更改配置如下:

// plugins/index.js

const fs = require('fs-extra');
const path = require('path');

function getConfig(env) {
  const configPath = path.resolve('..', 'Cypress/cypress/config', `cypress.${env}.json`)
  return fs.readJson(configPath)
}

module.exports = (on, config) => {
  // 取参数
  const file = config.env.configEnv || 'dev';
  // 修改config
  return getConfig(file);
}

这些运行命令行:

# 指定环境为 qa
npm run cypress:open --env configEnv=qa

运行时动态的指定环境变量

设置 cypress.json:

{
  // ...
  "targetEnv": "dev",
  "env": {
    "dev": {
      "uesrname": "jane.lane",
      "password": "password123",
      "url": "http://localhost:5883"
    },
    "qa": {
      "uesrname": "wrongUser",
      "password": "wrongPassword",
      "url": "https://qa.test.com:5883"
    }
  }
}

更改 support/index.js:

关于 support 的作用,请前往 here

// 接受用户的参数 testEnv,如果没有指定 testEnv,则使用 cypress.json 中 targetEnv 的默认设置
beforeEach(() => {
  const targetEnv = Cypress.env('targetEnv') || Cypress.config('targetEnv');

  cy.log(`测试环境为: \n ${JSON.stringify(targetEnv)}`);
  cy.log(`测试环境详细配置为: \n ${JSON.stringify(Cypress.env(targetEnv))}`);

  // targetEnv = 'qa'
  // Cypress.env(targetEnv) 取出对应的配置

  Cypress.config('baseUrl', Cypress.env(targetEnv).url);
})

运行命令行指定运行的测试环境:

npm run cypress:open --env testEnv=qa

动态挑选待运行测试用例

是指给测试用例添加一个或多个相应描述关键字,在运行时,指定相应的关键字,运行或者排斥测试用例。

1、安装插件

npnm i --save-dev cypress-select-tests

2、更改 cypress/plugins/index.js

const selectTestsWithGrep = require('cypress-select-tests/grep');

module.exports = (on, config) => {
  on('file:preprocessor', selectTestsWithGrep(confog));
}

3、这样编写测试用例

describe("测试登录", () => {
  const username = 'jane.lane';
  const password = 'password123';

  context('登录成功, 跳转到 dashboard 页', () => {

    it("['smoke'] 登录用例1", () => {
      cy.visit('http://localhost:7077/login');
      cy.get('input[name=username]').type(username);
      cy.get('input[name=password]').type(password);
      cy.get('form').submit();

      cy.get('h1').should('contain', 'jane.lane')
    })

    it("[e2e, 'smoke'] 登录用例2", () => {
      cy.log('iTesting');
    })
  })
})

4、这样指定标签运行

npm run cypress:open --env grep=e2e

或者直接根据文件名来筛选待测试用例:

# 所有文件名中包含 “Login” 字符的测试套件将被运行
npm run cypress:open --env fgrep=Login

测试运行失败自动重试

由于各种不确定因素,偶尔会发生测试用例失败的情况,此时如果测试用例可以自动重新运行,则减少了测试由于环境等不确定因素失败的情况。

1、安装 cypress-plugin-retries

npm i -D cypress-plugin-retries

2、在 cypress/support/index.js 下增加以下代码:

require('cypress-plugin-retries')

3、配置 package.json 执行脚本。

// package.json

{
  "scripts": {
    "retryCases": "CYPRESS_RETRIES=2 cypress run" // 重试测试为2次
  }
}

4、使用

npm run retryCases

Cypress连接DB

在测试时,为了达到某个测试状态,有时需要连接DB,这时候需要使用到 cy.task()

它的语法是:

cy.task(event);
cy.task(event, arg);
cy.task(event, arg, options);

1、修改 cypress/plugins/index.js

module.exports = (on, config) => {
  on('task', {
    log(message) {
      console.log(message);
      return null
    }
  })
}

2、在测试代码中这样使用

// 触发 log task
cy.task('log', '这是一个log')

连接DB

1、安装 mysql

npm i mysql --save-dev

2、配置 cypress.json

{
  // ...
  "env": {
    // ...
    "db": {
      "host": "your-host",
      "user": "your-username",
      "password": "your-password",
      "database": "your-database"
    }
  }
}

3、修改 cypress/plugins.js

const mysql = require('mysql');

function queryTestDb(query, config) {
  const connection = mysql.createConnection(config.env.db);

  connection.connect();

  return new Promise((resolve, reject) => {
    connect.query(query, (error, results) => {
      if (error) {
        reject(error);
      } else {
        connection.end();
        console.log(results);
        return resolve(results);
      }
    })
  })
}

module.exports = (on, config) => {
  on('task', {
    queryDb: (query) => {
      return queryTestDb(query, config)
    }
  })
}

4、这样使用

var query = 'select id from id_table';

cy.task('queryDb', query);