【e2e测试】基于Cypress+Mocha+gitlabCI的持续集成并行测试方案实践

背景介绍

最近有时间可以写一点东西, 总结下我在前司做的基于Cypress+Mocha+gitlabCI的持续集成并行测试方案。为了方便测试同学自己维护自动化代码,所以自动化代码和前端项目代码是使用不同gitlab仓库管理的。因此本文整体方案:

  1. 前端项目merger master触发上线pipeline:
  • 自动化专用环境更新(更新方式:docker + rancher)
  • 触发自动化环境回测pipeline (触发方式:gitlab ci pipeline api)
  • 等待自动化环境测试成功
  • 线上环境更新(更新方式:docker + rancher )
  1. 自动化项目回测pipeline:
  • 10个并行runner同时跑case(gitlab ci parallel关键字)
  • 合并10个runner的测试报告
  • 发送测试报告到mfs
  • 发送html测试报告(tomcat服务读取mfs中html)

并行方案

Cypress官方并行测试方案

Cypress官网其实给出了一个并测实测方案: Parallelization

中心思想:

 根据cypress的balance strategy策略将所有测试文件划分给多个可用机器 

用法:

      cypress run --record --key=abc123 --parallel

缺点:

  • 得有多个同时安装cypress以及其他环境的机器来执行用例
  • 依赖官方的算法策略和云服务,无法了解细节

Gitlab CI的parallel

在查找Cypress的parallel的过程中,发现gitlab ci也提供了一种在Pipeline中可以并行执行job的方法 parallel

用法:

     test:
     script: rspec
     parallel: 2

上述代码是将 当前test job划分为2个并行job , 实现结果如图:
在这里插入图片描述

命名为: test 1/N - test N/N

因此可以将所有的用例划分为几份,分发给定义的多个并行job。

分配测试用例

Cypress可以执行指定的测试文件,而且可以指定多个,用到的参数是:

    运行某个单独的测试文件而不是所有的测试用例:

    cypress run --spec "cypress/integration/examples/actions.spec.js"

    运行*号匹配到的文件目录(注意:推荐使用双星号**)

    cypress run --spec "cypress/integration/login/**/*"

     运行指定多个测试文件:

    cypress run --spec "cypress/integration/examples/actions.spec.js,cypress/integration/examples/files.spec.js"

因此将所有的测试文件的路径划分为几份, 然后作为spec参数传递设置的并行的job中

  tests:
     image: cypress/browsers:node16.14.2-slim-chrome100-ff99-edge
     stage: test
     parallel: 5
     script:
       - yarn cypress:run --spec "测试文件所在路径"

然后恰巧我的测试用例都是以文件夹形式划分的, 每个模块一个文件夹,因此我只要有个function来查找出所有的可测文件的路径,然后划分为几等份即可

考虑到有些模块执行时间较长,如果这些执行时间较长的文件都分配到同一runner,那样总执行时间并不会变短,考虑使用加入贪心算法进行测试用例的分配,使用单独json文件存储每个js文件执行的时间(是比较麻烦,需要在本地调试的时候查看当前文件的执行时间),分配测试用例到当前最短时间的runner , 完整代码详见:

const fs = require("fs");
const path = require("path");
const NODE_INDEX = Number(process.env.CI_NODE_INDEX || 1);
const NODE_TOTAL = Number(process.env.CI_NODE_TOTAL || 1);

const TEST_FOLDER = path.resolve(process.cwd(), "cypress/integration/test");

const testFileExecutionTimes = require("../support/testFileExecutionTimes");

const getAllTestFiles = (dir) => {
  const files = fs.readdirSync(dir);
  const result = [];
  files.forEach((file) => {
    const filePath = path.resolve(dir, file);
    const stats = fs.statSync(filePath);
    if (stats.isFile()) {
      result.push(filePath);
    } else if (stats.isDirectory()) {
      result.push(...getAllTestFiles(filePath));
    }
  });
  return result;
};

const getCurTestFiles = () => {
  const allSpecFiles = getAllTestFiles(TEST_FOLDER);

  const sortedTestFiles = allSpecFiles
    .map((file) => ({
      file,
      time: testFileExecutionTimes[path.basename(file)] || 0,
    }))
    .sort((a, b) => b.time - a.time);

  const pipelineTestFiles = Array.from({ length: NODE_TOTAL }, () => []);

  sortedTestFiles.forEach(({ file, time }) => {
    const shortestPipelineIndex = pipelineTestFiles.reduce(
      (minIndex, cur, index, arr) => {
        const curTotalTime = cur.reduce(
          (sum, f) => sum + testFileExecutionTimes[path.basename(f)],
          0
        );
        const minTotalTime = arr[minIndex].reduce(
          (sum, f) => sum + testFileExecutionTimes[path.basename(f)],
          0
        );
        return curTotalTime < minTotalTime ? index : minIndex;
      },
      0
    );

    pipelineTestFiles[shortestPipelineIndex].push(file);
  });

  pipelineTestFiles.forEach((testFiles, index) => {
    const totalTime = testFiles.reduce(
      (sum, file) => sum + testFileExecutionTimes[path.basename(file)],
      0
    );
  });

  return pipelineTestFiles[NODE_INDEX - 1];
};

