Jsonz bug-log

后续更新会放在 github.com/jsonz1993/blog

0%

自动化功能测试流程方案

Introduction

得益于谷歌开源了 puppeteer 无界面版的 Chrome nodeJs api。
现在前端可很方便快捷的开发一些脚本去跑浏览器端的操作,包括自动化测试,爬虫或其他比较机械化的操作。
可以参见我上一篇文
自动化测试 puppeteer 与qq空间

ps: 🤦‍ 早知道当时删微博就直接写一个脚本,人工去删还漏了一些被批斗了一顿

既然 puppeteer 可以拿来做这么多事情,那前端是不是可以整合出一套流程测试的方案?

大概的流程是:

  • 定时跑业务流程
  • 成功则发送一条消息通知服务器
  • 失败则 截图发送截图与其他信息
    • 到达一定的阈值就通知相关人员排查问题
  • 提供一个前端页面可以查询 任务情况
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
                                        +-------------------+
+-----------------------------> | 记录消息 |
| success | |
| +------+------------+
| |
+-------+------+ |
| | v
| 定时跑测试任务 | +-----+-----------+
| | | |
| | | |
+------+-------+ | 查看任务记录 |
| | |
| | |
| +-----+-----------+
| ^
| |
| error +-----+----------+
| | |
+-------------------------------> | 记录消息 |
| |
| |
+-------+--------+
|
|
| 阈值
|
+--------v---------+
| |
| |
| 通知人员 |
| |
| |
+------------------+

用到的技术栈

自动化测试: puppeteer + axios + node-schedule
后台: egg + mongoose
前端界面: antd + dva

其实主要就 puppeteer + egg + mongoose 即可,前端界面只是刚好公司一个后台是用 antd-pro 写的 所以顺手带上去而已 = =#

本地一些环境:node 8.9.4, npm 5.6.0, oxs 10.13.3, 编辑器 vscodeeeeee

以下涉及到公司项目业务的 都会略过讲一下过程 不会出现具体代码或截图等…

自动化测试(auto_test)部分

目录结构:
auto_test_project

入口: app.js 主要处理一些获取自动化测试浏览器的对象, 项目启动, 定时任务启动

app.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
const mMonitor = require('./scripts/m');  // m端测试脚本入口
const xDate = require('xdate');
const login = require('./scripts/login'); // 登录操作处理
const { scheduleDelPic } = require('./scripts/schedule'); // 定时任务

// 项目入口
;(async()=> {
// 因为公司项目需要一些前置的 操作才能访问,比如登录管理后台等操作,
// 所以这里直接用一个文件把这一块抽出来,方便后面专心处理业务 不用关心用户登录 权限 测试账号等问题
// 处理完直接返回一个 browser 对象,后面的其他测试只需要在当前的 browser 新开一个tab去跑即可
const browser = await login();

// m端项目
mMonitor(browser); // 传入browser 对象 开始处理自动化测试

// 10分钟跑一次,这里也可以把间隔抽出来放到config里
// 多嘴说一句,用setInterval 有一个弊端就是,前面执行脚本堵塞,会造成 一个任务跑完 直接跑下一个,中间不是间隔10min。 看具体的业务需求,也可以用 setTimeout 或者递归等去执行
setInterval(() => mMonitor(browser), 1000 * 3600 * 1);

// 其他的任务 定时任务等
// 这里是写了个定时清理图片
scheduleDelPic();
})();

前置处理: login.js (这里可以取其他名字)

对于我这个项目来说 在开始跑之前要处理测试账号登录等问题,因为考虑到后面会有多个任务在跑的话,直接在 browser 对象 create new tab 可以同步跑,而不用每次都去处理登录问题。

login.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
const puppeteer = require('puppeteer');

module.exports = async function () {
const browser = await puppeteer.launch({
headless: false,
devtools: true,
slowMo: 100,
ignoreHTTPSErrors: true,
});

// 处理一些其他的逻辑
// ...
// 把处理完的 browser 返回回去
return browser;
}

