[toc]

# 1. 写在前面

基本上规范的大型项目都有自动化测试,它可以快速发现问题,提升敏捷开发的信心。

但是由于是机器执行,不是人工,所以总会有错误,比如环境问题、用例设计错误等,所以遇到用例失败不要慌,先定位问题,如果是经常失败的用例可以就不用管,切忌强迫症,面面俱到,却忽略了核心流程。

很典型的例子是,因为用例经常报错就废弃掉自动化测试,无异于因噎废食。所以这里想说的是,对于复杂的项目,请不要追求100%正确率,或者不要急于追求100%的正确率。先跑着,用起来。

# 2. 开始

自动化测试要落地好比较困难,后期有一些维护成本。

对比下,跨端工具基本是开发好了后,就不用管了,除非写的不够好。但是自动化测试需要持续的维护,因为其面向的是业务,而业务是很有可能变动的。

影响自动化测试的因素比较多,比如环境、测试页面的网络、后台服务器稳定性等,要在本地多跑几遍,再上远端运行,并且代码也要写得足够健壮,对元素缺失、点击无响应等做好预案。

2年前也搞过一段时间的自动化测试,那时候想错了,总觉得需要把拉起游戏这个步骤打通。现在看来这个完全可以用手工测试来覆盖,自动化测试可以覆盖80%以上的场景,没必要强求面面俱到,毕竟机器不是人。

不管做什么一定要有沉淀,自动化测试可以沉淀什么呢?一些提炼的工具方法、模板化的的配置方法。

自动化测试项目要求开发者对框架比较熟悉,才能让代码结构清晰、易维护、用例稳定迭代。

# 3. 自动化测试闭环

# 3.1. 结构

目前架构如下,所有项目的e2e测试用例都在一个仓库中,分文件夹区分不同项目,比如:

- e2e
  - pvp-esports
    - 01.create-game.cy.js           
    - 02.game-set.cy.js              
    - 03.custom-schedule.cy.js       
    - 04.round-set.cy.js             
    - 05.schedule-page.cy.js
    - 11.index-page.cy.js
    - 21.homepage.cy.js
    - 31.message-center-detail.cy.js
    - config.js
  - pvp-wsq
    - draw-benefit.cy.js
    - config.js

为什么不把不同的项目分成不同的仓库呢?因为我们的测试用例并不多,放在一起方便管理,方便复用一些逻辑。

# 3.2. 接入研发平台

项目的每个文件夹名就是项目的简称,对应的是业务仓库的一个子工程,所以e2e项目和研发平台的项目是多对一的关系,这里用配置文件将二者关联起来。

e2e项目 --- 目录 --- 配置文件 --- 研发平台项目

流水线运行的时候,将项目名称、对应的研发平台项目Id、测试结果、耗时、流水线等信息,发送到研发平台的后台进行记录,并可以在研发平台查看。

研发平台对外提供的接口是框架无关的,也就是之后接入minitest或者其他框架,都可以很方便。

# 3.3. 报告处理

同时,将测试的录屏发送到腾讯云COS上,这样是为了在失败时查看录屏方便。

此外,为了防止COS上内容爆炸,对存储数量做了限制,同一项目的录屏如果超过了配置数值,就删除很早之前上传的。

小结下,报告处理涉及三个平台,研发平台记录测试结果,COS存储录屏,失败通知发送给机器人。

### 3.4. 通知策略

关于机器人的通知策略,这里也描述下。

  • 成功
    • 每天早上9点发送到组内群
    • 每天10点、14点、16点发送给自己
  • 失败
    • 发送给订阅机器人的所有群

为什么成功的用例还要通知呢?是为了同步进展,确保测试任务正常运行。

# 3.5. 分项目执行

多个项目放在一个仓库,如何在流水线中分项目执行呢?

这里采用的方式是流水线中传入变量key,拉取配置获取key对应的项目,测试命令中只执行对应的目录下的用例。

流水线变量 --- 配置文件 --- 测试命令

分项目执行的好处是方便管理,以及为后面的多分支、多环境执行做准备。

# 3.6. 多分支执行

多分支、多环境其实就是多了一些变量,如baseUrltitle等,这个可以在配置中心保存,然后采用的还是用上面的方法:

