notes icon indicating copy to clipboard operation
notes copied to clipboard

Cypress.io如何测试多账户之间的真实互动? or How Cypress.io Tests Real Interaction between Multiple Accounts?

Open lanlin opened this issue 6 years ago • 4 comments

背景

cypress.io 是个极好的东西。尤其是用 cypress.io 来进行端到端测试。 不过,凡事也都有其局限性。cypress.io 目前只支持单实例运行同一个测试文件。 而且,官方固执的认为,你不需要一次性开启多个浏览器实例来测试交互。 在众多开发者提出的疑问里面,巴拉巴拉一大堆,最后都是不了了之。

image 以上官方不支持多浏览器实例的理由

而且,它的请求和响应是被封装为自动完成的。也就是说,你企图在实例内发出的请求, 其返回的头信息中包含的 cookie 都是自动被设置到实例中的。 因为它这样的封装,导致我们无法同时登录两个账户并完成交互操作。

办法

  1. 用 cy.task() 方法在后台开启一个单独的 node 任务,直接发请求给后端。 以达到通过后台登录和操作其他账户的目的。比如将 axios 封装到一个 task 的监听事件。 这样就可以在测试实例中调用这些事件,从而在后台发起独立于测试实例的请求。

  2. 启动多个 cypress.io 服务,分别运行不同账户的对应测试文件。 比如依次启动两个 cypress.io 的服务,一个监听 8080,一个监听 8081.

  3. 借助 webdriver 或者 puppeteer 来实现,也是封装相应的调用为 task 事件监听。

实现 (VUE 项目为例)

关于上述三种方法的具体实现,下面以 vue 项目为例,大致说一下前两种。至于第三种,请自行搜索其他参考。

lanlin avatar Mar 20 '19 08:03 lanlin

第一种方案:

编写针对 task 的 plugin,目录如下 image

index.js 代码如下

const request = require('./request');

module.exports = (on, config) => {

  // 注册 request.js 中的事件监听方法
  on('task', {
    doGet: request.doGet,
    doPost: request.doPost,
  });

  return Object.assign({}, config, {
    fixturesFolder: 'tests/e2e/fixtures',
    integrationFolder: 'tests/e2e/specs',
    screenshotsFolder: 'tests/e2e/screenshots',
    videosFolder: 'tests/e2e/videos',
    supportFile: 'tests/e2e/support/index.js',
  });
};

request.js 代码如下

const axios = require('axios');  // 引入 axios
const users = require('../fixtures/user'); // 请先在 fixtures 中建立用户列表

// ------------------------------------------------------------------------------

const TEMP = {};
const base = { url: '/', baseURL: '你的基本url地址' };

// ------------------------------------------------------------------------------

// check exists
const hasWho = who => Object.hasOwnProperty.call(TEMP, who);

// ------------------------------------------------------------------------------

// create http
const createHttp = (who) => {
  if (!hasWho(who)) {
    throw new Error(`user ${who} not logined yet!`);
  }

  let Cookie = '';

  TEMP[who].forEach((cookie) => {
    Cookie += `${cookie.split(' ')[0]} `;
  });

  return Object.assign({}, base, { headers: { Cookie: Cookie.trim() } });
};

// ------------------------------------------------------------------------------

// doing login
const doLogin = async (who) => {
  const user = users[who];

  if (!user) { throw new Error(`user ${who} not found!`); }
  if (hasWho(who)) { return createHttp(who); }

  // 请修改为你自己的登录参数
  const params = {
    password: user.password,
    useremail: user.username,
  };

  const result = await axios.create(base).post('你的登录API地址', params);
  TEMP[who] = result.headers['set-cookie'];

  return createHttp(who);
};

// ------------------------------------------------------------------------------

// doing get request
const doGet = async ({ url, who = null }) => {
  let options = null;

  if (who) { options = await doLogin(who); }

  const result = await axios.get(url, options);

  return {
    data: result.data,
    status: result.status,
    statusText: result.statusText,
    headers: result.headers,
  };
};

// ------------------------------------------------------------------------------

// doing post request
const doPost = async ({ url, data, who = null }) => {
  let options = null;

  if (who) { options = await doLogin(who); }

  const result = await axios.post(url, data, options);

  return {
    data: result.data,
    status: result.status,
    statusText: result.statusText,
    headers: result.headers,
  };
};

// ------------------------------------------------------------------------------

/**
 * @type {{doPost(*=): *, doGet(*=): *}}
 */
module.exports = { doGet, doPost };

测试用例中调用方式

  it('测试第二个账户', () =>
  {
    cy.task('doGet', {
      who: 'zhangsan',     // 在 fixtures/user.json 中配置
      url: '你要请求的API地址',
    }).then((data) => {
      console.log(data);
    });
  });

tests/e2e/fixtures/user.json示例

{
    "zhangsan": {"username": "[email protected]", "password": "123456"},
    "lisi": {"username": "[email protected]", "password": "123456"},
}

lanlin avatar Mar 20 '19 09:03 lanlin

第二种方案:

我假定你正常运行时,项目的端口地址是 8080。 下面的步骤是让你启动另一个 Cypress.io 服务,并运行在 8081 下面。

  1. 在 vue 项目的根目录,建一个叫做 .env.port 的文件。在文件中加入以下代码
NODE_ENV=development
VUE_APP_PORT=8081
  1. 在项目根目录的 vue.config.js 中进行以下修改
// 根据环境变量,决定端口号
module.exports = {
  devServer: {
      port: process.env.VUE_APP_PORT || 8080,
      public: '你的域名' + (process.env.VUE_APP_PORT || 8080),
  }
};
  1. 如果在 vue 代码中的url地址也有端口号的,同样以相同的方式进行判断
process.env.VUE_APP_PORT || 8080
  1. 修改根目录 package.json
  "scripts": {
    "test-cypress1": "vue-cli-service test:e2e",
    "test-cypress2": "vue-cli-service test:e2e --mode port",
  }
  1. 依次启动两个 cypress.io 服务
> npm run test-cypress1
> npm run test-cypress2
  1. 注意不同的 Cypress.io 服务尽量不要使用相同的浏览器,避免冲突导致无法运行测试。

可以一个选择 Chrome,一个选择 Electron. 如下如图

image

image

lanlin avatar Mar 20 '19 09:03 lanlin

第三种方案

image

参见各种 issues 里面官方和民间的扯皮。 https://github.com/cypress-io/cypress/issues/1207 https://github.com/cypress-io/cypress/issues/590 Multiple browsers open at the same time

image 官方文档关于多浏览器的说明

基本上官方的说法就是,你爱用不用,有本事你自己写一个...

image

这个问题确实让人很恼火,官方建议你把跨账户交互写成了单元测试。 我TM是要进行 e2e 测试啊,各种 stub,mock,以及用别家的东西来辅助。 那还不如直接用别家的东西啊。怪自己能力有限啊...

lanlin avatar Mar 20 '19 09:03 lanlin

如果大家有什么更简单有效的疗法,欢迎补充!

lanlin avatar Mar 20 '19 09:03 lanlin