一个月前看了 monkey 大神的 stf 修改帖子后心生想法, 能不能根据 STF 定制一个符合我司测试需求的平台, 在上面跑一些自动化测试以及查看结果.
怀着这个想法, 啃了一个月的源码. 从最初的摸不着头脑到现在的略有领悟, 终于改造了一个简单的测试平台.
把经验和看法分享给大家. 第一次发帖, 有不足的地方还望大家指正,能多多交流
关于 STF 环境搭建和运行我就不多说了, 可参考帖子:
在这简单介绍下 stf 的结构,因为我刚开始接触就很摸不着头脑, 希望能对刚接触 STF 的人一些帮助吧
STF 代码主要有两个目录,lib 和 res. lib 里放的是后端的代码, res 里放的是前端的代码.
STF 启动运行命令是 stf local
, 实际上是 node lib/cli.js local
.
也就是说 lib 下 cli.js 是 stf 后台的入口,local 是传入的参数. cli.js 中启动了一些后端的服务
以下是启动 app 服务代码:
// app
, procutil.fork(__filename, [
'app'
, '--port', options.appPort
, '--secret', options.authSecret
, '--auth-url', options.authUrl || util.format(
'http://%s:%d/auth/%s/'
启动方式就是调用了 procutil 的 fork(),fork 里是启动了一个 child_process,在这就不细说了.
首先我想在页面最上方增加一个 test 的标签, 在 res/app/menu/menu.jade 里添加:
a(ng-href='/#!/devices', accesskey='1')
span.fa.fa-sitemap
span(ng-if='!$root.basicMode', translate) Devices
a(ng-href='/#!/apptest')
span.fa.fa-bug
span(ng-if='!$root.basicMode', translate) Test
增加了一个和 Devices 一样的标签, 修改了其中的图标为 fa-bug, 其实 STF 大多数的 icon 都是借用的 font awesome 里的 icon, 传送门:
在 res/app 目录下新建 apptest 文件夹,
在 index 中配置 route:
.config(function($routeProvider) {
$routeProvider.when('/apptest', {
template: require('./app-test.jade')
})
})
标签添加完成,在 test 标签下, 我想做两个子页面, 一个用于配置测试的, 一个展示测试状态列表,在 app-test.jade 中添加
div(fa-pane,pane-id='test-tabs')
.widget-container.fluid-height
nice-tabs(key='ControlBottomTabs', tabs='appTestTabs', filter='')
div(fa-pane, pane-anchor='south', pane-size='50%', pane-handle='4').pane-bottom-p
.widget-container.fluid-height
status-Directive(tests='tests',columns='columns')
为配置测试页增加 crawl 和 smoke 两个 tab:
$scope.appTestTabs = [
{
title: gettext('Crawl'),
icon: 'fa-refresh fa-fw',
templateUrl: 'apptest/crawl/crawl.jade'
},
{
title: gettext('Smoke'),
icon: 'fa-rss fa-fw',
}
]
效果如下图:
statusDirective 是状态列表的 directive, 之所以放在 app-test.jade 里,是想切换 tab 时,状态列表始终展示.
在 directive 中可以进行适时的数据更新,该处是仿照 device 列表写的,
function createRow(test) {
var id = test.id
var tr = document.createElement('tr')
var td
tr.id = id
for (var i = 0, l = columns.length; i < l; ++i) {
td = scope.testCell[columns[i].name].build()
scope.testCell[columns[i].name].update(td, test)
tr.appendChild(td)
}
return tr
}
function updateRow(test)
{
var tr = tbody.children[test.id]
if (tr) {
for (var i = 0, l = columns.length; i < l; ++i) {
scope.testCell[columns[i].name].update(tr.cells[i], test)
}
return tr
}
效果如下图:
STF 使用的数据库是 rethinkdb, rethinkdb 是以 json 方式存储数据的. 先在 lib/db/tables.js 中新建表 tests 主键为 id:
tests: {
primaryKey: 'id'
}
在 api.js 中定义操作数据库的方法
dbapi.saveTests = function(tests)
{
var data = {
testType: tests.testType
, startTime: tests.startTime
, endTime: tests.endTime
, owner: tests.owner
, serial: tests.serial
, status: tests.status
, result: tests.result
, device: tests.device
}
return db.run(r.table('tests').get(tests.id).update(data))
.then(function(stats) {
if (stats.skipped) {
data.id = tests.id
data.createdAt = r.now()
return db.run(r.table('tests').insert(data))
}
return stats
})
}
dbapi.loadTest = function(id)
{
return db.run(r.table('tests').get(id))
}
dbapi.loadTests = function()
{
return db.run(r.table('tests'))
}
有了界面, 要在后端执行我的遍历测试并返回结果,需要前后端通信.首先前端触发开始测试操作:
$scope.runTest = function()
{
var mydevices = $scope.devices
for (var i = 0; i < mydevices.length; i++) {
var mydevice = mydevices[i]
console.log(mydevice.model);
if(mydevice.selected)
{
mydevice.testing = true;
mydevice.tester = user;
$scope.control = ControlService.create(mydevice, mydevice.channel)
GroupService.invite(mydevice)
$scope.control.runCrawler(mydevice)
}
};
}
在这我给 device 新加了两个字段:测试状态 testing 和测试人员 tester, 因为在测试中的设备,我不想让它展示出来. tester 则在列表中展示
触发测试指令是用 websocket 来传达, 在 control-service 中添加
this.runCrawler = function(device)
{
return sendOneWay('runCrawler',{
device: device
})
}
this.stopTest = function(device,test)
{
return sendOneWay('stopTest',{
device: device
})
}
sendOneWay 里是向所有 client 发送 action,然后我们在后台中监听这个 action 在 lib/units/websocket/index.js 中添加:
.on('runCrawler', function(channel, data) {
controller.runTest(io,data.device);
})
.on('stopTest', function(channel, data) {
controller.stopTest(socket,data.serial);
})
controller 中是我们具体执行测试的代码:
this.runTest = function(socket,device){
var myDevice = {};
var test = createTest(device);
crawler = spawn('java', ['-jar','monkeytest/monkeytest.jar','-d','android','-p',
'com.android.text','-a','MainActivity','-s',device.serial,'-i',test.id]);
console.log(device.serial);
crawler.serial = device.serial
crawler.stdout.on('data', (data) => {
console.log(`stdout: ${data}`);
});
crawler.stderr.on('data', (data) => {
console.log(`stderr: ${data}`);
});
crawler.on('close', (code) => {
console.log(`child process exited with code ${code}`);
if(code == 0)
{
test.endTime = getNowFormatDate();
test.status = 'Finish';
dbapi.saveTests(test);
dbapi.setDeviceTestStatus(crawler.serial,test.status)
triggerTestComplete(socket,test);
}
else
{
test.endTime = getNowFormatDate();
test.status = 'Stop';
dbapi.saveTests(test);
dbapi.setDeviceTestStatus(crawler.serial,test.status)
triggerStopTest(socket,test);
}
});
var serial = device.serial;
deviceList[serial] = crawler;
dbapi.saveTests(test);
dbapi.setDeviceTestStatus(device.serial,test.status)
triggerStartTest(socket,test);
}
this.stopTest = function(socket,serial)
{
deviceList[serial].kill();
}
由于执行的是我本地上的遍历工具, 所以用了 child_process, 在 close 时对 exitcode 进行判断,来分辨测试是正常执行完或中断. 然后对数据库操作后 trigger 前端更新状态
function triggerStartTest(socket,test)
{
console.log('triggerStartTest');
socket.emit('test.start',test);
}
在这也是用的 socket, 就不细述了.
效果图:
总结一下吧 STF 二次开发还是有些难度的, 像我这种只会 java 的刚开始看想死的心都有了.
踏遍了无数的坑, 辛酸只有自己知道. 不过风雨之后见彩虹, 看到有些成果心里也满足了.
希望有改造 STF 想法的同学也不要气馁,努力必有收获!