流水线变量 --- 配置文件 --- 测试命令

此外,多分支同时执行要特别注意互相影响的情况,比如互相抢IM的登录态,获取最新消息被覆盖,首页赛事第一条被覆盖等情况。

# 3.7. 顺序测试

有些测试用例之间是有顺序的,比如创建赛事、加入队伍、结束报名、改判比分,这几个操作需要顺序执行,有两个方案:

  1. 可以将它们都放到一个文件中,缺点是文件将变得臃肿,难以维护。
  2. 给文件排序,或者重命名文件,让它们执行的顺序等于业务需要的顺序。

这里采用的是第二种,规范如下:

对于需要特定执行顺序的测试文件,文件名以010203等开头。

对于同一个业务中不需要顺序执行的文件,文件名以99开头,这样的目的是让它们不影响前面有顺序要求的文件。

# 3.8. 项目规范

相比于单元测试和业务项目,自动化测试项目更新频繁,后期很容易变得混乱。

为保证项目持续、健康、稳定迭代,目前制定了一些规范,包括:

  • e2e下每个子文件夹代表一个项目
  • 不同页面分文件测试,比如homepage.cy.js
  • 一个业务页面可以对应多个测试文件,格式为page.module.cy.js
  • 每个测试文件不超过10个用例
  • 每个用例代码不超过10行,复杂的逻辑要封装

# 4. 基于配置的方式

我们来思考一下配置化的本质,配置化也是把不变的和变化的分开,其中将不变的做到最大,变化的部分也就是配置的部分做到最小,所以配置化也是一种设计模式。

如果能够做到完全配置,零代码开发的自动化测试自然是最好的,但是目前并不能做到这样,也就是可以把一个个操作抽离成Map,然后用配置驱动,但是后期仍需要不断开发调试来维护。

因为现网环境一般是复杂的,很多测试的目标依赖于上下文,需要不断调试,所以用配置的方式其实反而是多了一些步骤。就像低代码一样,做简单的需求还可以,复杂一点的就hold不住了,还不如直接写代码。

# 5. 测试框架对比

之前用的puppeteer,现在用的是cypress,个人感觉是快很多,因为了使用JS注入页面,而不是双端通信方式,API也简单。

指标 Selenium Puppeteer TestCafe Cypress
语言 多种语言:Java、Python、Ruby JavaScript JavaScript JavaScript
实现原理 Json wire 协议 chrome CDP 协议 JS注入页面 JS注入页面
等待方式 阻塞等待 异步 异步 异步
支持的浏览器 IE、Firefox、Chrome等 仅Chrome IE、Firefox、Chrome等 Chrome、Edge、Electron、Firefox
测试断言库 内含 Mocha/Chai
用途 测试 广泛 测试 测试

# 6. 小程序自动化测试

# 6.1. minium、minitest

minium (opens new window)是小程序的自动化框架,后面带个nium,类似的还有appniumselenium

minitest (opens new window)是基于minium的自动化测试服务,支持云测。

上面是我的臆测,这两概念是你中有我,我中有你的关系,比如下面的代码:

class CreateGameTest(minium.MiniTest):

# 6.1.1. 路由跳转

测试路由跳转,不要用下面的方式,因为等待时间不可控,可能会花费更多的测试时间:

time.sleep(3)
self.assertEqual(self.page.path, '/views/match-detail/match-detail')

可以用下面的方式:

ret=self.app.wait_for_page('/views/match-detail/match-detail')
self.assertTrue(ret, 'wait success')

# 6.1.2. 项目架构

从下面测试截图的地址,可以查看minitest平台的架构:

https://minitest.xxx.com/resource/1381/199391/231679/3758/2408515/setup.jpg

分为以下几级:

  • 项目
  • 测试计划
  • 测试任务
  • 设备
  • 结果

也就是appid/planId/taskId/deviceId/resultId

# 6.2. miniprogram-automator

miniprogram-automator (opens new window)minium 底层是同一套东西,前者语言是js/ts,后者语言是python,二者都不支持headless模式,无法在linux环境中运行。

另外,miniprogram-automator只能用开发者工具或真机,不能用云真机。