mMonitor 移动端测试任务

主要处理的事情有:

  • 调用其他的流程测试任务,比如我是: 公司项目的主流程下单任务
  • 处理一些需要记录的信息,比如从什么时候跑,什么时候结束
  • 处理与服务器的交互,成功调接口
  • 失败截图保存用户信息等 传递给服务端
m.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
module.exports = async function mMonitor(browser) {
// 建立一个 page 的引用,方便后面可以调用方法
let page;

try {
startTime = Date.now();

page = await browser.newPage();

// 把页面传入主流程的测试任务
// 这里面就有超级无敌大量的 业务代码... emmm 根据项目不同来写 完全没有参考意义就不写了
await mainProcess(page);

// 测试任务执行成功的处理
const params = {
// 测试结果数据
}

// 调接口存数据
const data = await mRequest(params);
console.log('上报success接口成功', params);

} catch(e) {
// 跑测试流程中错误的对应处理

const params = {
// 错误结果的数据, 比如
// 截图地址url
// title
// url
// serviceCode 等等
}

const data = await mRequest(params);
console.log('上报error 接口成功', params);
} finally {
// 关闭当前页面
page.close();
// 其他操作...
}
}

schedule 定时任务

这里的定时任务用的是 node-schedule 非常好用 支持 Cron-style

schedule.scheduleJob(cronStyle)

cronStyle 参数 传入对应的时间,既可按照传入的参数 定时去执行,具体可以看👆链接

1
2
3
4
5
6
7
8
9
10
11
cronStyle: 

* * * * * *
┬ ┬ ┬ ┬ ┬ ┬
│ │ │ │ │ │
│ │ │ │ │ └ day of week (0 - 7) (0 or 7 is Sun)
│ │ │ │ └───── month (1 - 12)
│ │ │ └────────── day of month (1 - 31)
│ │ └─────────────── hour (0 - 23)
│ └──────────────────── minute (0 - 59)
└───────────────────────── second (0 - 59, OPTIONAL)

目前 auto_test 用到的定时任务只有一个

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
const schedule= require('node-schedule');
const path = require('path');
const fs = require('fs');

//删除某个目录的一层文件
function delPathFile(path) {
if (!fs.existsSync(path)) return;
const files = fs.readdirSync(path);
files.forEach(file => fs.unlinkSync(`${path}/${file}`));
}

// 定时删除图片,每周一一次
exports.scheduleDelPic = function() {
const initPath = path.join(process.cwd(), '/static/init');
const errorPath = path.join(process.cwd(), '/static/error');
const successPath = path.join(process.cwd(), '/static/success');

schedule.scheduleJob('* * * * * 1', function () {
delPathFile(initPath);
delPathFile(errorPath);
delPathFile(successPath);
});
}

总的来说 auto_test 做的事情大概就是

  • 启动 app.js, 项目就会 10分钟自动跑一次测试程序。
  • 成功、失败后做出对应的操作。
  • 每周清除一次本地的截图数据。

但是几个弊端

  1. puppeteer 是模拟浏览器,所以你的所有行为都是模拟用户操作:选择dom,点击、填写、选择操作。这些都是很容易因为页面的dom变化而失效,比如本来是选了一个 #userId的文本框,后面迭代把 #userId改为#uuid 那你就选不到了。 所以业务测试代码(__mainProcess__模块) 做好模块拆分,后面跟项目迭代也方便调整。
  2. 因为网络或dom操作经常触发异步行为,所以业务测试代码里面充满各种 waitFor waitForSelector waitForNative 等…不注意可能就会操作到没有出现的元素,这块要做好控制,比如waitFor 时间久一点,或者在page上封装一个方法,比如 page._click:
    1
    2
    3
    4
    async _click(selector) {
    await page.waitForSelector(selector, { visible: true });
    return page.click(selector);
    },
    在每次点击之前,先等待该元素出现再点击。这样可以避免很多运行时 没有找到元素的错误
  3. 模仿用户行为测试的话,意味着你要做一些前置的处理 如login.js, 要把账号密码等写到代码里面…虽然可以加密 不过还是会存在部分泄露风险…