console.log(getCurTestFiles().join(", "));

gitlab-ci.yml

api-tests:
  image: cypress/browsers:node16.14.2-slim-chrome100-ff99-edge
  stage: test
  parallel: 1
  script:
    - yarn cypress:run --spec $(node cypress/support/cypress-parallel-file.js)

合并Mocha测试报告

当每个runner测试完成后 都会在mochawesome-report目录下生成相应的json文件,例如: mochawesome.json mochawesome_001.josn
所以多个runner的时候会产生多份相同名称的json文件,而为了清晰的展示本次的测试结果,应该只产生一个完整的测试报告,包含所有执行的case,因此我们新建公共文件夹 并且重命名每份文件:

parallel: 2
 script:
    - yarn cypress:run --spec $(node cypress/support/cypress-parallel-file.js) --reporter mochawesome  --reporter-options reportDir=mochawesome-report,overwrite=false,html=false,json=true
    - mkdir public  // 新建公共文件夹
    - for f in mochawesome-report/*.json; do mv -- "$f" "${f%.json}${CI_JOB_ID}.json"; done; // 已job id 重命名
    - for f in mochawesome-report/*.json; do mv $f  public; done;  // 转移到公共目录下

但是有测试用例失败时,当前script中剩余步骤不会执行 ,即无法将每次运行生产的json文件转移到公共目录下
在这里插入图片描述
开始各种翻gitlab docs … ,翻到了 after_script: 该关键字可以在每个job后运行,(失败的也阔以~),
在这里插入图片描述
所以, 修改api-test job, 将每个runner的json类型的报告转移公共目录的步骤放在after_script中:

api-tests:
  image: cypress/browsers:node16.14.2-slim-chrome100-ff99-edge
  stage: test
  parallel: 2
  script:
    - mkdir public
    - yarn cypress:run --spec $(node cypress/support/cypress-parallel-file.js) --reporter mochawesome  --reporter-options reportDir=mochawesome-report,overwrite=false,html=false,json=true
  after_script:
    - for f in mochawesome-report/*.json; do mv -- "$f" "${f%.json}${CI_JOB_ID}.json"; done;
    - for f in mochawesome-report/*.json; do mv $f  public; done; 
  when: always
  allow_failure: true
 

api-tests阶段执行完成后,所有的json类型测试报告都会懂到public目录下, 接下来就是将所有json报告合并成html类型报告

merge:
  image: cypress/browsers:node16.14.2-slim-chrome100-ff99-edge
  image: cypress/base:10
  stage: merge
  script:
    - cd public
    - npx mochawesome-merge *.json  >  mochawesome_merged.json // 合并成同一个json报告
    - npx marge mochawesome_merged.json  //生成html类型报告, 该命令执行完仍然会在public文件夹内生成一个目录 mochawesome-report 此时并不是原存储测试结果的目录reportDir=mochawesome-report, (踩坑了,合并的测试报告在public下找不到,要到public/mochawesome-report下才能找到)
    - cd mochawesome-report
    - mv  mochawesome_merged.html mochawesome_merged_${CI_PIPELINE_ID}.html  // 以pipeline ID 重命名html报告,后续可以区分哪次pipeline进行的测试

发送测试报告

因为该方案是使用gitlab default runner进行测试,每次执行完成后无法通过default runner的文件目录(如果大家有的话,欢迎评论区留言),也因此读取测试报告的服务无法读取到default runner下的html报告. 所以我采取的方案是使用artifacts将测试报告下载并上传到mfs上,然后通过tomcat服务读取mfs目录下测试报告.
在这里插入图片描述
在这里插入图片描述
因此新建jenkins job,用于接收合并测试文件的CI_JOB_ID, 调用下载artifacts的api :
在这里插入图片描述

这里踩了个坑点,当我将触发jenkins下载的步骤放在merge时, 会发现下载下来的 artifacts.zip 是空

merge:
  image: cypress/browsers:node16.14.2-slim-chrome100-ff99-edge
  image: cypress/base:10
  stage: merge
  script:
    - cd public
    - npx mochawesome-merge *.json  >  mochawesome_merged.json
    - npx marge mochawesome_merged.json  // 该命令执行完仍然会在public文件夹内生成一个目录 mochawesome-report 此时并不是原存储测试结果的目录reportDir=mochawesome-report, (踩坑了,合并的测试报告在public下找不到,要到public/mochawesome-report下才能找到)
    - cd mochawesome-report
    - mv  mochawesome_merged.html mochawesome_merged_${CI_PIPELINE_ID}.html  // 以pipeline ID 重命名html报告,后续可以区分哪次pipeline进行的测试
    - curl -X POST http://XXXXX.com/jenkins/view/%E6%9C%89%E9%81%93%E5%B9%BF%E5%91%8A/view/XXX/job/download-autotest-report/buildWithParameters?token=XXX -d "projectId=${CI_PROJECT_ID}&jobId=${CI_JOB_ID}"。// 此时下载下来的artfacts.zip是空

回头去看了下官方文档, 只有当前job执行完成后,才会将文件上传至gitlab
在这里插入图片描述
因此将下载的放在下一个job中 ,但在sendReports job中我们需要获取到上一Job: merge 的CI_JOB_ID,这样才可以通过api下载,因此要用到pipeline之间的变量传递:

merge:
  image: cypress/browsers:node16.14.2-slim-chrome100-ff99-edge
  image: cypress/base:10
  stage: merge
  script:
    - cd public
    - echo ${CI_JOB_ID}
    - echo "MERGE_JOB_ID=${CI_JOB_ID}" >> merge.env  // 将变量写成VARIABLE_NAME=ANY VALUE HERE.形式
    - npx mochawesome-merge *.json  >  mochawesome_merged.json
    - npx marge mochawesome_merged.json
    - cd mochawesome-report
    - mv  mochawesome_merged.html mochawesome_merged_${CI_PIPELINE_ID}.html
  when: always
  artifacts:
    reports:
        dotenv: public/merge.env  // 将变量文件存在reports/dotenv下
    expire_in: 30 days
    when: always
    paths:
      - public
 
sendReports:
  image: cypress/browsers:node16.14.2-slim-chrome100-ff99-edge
  stage: sendReports
  script:
    - echo ${MERGE_JOB_ID}
    - curl -X POST http://XXX/jenkins/view/%E6%9C%89%E9%81%93%E5%B9%BF%E5%91%8A/view/XXX/job/download-autotest-report/buildWithParameters?token=XXX -d "projectId=${CI_PROJECT_ID}&jobId=${MERGE_JOB_ID}"
  dependencies:   // 执行添加dependencies
    - merge

这里用到JOB传递变量:在job脚本中,将变量保存为.env文件。文件的格式必须是每行一个变量定义。每个定义的行必须是VARIABLE_NAME=ANY VALUE HERE.值可以用引号引起来,但不能包含换行符 .
然后通过TomCat获取html文件,下载tomcat: apache-tomcat-10.0.27.tar.gz , vim conf/server.xml下添加路径 :
在这里插入图片描述

sh bin/startup.sh, 启动服务后配置域名,https://cypress-report.XXX.com/mocha_report_pipelineId获取每次的测试报告, 后续如果有统计代码覆盖率等操作,也可以直接在mfs的报告中进行分析.
这里还有一个踩坑点,第二步中使用 npx marge mochawesome_merged.json后 会生成mochawesome- report文件夹 ,该文件夹下不仅会包含html文件,还会包含assets文件夹,如果仅将htmls上传到mfs下,读取的到html将是空白页,因此需要将assets文件夹也移动到读取测试报告的目录下
在这里插入图片描述
最后将测试报告连接发送给测试或研发同学,该步骤是通过前司运维同学封装好的gitlab的通知功能,直接通知到工作群,因此这一步不详细讲述
在这里插入图片描述
但这样存在一个问题, 测试报告中仅有连接,无法知道具体执行的情况,比如成功多少,失败多少, 因此需要在通知中加入这些信息. 回过头翻看json类型的报告发现,json报告中详细描述了本次测试的情况,可以读取信息写在通知中
在这里插入图片描述


merge:
  image: cypress/browsers:node16.14.2-slim-chrome100-ff99-edge
  image: cypress/base:10
  stage: merge
  only:
    variables:
      - $AUTO_TEST
  before_script:
    - apt-get update && apt-get install -y  jq // 处理json数据使用
  script:
    - cd public
    - echo ${CI_JOB_ID}
    - echo ${OVERSEAS_PIPELINE_ID}
    - echo "MERGE_JOB_ID=${CI_JOB_ID}" >> merge.env
    - npx mochawesome-merge *.json  >  mochawesome_merged.json

    - TEST_DATA=$(cat mochawesome_merged.json | jq -r '.stats')
    - echo ${TEST_DATA}

    - FAILURES=$(echo "$TEST_DATA" | jq '.failures')
    - PASSES=$(echo "$TEST_DATA" | jq '.passes')
    - TESTS=$(echo "$TEST_DATA" | jq '.tests')

    - echo $FAILURES
    - echo $PASSES
    - echo $TESTS

    - echo "FAILURES=${FAILURES}" >> merge.env
    - echo "PASSES=${PASSES}" >> merge.env
    - echo "TESTS=${TESTS}" >> merge.env

    - npx marge mochawesome_merged.json
    - cd mochawesome-report
    - mv  mochawesome_merged.html mochawesome_merged_${OVERSEAS_PIPELINE_ID}.html
  when: always
  artifacts:
    reports:
        dotenv: public/merge.env
    expire_in: 30 days
    when: always
    paths:
      - public
      - screenshots_${OVERSEAS_PIPELINE_ID}

通知:

notify_success:
  stage: notify
  only:
   variables:
     - $AUTO_TEST
  tags:
    - k8s
  image: XXX
  variables:
    MSG: 'CI PIPELINE SUCCESS'
  script:
    - echo $FAILURES
    - echo $PASSES
    - echo $TESTS
    - if [[ $FAILURES -gt 0 ]]; then
      MSG="UI上线回测结果 存在失败用例!\n共执行${TESTS}个用例\n成功${PASSES}个\n失败${FAILURES}个\n测试报告:https://cypress-report.XXXX.com/pipelineReports/mochawesome_merged_${OVERSEAS_PIPELINE_ID}.html";
      else
      MSG="UI上线回测结果 全部成功!\n共执行${TESTS}个用例\n成功${PASSES}个\n失败${FAILURES}个\n测试报告:https://cypress-report.XXX.com/pipelineReports/mochawesome_merged_${OVERSEAS_PIPELINE_ID}.html";
      fi
    - ydci notify XXX -t $USER_EMAIL -s "$CI_PROJECT_NAME" -m "${MSG}"
  dependencies:
    - merge

pipeline触发和状态等待

前端项目触发自动化项目pipeline

job:auto-test:
  image: XXX
  tags:
    - k8s
  stage: auto-test
  rules:
    - if: $DEPLOY_AUTOTEST == "true"
    - if: $CI_COMMIT_REF_NAME == "master" && $CI_PIPELINE_SOURCE != "api" &&  $CI_PIPELINE_SOURCE != "trigger"
  script:
    - echo "start e2e test"
    - pipelineId=$(curl --request POST  --header "PRIVATE-TOKEN:XXX"
      --header "Content-Type:application/json"
      --data '{ "ref":"'master'","variables":[{"key":"BASE_URL", "value":"'XXX'"},{"key":"AUTO_TEST","value":"'true'"},{"key":"USER_EMAIL","value":"'${GITLAB_USER_EMAIL}'"},{"key":"OVERSEAS_PIPELINE_ID","value":"'${CI_PIPELINE_ID}'"},{"key":"NODE_ENV","value":"'test'"}]}'
      "https://gitlab.corp.youdao.com/api/v4/projects/12604/pipeline"  | awk -F[,:] '{print $2}') // 传参数
    - echo $pipelineId
    - sh checkAutoTestPipelineStatus.sh  $pipelineId // 等待pipelineId
    

通过gitlab pipeline api等待pipeline状态

# check gitlab pipeline status until it success 
status=$(curl --request GET  --header "PRIVATE-TOKEN:XXXX"  "https://gitlab.XXX.com/api/v4/projects/XXX/pipelines/$1" | awk -F[,:] '{print $10}'| awk -F'"' '{print $2}')
while [[ "$status" == "running" || "$status" == "pending" || "$status" == "created" ]]
do
  echo "pipeline is running ......"
  sleep 10
  status=$(curl --request GET  --header "PRIVATE-TOKEN:XXX"  "https://gitlab.XXX.com/api/v4/projects/12604/pipelines/$1" | awk -F[,:] '{print $10}'| awk -F'"' '{print $2}')
done

if [[ "$status" == "success" ]]; then
	echo "pipeline running successfully"
elif [[ "$status" == "failed" ]]; then
	echo "pipeline running failure"
	exit 1
else
	echo "pipeline is $status"
	exit 1
fi

  • 16
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值