minitest包含云测服务,但是只能用python写用例。

# 6.2.1. 启动

可以在测试脚本中启动:

// 工具 cli 位置,如果你没有更改过默认安装位置
const CLI_PATH = '/Applications/wechatwebdevtools.app/Contents/MacOS/cli'; 
// 项目文件地址
const PROJECT_PATH = '/Users/mike/Documents/web/dist/build/mp-weixin'; 

async function init() {
  miniProgram = await automator.launch({
    cliPath: CLI_PATH,
    projectPath: PROJECT_PATH,
  });
}

也可以在命令行中启动后,从脚本中连接:

# 进入微信开发者工具的安装目录
cd /Applications/wechatwebdevtools.app/Contents/MacOS
# 找到要执行自动化测试的目录
./cli --auto /Users/mike/Documents/web/dist/build/mp-weixin --auto-port 9420
async function init() {
  miniProgram = await automator.connect({
    wsEndpoint: 'ws://localhost:9420',
  });
}

# 6.2.2. 相同选择器的选择并点击

await page.$$eval('.set-item.list-press', async (btns) => {
  const btn = Array.from(btns).find(item => item.innerText === '加虚拟队');
  await btn.click();
  return btn;
});

# 7. 自动化测试与监控

自动化测试和监控都可以发现问题,二者有何区别?如何利用它们呢?

  • 检测时机不同,自动化测试可以在预发布环境中进行,监控只能在发布后进行
  • 成本方面,自动化测试编写、维护成本高于监控
  • 覆盖范围,一般情况下监控覆盖的范围更广一些,测试用例由于成本高,覆盖范围很难很大

自动化测试可以用来检测主流程,以及上线前的检查。

# 8. Cypress

# 8.1. 元素获取

元素可以通过cy.get()方法来获取,get的参数可以是css选择器。还可以通过cy.contains()获取特定文案的方式来获取元素,比如:

cy.get(BTN_MAP.INDEX.MENU_MESSAGE_ITEM)
  .contains('消息中心')
  .click();
cy.contains('小助手').click();

# 8.2. 自带断言

Cypress 查找元素其实已经自带断言了,比如get(),如果找不到元素,用例就会失败。

// 如果元素不存在,用例直接失败
cy.get('.uni-input-wrapper');

如何断言一个元素不存在呢?可以用下面的方式。