自动化测试服务端(auto_test_backend)部分

对比 auto_test 部分, auto_test_backend才是我最头疼的地方。
毕竟不是科班出身的前端,对后端思想 以及数据库操作也没碰过= =emmm 做起来真的是超级无敌的费劲
特别是 设计数据格式的时候,都是拍脑袋决定了。 咦 我加个字段, 咦 我改个字段 又不是很清楚怎么快捷操作…. 估计是不熟 mongoose 的原因….

这个项目用的是 egg 框架,所以基本项目结构都是按着egg的规范来。

目录结构:
auto_test_backend_project

egg 帮我们做了很多事情,开发的时候只要跑一下 npm run dev,就会启动一个默认端口为7001的服务

对我们现在来说,基本只是提供 RESTAPI。
剩下的就简单啦,在app/router.js写对应的路由 如:
router.post('/api/v1/monitor', app.controller.monitor.create); 这样如果有post请求:7001/api/v1/monitor 就会交由 monitor.create 这个 controller 处理。

路由配置 app/router.js

目前路由配置比较简单,就配置了两个,一个用来处理接收 auto_test项目中测试结果;一个用来输出测试列表。

app/router.js
1
2
3
4
5
6
7
8
module.exports = app => {
const { router, controller } = app;

// post 请求交由app.controller.monitor.create处理
router.post('/api/v1/monitor', app.controller.monitor.create);
// get 请求交由app.controller.monitor.get处理
router.get('/api/v1/monitor', app.controller.monitor.get);
};

controller 解析用户的输入,处理后返回相应的结果

egg帮我们做了封装,直接在controller文件夹下起文件,写对应的数据,后面可以从全局对象app.controller.fileName.mothedName去获取对应的方法,比如现在有这个文件 app/controller/test,那么我可以在其他地方通过 app.controller.test去获取。

这里拿 app.controller.monitor.get 举例

monitor.get 方法要做的事情有

  • 验证结构的请求参数规则
  • 处理查询参数
  • 处理分页的问题
  • 调service 获取数据
  • 处理数据
  • 调用rest中间件发送数据

验证参数,这里用到的是 egg-validate插件,直接配置就可以用了

config/plugin.js
1
2
3
4
exports.validate = {
enable: true,
package: 'egg-validate',
};
app/controller/monitor.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
const Controller = require('egg').Controller;

module.exports = class MonitorController extends Controller {
const ctx = this.ctx;

// 这里验证失败会抛出一个 422 的异常
ctx.validate({
start: { type: 'number', require: false},
// end, state, pagination等等其他参数验证
});

// 获取传过来的参数
const { start, end, state, pagination: _pagination} = ctx.query;

// 根据自身业务 处理查询参数
(typeof state !== 'undefined') && (params.state = state);
if (start && end) {
Object.assign(params, {
time: {
$gte: start,
$lte: end,
}
});
}

// 处理分页参数
const paramsPagination = Object.assign({ size: 20, page: 1 }, JSON.parse(_pagination || '{}'));

// 调 service 获取数据
// service也和 controller 一样,egg做了文件的映射,直接 ctx.service能很方便的去获取
const { list, pagination } = await ctx.service.monitor.find({ params, ...paramsPagination });

// 处理数据
list.forEach(item => {
item.img_url = `http://localhost:7001${item.img_url}`
});

// 调 rest中间件 处理返回数据
ctx.rest({
list,
pagination,
});
}

service 处理一些和数据库交互的逻辑

service和controller 一样,egg帮我们做了一些文件的映射,所以也是直接在app/service/monitor.js写对应的逻辑即可

