[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存储录屏,失败通知发送给机器人。


关于机器人的通知策略,这里也描述下。
- 成功
- 每天早上9点发送到组内群
- 每天10点、14点、16点发送给自己
- 失败
- 发送给订阅机器人的所有群
为什么成功的用例还要通知呢?是为了同步进展,确保测试任务正常运行。

# 3.5. 分项目执行
多个项目放在一个仓库,如何在流水线中分项目执行呢?
这里采用的方式是流水线中传入变量key
,拉取配置获取key
对应的项目,测试命令中只执行对应的目录下的用例。
流水线变量 --- 配置文件 --- 测试命令
分项目执行的好处是方便管理,以及为后面的多分支、多环境执行做准备。
# 3.6. 多分支执行
多分支、多环境其实就是多了一些变量,如baseUrl
、title
等,这个可以在配置中心保存,然后采用的还是用上面的方法:
流水线变量 --- 配置文件 --- 测试命令
此外,多分支同时执行要特别注意互相影响的情况,比如互相抢IM的登录态,获取最新消息被覆盖,首页赛事第一条被覆盖等情况。
# 3.7. 顺序测试
有些测试用例之间是有顺序的,比如创建赛事、加入队伍、结束报名、改判比分,这几个操作需要顺序执行,有两个方案:
- 可以将它们都放到一个文件中,缺点是文件将变得臃肿,难以维护。
- 给文件排序,或者重命名文件,让它们执行的顺序等于业务需要的顺序。
这里采用的是第二种,规范如下:
对于需要特定执行顺序的测试文件,文件名以01
、02
、03
等开头。
对于同一个业务中不需要顺序执行的文件,文件名以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
,类似的还有appnium
、selenium
。
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
,并没有解决。
后面发现等待几秒后再点击就没问题了,其实这里原因是组件状态被改变了,也就是组件更新了数据,popover
的props
发生了变更。
解决方法是什么呢,这里采用是等待页面中最慢的数据返回,然后再点击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 $LANG
为zh_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 聚合
根据project
、projectId
、subProject
、branch
聚合,并排序的例子:
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
取不到值的话,可以采取下面的方式:
- 流水线中注入所需的环境变量到
.env.local
中 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