// 判断元素不存在
cy.get('.check-box').should('not.exist');
// 判断是否可见
cy.get('.check-box).should('be.visible')

// 判断元素存在
cy.get('.check-box).should('exist')

# 8.3. 文案断言

推荐使用include.text,需要严格验证的文案则使用have.text

cy.get(BTN_MAP.MESSAGE_CENTER_INDEX.TITLE)
  .should('include.text', '消息');
cy.get(BTN_MAP.MESSAGE_CENTER_INDEX.TITLE)
  .should('have.text', '消息中心');

也可以使用正则:

cy.get(BTN_MAP.ROUND_SETTING.TABLE_ITEM(2, 2))
  .invoke('text')
  .should('match', /\d+\/\d+\s+\d+:\d+开赛/);

# 8.4. BDD、TDD

Cypress 支持 BDD(expect/should)和 TDD(assert)格式的断言:

  • BDD:Behavior Driven Development 行为驱动开发,就是编写行为和规范,然后驱动软件开发。
  • TDD:Test Driven Development 测试驱动开发,简单来说,就是先写测试用例,然后编写实际代码使测试用例通过。

Cypress 命令通常具有内置的断言,这些断言将导致命令自动重试,以确保命令成功或超时后失效。

BDD 例子:

it('should return 1 when given 0', function (){
  factorial(0).should.equal(1);
});

it('should return 1 when given 1', function (){
  factorial(1).should.equal(1);
});

TDD 例子:

test('equals 1 for sets of zero length', function (){
  assert.equal(1, factorial(0));
});

test('equals 1 for sets of length one', function (){
  assert.equal(1, factorial(1));
});

Cypress 提供了两个方法来断言:

(1) 隐式断言.should().and(),Cypress推荐该方式。

it('基础设置', function () {
  // 展开子菜单
  cy.contains('基础设置').click();
  cy.get('.menu-wrapper')
    .should('contain','营销年度设置')
    .and('contain','产品价格管理');
});

(2) 显式断言expect

expect允许传入一个特定的对象,并且对它进行断言。

it('基础设置', function () {
  // 展开子菜单
  cy.contains('基础设置').click();
  expect('营销年度设置').to.exist;
  expect('产品价格管理').to.exist;
});

参考:

  • https://github.com/jdavis/tdd-vs-bdd
  • https://joshldavis.com/2013/05/27/difference-between-tdd-and-bdd/
  • https://www.cnblogs.com/poloyy/p/13744006.html

# 8.5. 条件测试

在实际的测试过程中经常遇到一个场景"判断一个元素是否存在,如果存在则执行A操作,如果不存在则执行B操作",在Cypress中这种场景叫做条件测试。

在Cypress中条件测试被认为是测试执行不稳定的因素​​,在Cypress中建议通过指定前置测试条件,来避免不确定行为。也就是说当有A、B两个策略时,指定测试前置条件从而让A或B一定发生。

下面是错误的使用方式,因为get自带断言,所以下面元素不存在,会直接导致用例失败,不会走else

if (cy.get('.message-item')) {
    console.log('Message');
  }

可以用find

cy.wait(1000); // body元素会在页面全部渲染完成前获取到,可以加入等待时间

cy.get('body').then((body) => {
  if (body.find('.message-item').length) {
    console.log('1');
  } else {
    console.log('2');
  }
});

还可以通过includes,判断是否包含特定文案:

cy.get('.room-page-list').then((list) => {
  if (list.text().includes('小助手')) {
    cy.contains('小助手').click();
  }
});

# 8.6. 前进&后退

//后退
cy.go('back')
cy.go(-1)

//前进
cy.go('forward')
cy.go(1)

也可以:

cy.window().then((window) => {
  window.history.go(-1);
});

# 8.7. 页面滚动

滚动页面,直到找到包含“报名中”文案的元素:

function scrollIndex(index, max) {
  if (index > max) return;

  cy.get('.scroll-view').scrollTo(0, 1000 * index);
  cy.wait(300);

  cy.get('.tip-match-item.list-press').then((body) => {
    const text = body.text();
    if (!text.includes('报名中')) {
      scrollIndex(index + 1, max);
    }
  });
}

scrollIndex(1, 14);

# 8.8. 多用例共享url

首先要知道每个测试用例it,都会打开一个空白的网页。

有时候需要多用例共享url,一开始采取的是创建比赛后,每次回到首页去找赛事列表然后点击的方式,这种方法的缺点是只能同时进行一个环境&分支的测试,如果多分支同时进行,就会互相扰乱。

后面采用的url保存到文件的方式,代码如下:

Cypress.Commands.add('setNewGameUrl', (key = 'url') => {
  cy.url().then((url) => {
    cy.writeFile(`cypress/fixtures/${URL_PREFIX}${key}.json`, { url }, { flag: 'w+' });
  });
});

Cypress.Commands.add('visitNewGameUrl', (key = 'url') => {
  cy.fixture(`${URL_PREFIX}${key}`).then((data) => {
    cy.visit(data.url);
  });
});

url的理解如下,对于测试用例,url携带了3类信息:

  • 站点baseUrl
  • 当前路由
  • 登录态信息,query的一类

# 8.9. Toast文案的验证

比如切换用户角色后,提示信息"选择角色成功":

cy.contains('选择角色成功')
  .should('be.visible');

或者:

cy.get('.van-toast')
  .contains('选择角色成功')
  .should('be.visible');

# 8.10. 异步

Cypress中的操作是默认异步的,即使没使用await

let text = '';

cy.get(selector).then((el) => {
    text = el.text();
  });


// 下面获取语句将始终为空,因为上面的get方法是异步的
console.log('text');

# 8.11. 别名

用别名可以解决一些变量复用的问题。

const ref = {};
it('Test',() => {
  findNotByeTeams(0, (el) => {
    cy.get(el).click();
    ref.selector = el;
  })

  // ...

  // ref.selector为undefined,找不到元素
  cy.get(ref.selector).click();
});

selector并没有赋值成功,解决办法有多种,可以将下面的语言放在then方法中,也可以使用别名。

it('Test',() => {
  findNotByeTeams(0, (el) => {
    cy.get(el).as('el')
      .click();
  });

  // ...

  cy.get('@el').click();
})

参考:

  • https://www.5axxw.com/questions/content/7zwqb4
  • https://www.cnblogs.com/landhu/p/15753437.html

# 8.12. 失败后重试

cypress.config.js配置如下:

const { defineConfig } = require('cypress');

module.exports = defineConfig({
  retries: {
    runMode: 3,
    openMode: 3,
  },
})

另外,bash脚本层面可以控制全部任务的重试次数:

function runTest() {
  npm run report:remove

  if [[ -z "$PROJECT" ]];then
    npx cypress run 
  else
    npx cypress run --spec "cypress/e2e/${PROJECT}/**/*" 
  fi;
}

runTest || runTest || runTest || true;

这种重试是不好的方式,会导致成功的用例也被执行多次,这种缺点会随着用例数的增大而放大。

最理想的情况是各个测试用例没有耦合关系,这样的话只用上面的方式就可以了。

参考:https://developer.aliyun.com/article/914948

# 8.13. Popover一闪而过问题

有时候发现左上角的popover一闪而过,一开始以为是click方法问题,尝试了cypress-real-events,并没有解决。

后面发现等待几秒后再点击就没问题了,其实这里原因是组件状态被改变了,也就是组件更新了数据,popoverprops发生了变更。

解决方法是什么呢,这里采用是等待页面中最慢的数据返回,然后再点击popover

function clickSetItem(text) {
  cy.get('.iconfont.icon-set').click();
  cy.get('.set-item').contains(text)
    .click();
}

it('Set Privacy Recruit', () => {
  cy.get('.press-message-board-item__comment-content').contains('欢迎')
    .should('exist');

  clickSetItem('设为私密招募');
})

# 8.14. 环境变量传递

nodejs中的环境变量无法应用到测试用例中,比如process.env.XXX,可以采用下面的方式进行传递:

// cypress.config.js
const { defineConfig } = require('cypress');

module.exports = defineConfig({
  env: {
    CI_PROJECT: process.env.CI_PROJECT,
  },
});

用例中这样使用:

function getBaseUrl(key) {
  const CI_PROJECT = Cypress.env('CI_PROJECT') || '';

  //...
}

# 8.15. 拦截请求

这里的场景是获取i18n文件,拿来复用一些中文用例。

cy.intercept(/pt-BR.js/).as('i18n');
cy.visit('https://qq.com');
cy.wait('@i18n').then((res) => {
  // 这里的data就是返回的数据
  const data = resp.response.body || '';
});

cy.intercept的用途非常多,可以用来监听请求、修改请求头/请求体、修改返回数据等,具体可以看文档。

参考:

  • https://docs.cypress.io/api/commands/intercept

# 8.16. 多语言处理

海外版本和国内版本的测试用例分成两个项目,内部有一些共用的用例,其中会涉及语言的查找和断言等。

这块的处理方式是只给中文套了个$t,其会在海外版本测试时寻找对应的外语,比如:

it('Switch Tab', () => {
  cy.get('.van-tab.van-ellipsis').contains($t('参赛'))
    .click();
  cy.contains($t('赛事创建数')).should('not.be.visible');
  cy.contains($t('冠军次数')).should('be.visible');
}) 

$t会判断当前执行的测试用例是否是海外的用例文件,如果不是直接返回key,否则返回对应的外语。

let i18nData = null;
function $t(key) {
  if (Cypress.spec.relative.indexOf('xxxx') === -1) return key;
  if (i18nData) return i18nData[key] || key;
  return key;
}

Cypress.spec会输出用例文件的相关信息,下面是一个例子:

{
  absolute: "/xxx/cypress/e2e/xx-project/03.homepage.cy.js"
  baseName: "03.homepage.cy.js"
  fileExtension: ".js"
  fileName: "03.homepage"
  id: "xxx"
  name: "03.homepage.cy.js"
  relative: "cypress/e2e/xx-project/03.homepage.cy.js"
  specFileExtension: ".cy.js"
  specType: "integration"
  __typename: "Spec"
}

i18nData是从哪来的呢?它是从页面中拦截的i18n配置文件,经过解析并放在了fixtures中。

Cypress.Commands.add('saveI18n', () => {
  cy.intercept(/pt-BR.js/).as('i18n');
  cy.visit('https://xxx.com/#/');
  cy.wait('@i18n').then((res) => {
    saveI18nData(res);
  });
});

function saveI18nData(resp) {
  let data = resp.response.body || '';

  try {
    data = data.replace(/^\s*window\[.*?\]\s*=\s* /, '').replace(/;$/, '');
    data = JSON.parse(data);
  } catch (err) {}

  i18nData = data || {};
  cy.writeFile(`cypress/fixtures/${FILENAME}.json`, data, { flag: 'w+' });
}

为了保证$t调用的时候i18nData已经赋值了,可以将拦截的操作放在before中.

before(() => {
  cy.saveI18n();
});

这一系列操作的目的是,让海外和国内版本复用测试用例,为什么不直接用同一个项目呢,而是在底层,也就是用例层面适配呢?

因为海外和国内版本用例之间的异是大于同的,如果放在一个项目中,会写大量的if-else,这会让项目难以维护。

现在的做法是将公用的用例原子化,哪个项目想用都可以,只要做好适配即可。

再想一下,如果将来对横版项目做测试,是否该新建项目呢?答案应该是看异同点多少,如果差异点过多,需要写太多的if-else,就应该分离项目。

# 8.17. 登录态保存

如果不想每个用例都重新登录,可以将cookie保存起来,Cypress 中是 session 的概念,比如:

cy.session(name, () => {
  cy.visit('https://xxx.com/#/');
  cy.get('.uni-input-wrapper input[type!="password"]').type(accountName);
  cy.get('.uni-input-wrapper input[type="password"]').type(password);

  cy.get('.press-button--e-sport-primary').click();
  cy.get('.press-button--e-sport-primary').should('not.exist');
});

之前的设置cookie白名单的方式已经废弃:

Cypress.Cookies.defaults({
  whitelist: /^tip/,
});

# 9. 其他问题

下面是一些开发中遇到的问题,包括部署、后台等多方面,持续记录中。

# 9.1. mochawesome报错

invalid reporter '[object Object]'
TypeError: invalid reporter '[object Object]'
    at createInvalidReporterError (<embedded>:3240:183869)
    at k.reporter (<embedded>:3245:3431)
    at new k (<embedded>:3245:1480)
    at T.setRunnables (<embedded>:5179:15043)
    at Object.onTestsReceivedAndMaybeRecord (<embedded>:5226:425262)
    at p.<anonymous> (<embedded>:5226:66755)
    at p.emit (node:events:527:28)
    at p.emitUntyped (<embedded>:4937:84346)
    at <embedded>:4937:91863

解决办法:删除 mochawesome.json

参考:https://github.com/cypress-io/cypress/issues/3426

# 9.2. 中文字体设置

镜像需要有中文字体包,即使echo $LANGzh_CN,locale命令有中文,也不可以,另外设置 window.navigator.language 这种方式不可以。

先下载中文字体到git仓库,然后复制到linux中。

cp tests/asset/fonts/simfang.ttf /usr/share/fonts

参考:

https://stackoverflow.com/questions/56791796/how-to-set-the-browsers-language-in-cypress-io-electron-chrome

# 9.3. mongodb 根据默认_id删除

db.oneDb.deleteMany({_id:"someId"}) //错误

const { ObjectId } = require('mongodb');
db.oneDb.deleteMany({'_id':ObjectId("someId")}) // 正确 

# 9.4. mongodb 聚合

根据projectprojectIdsubProjectbranch聚合,并排序的例子:

bundleDB.aggregate = async ({
  start, limit, sort, platform,
}) => {
  const db = await mongo.connect();
  const startAndLimit = start != null && limit ? [
    {
      $skip: start,
    },
    {
      $limit: limit,
    },
  ] : [];

  const sortInfo = sort ? [
    {
      $sort: sort,
    },
  ] : [];

  let platforms = [1, undefined];
  if (platform == 2) {
    platforms = [2];
  }

  const result = await mongo.aggregate(db, COLLECTION, [
    {
      $match: {
        branch: { $in: ['develop', 'release'] },
        platform: { $in: platforms },
      },
    },
    {
      $group: {
        _id: {
          projectId: '$projectId',
          project: '$project',
          subProject: '$subProject',
          branch: '$branch',
        },
        info: {
          $last: '$$ROOT',
        },
        branch: {
          $last: '$branch',
        },
        mainBundleSize: {
          $last: '$mainBundleSize',
        },
        totalBundleSize: {
          $last: '$totalBundleSize',
        },
        createTime: {
          $last: '$createTime',
        },
      },
    },
    ...sortInfo,
    ...startAndLimit,
  ]);

  db.close();
  return result;
};

上面代码的顺序是:

  • 筛选
  • 分组
  • 排序
  • 截取

下面代码是求某种类型的数据总量:

operationsDB.static = async (aggregateType, match = {}) => {
  const db = await mongo.connect();
  const res = await mongo.aggregate(db, COLLECTION, [
    {
      $match: match,
    },
    {
      $group: {
        _id: aggregateType,
        count: { $sum: 1 },
      },
    },
  ]);
  db.close();
  return res;
};

可以这样使用:

// 按照操作者分组
const dataBaseOperator = await operationsDB.static('$operator', {
  timestamp: {
    $gt: startTimeStamp,
    $lt: endTimeStamp,
  },
});

// 按照操类型分组
const dataBaseType = await operationsDB.static('$type', {
  timestamp: {
    $gt: startTimeStamp,
    $lt: endTimeStamp,
  },
});


const match = {
  timestamp: {
    $gt: startTimeStamp,
    $lt: endTimeStamp,
  },
};
if (type && type !== 'ALL') {
  match.type = type;
}

// 按照某一天+某类型聚类
const dataBaseDate = await operationsDB.static({
  $dateToString: {
    format: '%Y-%m-%d',
    date: { $add: [new Date(0), '$timestamp'] },
  },
}, match);

# 9.5. pm2列表为空,但服务在运行

这个问题比较奇葩,出现场景:在pm2 restart过程中杀掉进程,然后pm2 list就为空了。

目前采用的方法是重启服务。

参考:

  • https://github.com/Unitech/pm2/issues/3528
  • https://www.jianshu.com/p/d0d628c866a9

# 9.6. puppeteer实现录屏

如何实现录制?用rrweb,注意测试页面本身并不引入,可以使用动态挂载JS的方式。

# 9.7. chrome允许跨域

chrome允许跨域,最后的目录是新的就行。

open -n /Applications/Google\ Chrome.app/ --args --disable-web-security  --user-data-dir=/Users/mike/Documents/MyChromeDevUserData

# 9.8. 流水线变量

流水线中的变量,比如BK_CI_开头的变量,会被自动注入到JS的process.env属性中。

如果将来流水线改版,process.env.ANY_ENV取不到值的话,可以采取下面的方式:

  1. 流水线中注入所需的环境变量到.env.local
  2. js脚本中读取.env.local文件,并取出环境变量赋值到process.env下。

# 9.9. echarts切换tab后宽度变小,只有100px

这个可以通过监听resize事件,并主动触发resize事件来解决。

methods: {
  initChart() {
    // ...
    option && myChart.setOption(option);
    window.addEventListener("resize", () => {
      myChart.resize();
    });
  },
  handleClick() {
    this.$nextTick(function () {
      const myEvent = new Event("resize");
      window.dispatchEvent(myEvent);
    });
  },
}

参考:https://blog.csdn.net/weixin_65793170/article/details/127608700

# 9.10. jest 按顺序执行

设置maxWorkers

参考:https://www.shuzhiduo.com/A/1O5EWRkWz7/

# 10. 后记

自动化测试本身是大有可为的,对于减少线上bug、增强敏捷开发的信心十分重要。只是需要开发人员对工具更加熟悉,对项目的设计更加合理,否则成本就会逐渐大于收益。

基本上规范的大型项目都有自动化测试,难维护不是放弃自动化测试的借口。

参考:https://blog.csdn.net/liudinglong1989/article/details/107012639