这一块应该是最麻烦的, mongoose api 不熟,也不确定这么写会不会最优,合不合理等…有时间或者以后有机会 会去看《SQL必知必会》 学学数据库这一块软肋。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
const Service = require('egg').Service;

module.exports = class MoitorService extends Service {
async find({params, size, page}) {

// 查复合条件的页数
const count = await this.app.model.Monitor
.find(params).count();

// 处理分页
const monitors = await this.app.model.Monitor
.find(params).sort('time').select('-__v')
.skip(size * (page-1))
.limit(size).lean();

// 处理要返回的格式
return {
list: monitors,
pagination: {
count, size, page,
}
};
}
}

middleware 中间件

egg 其实基于koa实现的,所以对中间件形式和koa一样是洋葱圈模型。

集成中间件也很简单,在app/middleware/下写对应的中间件,再在 config/config.default.js下配置即可启用。

写中间件

app/middleware/error_handler.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
module.exports = ()=> {
return async function errorHandler(ctx, next) {
try {
await next();
} catch (err) {
// 所有异常都在 app 上触发一个 error 事件
ctx.app.emit('error', err, ctx);

const status = err.status || 500;
// 生产环境时 500 错误的详细错误内容不返回给客户端,可能包含敏感信息
const error = status === 500 && ctx.app.config.env === 'prod'
? 'Internal Server Error'
: err.message;

// 从 error 对象上读出各个属性 设置到响应中
ctx.body = { error };
if (status === 422) {
ctx.body.detail = err.errors;
}
ctx.status = status;
}
}
}

添加配置

app/config/config.default.js
1
2
3
4
5
6
7
8
9
10
11
12
13
module.exports = appInfo=> {
const config = {
// 中间件配置
middleware: ['errorHandler'],

// errorHandler配置,只对 /api 开头的路由做处理
errorHandler: {
match: '/api',
}
}

return config;
}

阈值发送邮件功能

这一块要维护两个临时变量数组,三个配置参数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
const temp = {};
temp.errorObj = {
errorArray: [], // 用来存每次错误上报的时间戳
sendArray: [], // 用来存每次发送的时间戳
}
const errorConfig = {
maxCount: 5, // 单位时间超过n次就报警
unitTime: 30, // 单位时间为:n分钟
maxSend: 2, // 单位时间最多发n封邮件
}

大概的逻辑是, 每次报错的时候, errorArray 塞入一个时间戳,然后判断:
1. 判断是否到达发送阈值
2. 根据当前的时间戳和配置来清理过期的数据
3. 判断单位时间内是否达到发送阈值
4. 判断单位时间内是否超过发送次数
5. 构建发送内容, 🚀 处理后续的记录操作

用到的发邮件插件是 nodemailer 简单粗暴 用过都说好

and

其实 auto_test_project 最麻烦的是在定义数据库格式的时候,经常定义少一些关键的字段(目前肯定也还存在这种情况的)。
还有就是本来以为和 angular 一样,controller 会塞大量的业务逻辑, 等到后面有一个场景是要在某个 controller调用另一个controller 时才发现,原来 controller 主要的职责是负责处理路由和一些参数验证输出等比较对外的工作,对内的基本都写成了service

前端的小伙伴很多人懂node 可能也只是懂node的语法层 要想真的写后台或者微全栈,还是有很多东西要学的。

列表展示(admin)项目

列表展示相对简单,没什么好讲的。想用什么技术栈都比较随意,react,vue甚至hbs或者直接在js里面写html字符串循环都ok。

这里推一下一篇文章,介绍工作中的一个项目 react全家桶 && dva最佳实践

end

跑自动化测试脚本(auto_test)的log
log.png

自动化测试成功后提交数据(admin)
success

自动化测试失败后将数据上报(admin) 包含了标题,报错信息,链接,截图,标记等
admin

文章作为学习的记录到此结束,这个项目主要学习了 egg,mongodb,puppeteer 等 还是挺有收获的.