开篇先打个小广告,在《牛刀小试-LR性能测试》那篇小文中我有说到性能测试要做到性能的原子化 这样我们把性能可以分为 前端, 网络, 中间件,App(应用),操作系统,数据库等,今天 我们来一起开发一个专门对Web前端性能自动化平台(后续可以在该版本的技术和基础上完善其他功能 比如说:接口的自动化和接口性能以及对其他层的监控数据做可视化)。
大家都懂 敲代码之前有很多事情要做并且是最重要的事情(能敲代码的人多的是,最重要的是能产出发现问题并给出解决策略的idea ),what’s this? 当然是需求分析。
我们的愿景:
实现Web前端性能测试(自动遍历所有页面) 监控每个页面加载时间段的耗时,并且统计每个页面中附加的资源(css/js/img/XmlHttpRequest) 最后 利用精美的图表作展现。
技术可行性分析:
自动遍历所有页面?没问题啊 webdriver是这块儿的利器啊。
如何统计页面加载时间呢?performance.timing绝对靠谱。
哪儿有精美的图表? 百度Echarts团队为你分忧解难。
技术选型:
这是个非常值得探讨的问题,有些Leader是综合团队现状和实施成本(新技术是需要花时间学啊);有些知乎党 就不说了 啥最新用啥 只用最新的 不用最合适的;当然 还有一些人完全是靠自己的兴趣拍脑袋决定的。
我们这里选择python+webdriver+flask+sqlite+bootstrap+jquery来完成我们这个小平台的开发,至于为嘛会选择这几种技术, 学习成本低 开发效率高 总而言之一句话 ROI高!
好嘞~ 童鞋们可以先脑补下自己想要什么样的交互页面,我这里给出一个最简单的 嘿嘿~
主页面(展示统计到的页面信息并以堆叠图展示性能数据):
页面详情页面(展示页面中附加资源的组成以及该页面历史版本的数据对比图)
对技术选型这块儿同学们可以根据自己的需要做改变
如果你不想用flask 可以换成Django框架
如果你不想用sqlite数据库 可以换成mysql或者其他的NoSql类数据库
如果你不想用jquery 那你的选择就更多了 什么React,Nodejs,Vuejs,Angularjs还有啥backbone.js眼花缭乱。
我个人喜好小而精致的东西 就拿flask来说吧 它是个微型框架远远没有Django重,但是flask丰富的插件可以供我们快速地完成任务。所以嘛 要啥Django 要啥自行车 别太奢侈
技术没有Low不Low之分! 只有适合不适合。
这里是对应软件工程里的概要设计阶段。经过前面的梳理 我们很清楚地知道自己想要的是什么了。嗯~ 接下来咱把硬邦邦的需要转化为软件工程里的东东吧。
数据库:我们建两张表分别存放所有页面的统计数据就叫WebPages,还需要建立一张表来存放每个页面的详细信息 我们叫PageDetail。
建表语句如下:
CREATE TABLE [PageDetail] (
[project] text,
[resourceName] text,
[requestType] text,
[pageRequestCount] INTERGER,
[resourcetSize] INTERGER,
[resourceLoadTime] INTERGER,
[createTime] datetime,
[pageName] text,
[pageLoadTime] INTEGER);
CREATE TABLE WebPages (project text, name text ,url text ,direct INTERGER ,domReady INTERGER, loadEvent INTERGER ,request INTERGER ,ttfb INTERGER,loadPageAll INTERGER ,ifredirect INTERGER, createTime datetime);
接口:
我们重新执行脚本来统计前端性能的话 需要调用接口。
我们就命名一个接口吧 python-flask里接口的定义很简单哦~
# 重新执行测试脚本
@app.route('/redo', methods=['GET', 'POST'])
def redo():
if request.method == 'GET':
return u'该接口不支持GET方法访问'
else:
# 向测试环境发生get请求来验证测试环境是否存在
if env_test_code == 200:
# 重新执行测试脚本
Else:
Return 测试环境不存在
如果我们选择不同版本的话 我们也需要调用接口来返回所选版本的测试数据:
@app.route('/index', methods=['GET', 'POST'])
def index():
if request.method == 'POST':
# 从ajax请求中取参数selected_version 取不到则为null
version_text = request.form.get("selected_version", "null")
else:
version_text = '默认版本号'
# 接下来巴拉巴拉从sqlite中取数据并返回
让我们开始敲代码吧? 好啊! 来吧!
接下来 我们先完成前端部分的开发工作然后再搞后台部分的任务,bootstrap &flask学起!
啥是flask?
Flask是一个使用 Python 编写的轻量级 Web 应用框架。其 WSGI 工具箱采用 Werkzeug ,模板引擎则使用 Jinja2 。
Flask也被称为 “microframework” ,因为它使用简单的核心,用 extension 增加其他功能。Flask没有默认使用的数据库、窗体验证工具。然而,Flask保留了扩增的弹性,可以用Flask-extension加入这些功能:ORM、窗体验证工具、文件上传、各种开放式身份验证技术。
环境搭建:
先用pip安装virtualenv库~ (这样做是为了单独拉一个python环境出来以便我们一台服务器上可以跑多套服务)
Dos窗口里敲命令:pip install virtualenv
然后,新建一个目录 我们就叫AutoMan吧,Dos窗口里切换到AutoMan目录后执行:virtualenv venv(创建虚拟环境目录)
我们如果要在virtualenv里安装需要的包,需要先激活虚拟环境(切到venv/scirpts里执行activate)然后再执行pip安装即可。
我们这里需要安装的环境有flask,selenium,requests。我们可以分别执行安装,也可以将flask,selenium,requests放到requirement.txt文件里然后执行pip install –r requirement.txt。
上一张我们项目的目录结构图
目录说明:
1:Web我们的项目目录
1.1:static存放静态文件的目录,它下面又分为css,img,js目录分别存放样式文件图片和js文件(下载jquery.js放到js目录下,bootstrap.css放到css目录下)。
1.2:templates目录存放的是我们的html静态页面。
1.3:views.py里定义了接口以供前端发起请求。
1.4:__init__.py里初始化了Web这个服务。
2:和Web平级的venv是我们flask虚拟环境的目录,发布的时候不需要它。
3:AutoMan.db我们用到的sqlite数据库文件。
4:config.py文件里是我们用到的配置信息,比如说页面信息,统计性能的js方法等。
5:manage.py是我们程序的执行入口。
我们先来初始化我们的应用:
在__init__.py文件里输入:
接着,我们可以尝试下在页面输出一个字符串是什么感受,嗯啊,感受!
在views.py文件里输入:
# 定义路由
@web_app.route('/')
def root():
return 'Hello Flask'
在manage.py文件里启动我们的应用:
from Web import web_app
web_app.run(host='0.0.0.0', port=5566)
启动服务(我用的是pycharm调试),在浏览器里输入http://localhost: 5566
是不是很神奇~~~~ 我们继续 补充下flask中的路由知识。
Flask中route() 装饰器把一个函数绑定到对应的 URL 上,灰常简单!
比如下面的
@app.route('/ajaxAnalyse')
def ajax_analyse():
return render_template('ajaxAnalyse.html')
我们通过浏览器访问localhost:5566/ajaxAnalyse,flask路由会指定该地址并返回ajaxAnalyse.html模板交给浏览器渲染后呈现给我们。
@app.route('/resource_<name>_<version>')
def resource_analyse(name, version):
print name
return render_template('pages_detail.html')
如果我们想在路由中使用变量,<>包含起来即可,上面这个例子中我们使用两个变量,分别是name(页面名称)和version(项目版本)。
好嘞~ 接下来就简单了,我们开始构造url路由。
首先是首页,首页的话 我们想展示所有页面的统计信息和堆叠图。所以下面的代码即可实现(我们从数据库中读取数据并和html模板一起返回让浏览器渲染)。
# 主页显示所有页面前端信息
@app.route('/index', methods=['GET', 'POST'])
def index():
if request.method == 'POST':
# 从ajax请求中取参数selected_version 取不到则为null
version_text = request.form.get("selected_version", "null")
else:
version_text = 'Syf1.1.1'
g.db = sqlite3.connect(config.db_dir)
all_page_load_data = {}
page_time_detail = []
# 从数据库中查询所有页面汇总信息
for value in ['direct', 'domReady', 'loadEvent', 'request', 'ttfb', 'loadPageAll', 'name']:
sql = 'select ' + value + " from webLoad where project='" + str(version_text) + "'" + ' order by name'
current_data = g.db.execute(sql).fetchall()
current_list = []
if value == 'name':
for data in current_data:
current_list.append(str(data).strip("(u'").strip("',)"))
all_page_load_data[value] = current_list
else:
for data in current_data:
current_list.append(int(str(data).strip('(').strip(')').split(',')[0]))
all_page_load_data[value] = current_list
# 查询每个页面的时间详细
page_detail_sql = "select name, url, loadEvent, domReady, request, ttfb, 0 as request_count, 0 as request_size " \
"from webLoad where project='" + str(version_text) + "'" + ' order by name'
page_detail_data = g.db.execute(page_detail_sql).fetchall()
for data in page_detail_data:
page = {}
page['name'] = data[0]
page['url'] = data[1]
page['loadEvent'] = data[2]
page['domReady'] = data[3]
page['request'] = data[4]
page['ttfb'] = data[5]
page['request_count'] = data[6]
page['request_size'] = data[7]
page_time_detail.append(page)
g.db.close()
return render_template("index.html", all_page_load_data=all_page_load_data, page_time_detail=page_time_detail)
咦?这里我们貌似引入了一个新词 模板,没错flask中使用Jinja作为模板。
我们一起来看下Jinja中一些常用知识点。
0:表达式
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN">
<html lang="en">
<head>
<title>My Webpage</title>
</head>
<body>
<ul id="navigation"> {% for item in navigation %}
<li><a href="{{ item.href }}">{{ item.caption }}</a></li>
{% endfor %} </ul>
<h1>My Webpage</h1> {{ a_variable }}
</body>
</html>
两种分隔符{% … %} 和 {{ … }}. 前者用于执行表达式, 后者打印表达式的结果.
1.变量
#这两种表达方式是一样的,如果属性不存在默认的设置是返回空字符串
{{ foo.bar }} {{ foo['bar'] }}
2.Filters(过滤器,我们也可以自定义过滤器)
对变量操作的函数,以|的形式写在后面
#几个例子
foo|attr("bar") <=>(等价于) foo["bar"]
2.batch(value, linecount, fill_with=None)
#从左边取3个值,fill_with为不够的情况下的填充值
{%- for row in items|batch(3, ' ') %}
4.注释Comments
#写在{# ... #}之间
{# note: disabled template because we no longer use this
{% for user in users %} ... {% endfor %} #}
5.空白 whitespace control
1.没有配置trim_blocks 和 lstrip_blocks
<div> {% if True %} yay {% endif %} </div> -> <div> yay </div>
<div> yay </div>
<div> {%+ if something %}yay{% endif %} </div>
{% for item in seq -%} {{ item }} {%- endfor %}
6.转换符Escaping
# 使用{{}} ,或者raw block {% raw %}
{{ '{{' }} {% raw %} <ul> {% for item in seq %} <li>{{ item }}</li>
{% endfor %} </ul> {% endraw %}
7.Line Statements
配置后可以mark a line as a statement
# for item in seq:
<li>{{ item }}</li> ## this comment is ignored
# endfor
这种方法同 {% for item in seq %} <li>{{ item }}</li> {% endfor %}
8.模板继承 Template Inheritance
#1.父类模板 Template
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN">
<html lang="en"> <html xmlns="http://www.w3.org/1999/xhtml">
<head> {% block head %}
<link rel="stylesheet" href="style.css" />
<title>{% block title %}{% endblock %} - My Webpage</title>
{% endblock %}
</head>
<body> <div id="content">{% block content %}{% endblock %}</div>
<div id="footer"> {% block footer %} © Copyright 2008 by <a href="http://domain.invalid/">you</a>. {% endblock %} </div>
</body>
2.子类模板继承Child Template
{% extends "base.html" %}
{% block title %}Index{% endblock %}
{% block head %} {{ super() }}
<style type="text/css"> .important { color: #336699; } </style> {% endblock %} {% block content %} <h1>Index</h1>
<p class="important"> Welcome on my awesome homepage. </p> {% endblock %}
3.还可以用self,引用block的变量
<title>{% block title %}{% endblock %}</title>
<h1>{{ self.title() }}</h1> {% block body %}{% endblock %}
Super Blocks 引用parent block的内容
{% block sidebar %} <h3>Table Of Contents</h3> ... {{ super() }} {% endblock %}
4.block 结束符也可以加上名字
{% block sidebar %} {% block inner_sidebar %} ...
{% endblock inner_sidebar %}
{% endblock sidebar %}
5.block中的变量引用
{% for item in seq %} <li>{% block loop_item %}{{ item }}{% endblock %}</li>
# 打印为空,因为block内部不能引用外面的item变量 {% endfor %}
需要改变为
{% for item in seq %} <li>{% block loop_item scoped %}{{ item }}{% endblock %}</li> {% endfor %}
6.转换符HTML Escaping
可以使用自动或者手动转换
Automatic Escaping自动转换会转换所有字符,除非在应用中设置了safe的或者后面加了|safe
Manual Escaping手动转换:加|e filter: {{ user.username|e }}. 这是escape的缩写
7.控制结构
#1.0For 循环
<h1>Members</h1> <ul> {% for user in users %}
<li>{{ user.username|e }}</li>
{% endfor %} </ul>
2.0 if/else结构
if {% if kenny.sick %} Kenny is sick.
{% elif kenny.dead %} You killed Kenny! You bastard!!!
{% else %} Kenny looks okay --- so far
{% endif %}
8.宏Macros
#1.定义一个宏
{% macro input(name, value='', type='text', size=20) -%} <input type="{{ type }}" name="{{ name }}" value="{{ value|e }}" size="{{ size }}"> {%- endmacro %}
#2.使用
<p>{{ input('username') }}</p> <p>{{ input('password', type='password') }}</p>
#3.类似一个宏里调用另一个宏,使用call block
{% macro render_dialog(title, class='dialog') -%}
<div class="{{ class }}"> <h2>{{ title }}</h2>
<div class="contents"> {{ caller() }}
</div>
</div>
{%- endmacro %}
{% call render_dialog('Hello World') %} This is a simple dialog rendered by using a macro and a call block. {% endcall %}
# 带参数 {% macro dump_users(users) -%} <ul> {%- for user in users %} <li><p>{{ user.username|e }}</p>{{ caller(user) }}</li> {%- endfor %} </ul> {%- endmacro %} {% call(user) dump_users(list_of_user) %} <dl> <dl>Realname</dl> <dd>{{ user.realname|e }}</dd> <dl>Description</dl> <dd>{{ user.description }}</dd> </dl> {% endcall %}
9.Filter sections过滤器块
可以对一个block使用filter
{% filter upper %} This text becomes uppercase {% endfilter %}
10.赋值
{% set navigation = [('index.html', 'Index'), ('about.html', 'About')] %}
{% set key, value = call_something() %}
11.Include(包含)
# 把其他tempalte的内容加进来
{% include 'header.html' %} Body
{% include 'footer.html' %}
{% include ['special_sidebar.html', 'sidebar.html'] ignore missing %}
12. import(导入)
# 如forms.html定义了两个宏
{% macro input(name, value='', type='text') -%}
<input type="{{ type }}" value="{{ value|e }}" name="{{ name }}">
{%- endmacro %}
{%- macro textarea(name, value='', rows=10, cols=40) -%}
<textarea name="{{ name }}" rows="{{ rows }}" cols="{{ cols }}">{{ value|e }}
</textarea>
{%- endmacro %}
#两种import的方法
{% import 'forms.html' as forms %} <dl> <dt>Username</dt> <dd>{{ forms.input('username') }}</dd> <dt>Password</dt> <dd>{{ forms.input('password', type='password') }}</dd> </dl> <p>{{ forms.textarea('comment') }}</p>
{% from 'forms.html' import input as input_field, textarea %} <dl> <dt>Username</dt> <dd>{{ input_field('username') }}</dd> <dt>Password</dt> <dd>{{ input_field('password', type='password') }}</dd> </dl> <p>{{ textarea('comment') }}</p>
Jinja2模板的用法非常多,我们只需要掌握常用的即可,遇到不会的直接去官网搜吧。
下面贴上我这边Index.html页面源码中所用到的Jinja:
基类模板内容:
<!doctype html>
<html>
<head>
<meta charset="utf-8">
<title>{% block title %} {% endblock %}</title>
<link rel="stylesheet" type="text/css" href="../static/css/bootstrap.min.css">
<script src="../static/js/jquery.min.js"></script>
<script type="text/javascript" src="../static/js/echarts-all.js"></script>
<script type="text/javascript" src="../static/js/bootstrap.js"></script>
<script type="text/javascript" src="../static/js/events.js"></script>
{% block head %}{% endblock %}
</head>
<body>
<nav class="navbar text-info" role="navigation">
<ul class="nav nav nav-tabs panel-title">
<li class="dropdown">
<a class="dropdown-toggle" data-toggle="dropdown" href="#">前端性能<b class="caret"></b></a>
<ul class="dropdown-menu">
<li><a href="/index">Clinical项目</a></li>
<li><a href="#">GeStorage项目</a></li>
<li><a href="#">Training项目</a></li>
<li><a href="#">PerformanceTiming学习</a></li>
</ul>
</li>
<li class="dropdown">
<a class="dropdown-toggle" data-toggle="dropdown" href="#">UI自动化测试<b class="caret"></b></a>
<ul class="dropdown-menu">
<li><a href="ui_auto">Clinical项目</a></li>
<li><a href="ui_auto">GeStorage项目</a></li>
<li><a href="ui_auto">Training项目</a></li>
<li><a href="ui_auto">RobotFrame知识库</a></li>
<li><a href="ui_auto">UI自动化脚本设计</a></li>
</ul>
</li>
<li class="dropdown">
<a class="dropdown-toggle" data-toggle="dropdown" href="#">接口自动化测试<b class="caret"></b></a>
<ul class="dropdown-menu">
<li><a href="service_auto">Clinical项目</a></li>
<li><a href="service_auto">GeStorage项目</a></li>
<li><a href="service_auto">Training项目</a></li>
<li><a href="#">接口测试知识库</a></li>
</ul>
</li>
<li class="dropdown">
<a class="dropdown-toggle" data-toggle="dropdown" href="#">兼容性自动化测试<b class="caret"></b></a>
<ul class="dropdown-menu">
<li><a href="#">Clinical项目</a></li>
<li><a href="#">GeStorage项目</a></li>
<li><a href="#">Training项目</a></li>
<li><a href="#">兼容性实施原理</a></li>
</ul>
</li>
<li class="dropdown">
<a class="dropdown-toggle" data-toggle="dropdown" href="#">性能测试<b class="caret"></b></a>
<ul class="dropdown-menu">
<li><a href="#">Clinical项目</a></li>
<li><a href="#">GeStorage项目</a></li>
<li><a href="#">Training项目</a></li>
<li><a href="#">性能测试实施细则</a></li>
</ul>
</li>
<li><a href="per_auto">全局变量配置</a></li>
</ul>
</nav>
<div id="content" class="container">
{% block content %}
{% endblock %}
</div>
</body>
</html>
我这边index.html内容如下:
{% extends "base.html" %} //继承基类模板
{% block title %}平台主页{% endblock %}
{% block content %}
<div class="row">
<label>项目版本</label>
<select id="project_version" onchange="optionChange()">
{% for option in project_version.Clinical %}
<option value={{option}}>{{option}}</option>
{% endfor %}
</select>
<button id="redo" onClick="ajaxReRun()">重新执行</button>
</div>
<br>
<table id="pages_summary" class="table table-hover">
<th>页面名称</th>
<th>页面URL</th>
<th>DNS耗时</th>
<th>Request耗时</th>
<th>解析DOM耗时</th>
<th>DomReady耗时</th>
<th>LoadEvent耗时</th>
<th>白屏时长</th>
<th>整个页面加载耗时</th>
{% for page in page_load_result %} //Jinja中的循环体
<tr class="active hover">
<td class="active"><a href="javascript:{{page.page_name}}()">{{page.page_name}}</a></td>
<script type="text/javascript">
function {{page.page_name}}()
{
var href = $('#project_version').get(0).selectedOptions[0].text + "_" +"{{page.page_name}}";
location.href = href
}
</script>
<td class="active">{{page.url}}</td>
<td class="active">{{page.dns}}</td>
<td class="active">{{page.request}}</td>
<td class="active">{{page.dom_parser}}</td>
<td class="active">{{page.dom_ready}}</td>
<td class="active">{{page.load_event}}</td>
<td class="active">{{page.whitewait}}</td>
<td class="active">{{page.loadall}}</td>
</tr>
{% endfor %} //Jinja中循环体的结束标识
</table>
<br><br>
<h4>所有页面加载数据统计堆叠图</h4>
<div id="page_summary_chart">
<table id="data_chart" width="100%">
<tr>
<td align="center">
<div id="chart" class="text-align:center" style="width: 1250px;height:650px;">
<script type="text/javascript">
// 基于准备好的dom,初始化echarts实例
var myChart = echarts.init(document.getElementById('chart'));
// 指定图表的配置项和数据
option = {
tooltip : {
trigger: 'axis',
axisPointer : {type : 'shadow'}
},
legend: {
data:['dns(ms)', 'request(ms)', 'dom_parser(ms)', 'dom_ready(ms)', 'load_event(ms)', 'whitewait(ms)', 'loadall(ms)'],
},
toolbox: {
show : true,
feature : {
mark : {show: true},
dataView : {show: true, readOnly: false},
magicType : {show: true, type: ['line', 'bar', 'stack', 'tiled']},
restore : {show: true},
saveAsImage : {show: true}
}
},
calculable : true,
xAxis : [
{
type : 'value'
}
],
yAxis : [
{
type : 'category',
data : {{page_chart_data.page_name|safe}}
}
],
series : [
{
name:'dns(ms)',
type:'bar',
stack: 'Time',
itemStyle : { normal: {label : {show: true, position: 'insideRight'}}},
data: {{page_chart_data.dns|safe}}
},
{
name:'request(ms)',
type:'bar',
stack: 'Time',
itemStyle : { normal: {label : {show: true, position: 'insideRight'}}},
data: {{page_chart_data.request|safe}}
},
{
name:'dom_parser(ms)',
type:'bar',
stack: 'Time',
itemStyle : { normal: {label : {show: true, position: 'insideRight'}}},
data: {{page_chart_data.dom_parser|safe}}
},
{
name:'dom_ready(ms)',
type:'bar',
stack: 'Time',
itemStyle : { normal: {label : {show: true, position: 'insideRight'}}},
data: {{page_chart_data.dom_ready|safe}}
},
{
name:'request(ms)',
type:'bar',
stack: 'Time',
itemStyle : { normal: {label : {show: true, position: 'insideRight'}}},
data: {{page_chart_data.request|safe}}
},
{
name:'load_event(ms)',
type:'bar',
stack: 'Time',
itemStyle : { normal: {label : {show: true, position: 'insideRight'}}},
data: {{page_chart_data.load_event|safe}}
},
{
name:'whitewait(ms)',
type:'bar',
stack: 'Time',
itemStyle : { normal: {label : {show: true, position: 'insideRight'}}},
data: {{page_chart_data.whitewait|safe}}
},
{
name:'loadall(ms)',
type:'bar',
stack: 'Time',
itemStyle : { normal: {label : {show: true, position: 'insideRight'}}},
data: {{page_chart_data.loadall|safe}}
}
]
};
// 使用刚指定的配置项和数据显示图表。
myChart.setOption(option);
</script>
</div>
</td>
</tr>
</table>
</div>
{% endblock %}
(PS:在一个表格中循环查询到的结果并输出到页面)
下面贴出来我这边views.py文件里的全部内容供同学们参考:
# -*- coding:utf-8 -*-
import sqlite3
import sys
import time
import requests
from flask import render_template, g, redirect, url_for, request, send_file
from selenium import webdriver
import config
from Web import web_app
reload(sys)
sys.setdefaultencoding("utf-8")
# 定义根路由
@web_app.route('/')
def root():
return render_template('login.html')
@web_app.route('/login_success')
def login_success():
return render_template('login_success.html')
# 定义路由index
@web_app.route('/index', methods=['GET', 'POST'])
def index():
if request.method == 'POST':
version = request.form.get("selected_version", "null")
else:
version = 'syf1.1.1'
g.db = sqlite3.connect(config.db_dir)
summary_data_sql = 'select page_name, url, dns, request, dom_parser, dom_ready, load_event, whitewait, loadall '\
+ " from WebLoad where version='" + str(version) + "'" + ' order by page_name'
page_load_result = query_db(summary_data_sql)
g.db.close()
project_version = config.project_version_info
# 将页面信息转换为Echarts使用的数据
page_chart_data = {}
page_name_list = []
page_dns_list = []
page_request_list = []
page_dom_parser_list = []
page_dom_ready_list = []
page_load_event_list = []
page_whitewait_list = []
page_loadall_list = []
for result in page_load_result:
page_name_list.append(str(result['page_name']).strip("u'"))
page_dns_list.append(result['dns'])
page_request_list.append(result['request'])
page_dom_parser_list.append(result['dom_parser'])
page_dom_ready_list.append(result['dom_ready'])
page_load_event_list.append(result['load_event'])
page_whitewait_list.append(result['whitewait'])
page_loadall_list.append(result['loadall'])
page_chart_data['page_name'] = page_name_list
page_chart_data['dns'] = page_dns_list
page_chart_data['request'] = page_request_list
page_chart_data['dom_parser'] = page_dom_parser_list
page_chart_data['dom_ready'] = page_dom_ready_list
page_chart_data['load_event'] = page_load_event_list
page_chart_data['whitewait'] = page_whitewait_list
page_chart_data['loadall'] = page_loadall_list
return render_template('index.html', project_version=project_version, page_load_result=page_load_result, page_chart_data=page_chart_data)
# 定义重新执行脚本的路由
@web_app.route('/redo', methods=['GET', 'POST'])
def redo():
if request.method == 'GET':
return 'route does not support get'
else:
# 向测试环境发生get请求来验证测试环境是否存在
env_for_test = request.form.get("selected_version", "null")
current_env = config.hosts['164'] + env_for_test
env_test_code = requests.get(current_env).status_code
if env_test_code == 200:
# 定义页面名称列表
all_page_data = {}
page_urls = config.page_urls
timing_js = config.timing_js
# 统计页面加载时间的6个维度 登录页专用
index_data = {'dns': 0, 'request': 0, 'dom_parser': 0, 'dom_ready': 0, 'load_event': 0, 'whitewait': 0, 'loadall': 0}
# index页面资源的信息 登录页专用
page_index_info = []
# 将统计数据保存到sqlite
connection = sqlite3.connect(config.db_dir)
cursor = connection.cursor()
# 先删除页面资源细分表webDetailLoad所有数据
clear_resource_sql = "delete from PageDetail where project='Clinical' and version='" + env_for_test + "'"
cursor.execute(clear_resource_sql)
# 先清理上次的数据
clear_webload_sql = "delete from WebLoad where project='Clinical' and version='" + env_for_test + "'"
cursor.execute(clear_webload_sql)
connection.commit()
# get到clinical登录页(登录页需要单独收集其页面加载性能指标)
driver = webdriver.Chrome()
login_url = current_env + '/login/index'
driver.get(login_url)
driver.maximize_window()
time.sleep(5)
time_line = driver.execute_script(config.timing_js)
# 将页面加载时间存入all_page_data
for key in index_data.keys():
index_data[key] = time_line[key]
# 将index的数据存入all_page_data
all_page_data['Index'] = index_data
# 获得index页面所有的资源实体
entries = driver.execute_script(config.get_entries_js)
# 遍历登录页面所有资源
for items in entries:
# 以页面名:数组形式保存页面信息
page_index_resource = {}
# 资源名称
page_index_resource['name'] = items['name']
# 资源类型分类
resource_type = str(items['name']).split('.')[-1]
if resource_type in ['jpg', 'png', 'gif', 'jpeg']:
page_index_resource['resourceType'] = 'img'
elif resource_type == 'js':
page_index_resource['resourceType'] = 'js'
elif resource_type == 'css':
page_index_resource['resourceType'] = 'css'
else:
if items['initiatorType'] == 'xmlhttprequest':
page_index_resource['resourceType'] = 'xmlhttprequest'
elif items['initiatorType'] == 'script':
page_index_resource['resourceType'] = 'js'
else:
page_index_resource['resourceType'] = 'other'
# 资源大小
page_index_resource['transferSize'] = items['transferSize']
# 资源耗时
page_index_resource['duration'] = items['duration']
# 将登录页资源信息存入列表
page_index_info.append(page_index_resource)
# 将登录页资源列表信息存入字典对象 对应key为Index
save_resource_sql = 'insert into PageDetail ' + "values('Clinical','" + str(env_for_test) + "'," + "'" + str(
'Index') + "'," + "'" + str(page_index_resource['name']) + "'," + "'" + str(
page_index_resource['resourceType']) + "'," + "'" \
+ str(page_index_resource['transferSize']) + "'," + "'" + str(
page_index_resource['duration']) + "'," + 'CURRENT_TIMESTAMP)'
cursor.execute(save_resource_sql)
connection.commit()
# 登录系统后 对其他所有页面进行遍历以收集其页面加载性能数据
driver.find_element_by_id('txtUserName').send_keys('30048')
driver.find_element_by_id('txtPassword').send_keys('5436')
driver.find_element_by_id('btnConfirm').click()
time.sleep(3)
# 循环登录后的所有页面
for page in page_urls.keys():
# 将页面加载时间统计出来存入all_page_data
current_page_timing = {"dns": 0, "request": 0, "dom_parser": 0, "dom_ready": 0, "load_event": 0, "whitewait": 0, "loadall": 0}
current_page_url = current_env + (str(page_urls[page]))
driver.get(current_page_url)
time.sleep(10)
durations = driver.execute_script(timing_js)
for item in current_page_timing.keys():
current_page_timing[item] = durations[item]
# 将当前页面的name和统计到数据存入到all_page_data
all_page_data[page] = current_page_timing
entries = driver.execute_script("return window.performance.getEntries()")
# 遍历当前页面所有资源
for items in entries:
current_resources = {}
current_resources['name'] = items['name']
# 根据资源类型进行统计
resource_type = str(items['name']).split('.')[-1]
if resource_type in ['jpg', 'png', 'gif', 'jpeg']:
current_resources['resourceType'] = 'img'
elif resource_type == 'js':
current_resources['resourceType'] = 'js'
elif resource_type == 'css':
current_resources['resourceType'] = 'css'
else:
if items['initiatorType'] == 'xmlhttprequest':
current_resources['resourceType'] = 'xmlhttprequest'
elif items['initiatorType'] == 'script':
current_resources['resourceType'] = 'js'
else:
current_resources['resourceType'] = 'other'
# 资源大小
current_resources['transferSize'] = items['transferSize']
# 资源耗时
current_resources['duration'] = items['duration']
# 保存资源
save_resource_sql = 'insert into PageDetail ' + "values('Clinical','" + str(
env_for_test) + "'," + "'" + str(
page) + "'," + "'" + str(
current_resources['name']) + "'," \
+ "'" + str(current_resources['resourceType']) + "'," + "'" + str(
current_resources['transferSize']) + "'," + "'" + str(
current_resources['duration']) + "'," + 'CURRENT_TIMESTAMP)'
cursor.execute(save_resource_sql)
connection.commit()
# 退出chrome driver进程
driver.quit()
# 更新WebLoad表中的数据
for name in all_page_data.keys():
page_time = all_page_data[name]
if name == 'Index':
insert_sql = 'insert into WebLoad ' + "values('Clinical','" + str(env_for_test) + "'," + "'"\
+ str(name) + "'," + "'" + str(current_env) + "/login/index'," + str(page_time['dns']) \
+ ',' + str(page_time['request']) + ',' + str(page_time['dom_parser']) + ',' + str(page_time['dom_ready']) \
+ ',' + str(page_time['load_event']) + ',' + str(page_time['whitewait']) + ',' + str(page_time['loadall']) + ',' + 'CURRENT_TIMESTAMP)'
else:
insert_sql = 'insert into WebLoad ' + "values('Clinical','" + str(env_for_test) + "'," + "'" \
+ str(name) + "'," + "'" + str(current_env) + str(page_urls[name]) + "'," + str(page_time['dns']) \
+ ',' + str(page_time['request']) + ',' + str(page_time['dom_parser']) + ',' + str(page_time['dom_ready']) \
+ ',' + str(page_time['load_event']) + ',' + str(page_time['whitewait']) + ',' + str(page_time['loadall']) + ',' + 'CURRENT_TIMESTAMP)'
cursor.execute(insert_sql)
connection.commit()
connection.close()
return 'success'
else:
return 'environment is not exist'
@web_app.route('/<project_version>_<page_name>')
def page_detail(project_version, page_name):
g.db = sqlite3.connect(config.db_dir)
# 页面资源信息
resource_sql = "select resource_name, resource_type, resource_size, resource_duration from PageDetail where version='" + str(project_version) + "' and page_name='" + str(page_name) + "' order by resource_duration desc"
history_sql = "select version, dns, request, dom_parser, dom_ready, load_event, whitewait, loadall from WebLoad where page_name='" + str(page_name) + "' group by version"
composition_sql = "select resource_type, count(resource_type) as counts, sum(resource_size) as size_count, sum(resource_duration) as duration_count from PageDetail where version='" + str(project_version) + "' and page_name='" + str(page_name) + "' group by resource_type"
top_sql = "select resource_name, resource_duration, resource_size from PageDetail where version='" + str(project_version) + "' and page_name='" + str(page_name) + "' order by resource_duration desc limit 3"
resource_composition = query_db(composition_sql)
resource_summary = query_db(resource_sql)
top_resources = query_db(top_sql)
# 对历史数据比对来说 若只有一个版本需要添加默认的比对数据
history_info = query_db(history_sql)
g.db.close()
if len(history_info) > 1:
pass
else:
default_data = {'version': 'syf-next-version', 'dns': 0, 'request': 0, 'dom_parser': 0, 'dom_ready': 0, 'load_event': 0, 'whitewait': 0, 'loadall': 0}
history_info.append(default_data)
version_list = []
for data in history_info:
version_list.append(str(data['version']).strip("u"))
resource_type_list = []
for item in resource_composition:
resource_type_list.append(item['resource_type'])
return render_template('page_detail.html', resource_summary=resource_summary, history_info=history_info, page_name=page_name,
version_list=version_list, resource_composition=resource_composition, resource_type_list=resource_type_list ,
top_resources=top_resources)
# 全局环境变量配置
@web_app.route('/variable_config', methods=['GET', 'POST'])
def variable_config():
if request.method == 'GET':
return render_template('variable_config.html')
else:
return render_template('variable_config.html')
# 以字典形式返回查询结果
def query_db(query, args=(), one=False):
cur=g.db.execute(query,args)
rv=[dict((str(cur.description[idx][0]), str(value)) for idx, value in enumerate(row)) for row in cur.fetchall()]
return (str(rv[0]) if rv else None) if one else rv
@web_app.route('/ui_auto')
def ui_auto():
return render_template('ui_auto.html')
@web_app.route('/service_auto')
def service_auto():
return render_template('service_auto.html')
@web_app.route('/case_detail')
def case_detail():
return render_template('case_detail.html')
@web_app.route('/cap_auto')
def cap_auto():
return render_template('cap_auto.html')
@web_app.route('/per_auto')
def per_auto():
return render_template('per_auto.html')
这块儿的是很简单的,jquery发送ajax有固定格式滴~
function test(){
$.ajax({
//提交数据的类型 POST GET
type:"POST",
//提交的网址
url:"接口地址(对应flask中的路由url) ",
//提交的数据
data:{Name:"sanmao",Password:"sanmaoword"},
//返回数据的格式
datatype: "html",//"xml", "html", "script", "json", "jsonp", "text".
//在请求之前调用的函数
beforeSend:function(){$("#msg").html("logining");},
//成功返回之后调用的函数
success:function(data){
$("#msg").html(decodeURI(data));
} ,
//调用执行后调用的函数
complete: function(XMLHttpRequest, textStatus){
alert(XMLHttpRequest.responseText);
alert(textStatus);
//HideLoading();
},
//调用出错执行的函数
error: function(){
//请求出错处理
}
});
}
我们这里用到两个ajax请求,1:重新执行脚本 2:下拉框选值版本
重新执行脚本时,我们发送ajax的源码是这样的:
// 重新执行测试脚本
function ajaxReRun(){
var select = document.getElementById("project_name");
var options = select.options;
var index = select.selectedIndex;
var version_text = options[index].text;
$.ajax({
//提交数据的类型 POST GET
type:"POST",
//提交的网址
url:"/redo",
//提交的数据
data:({"selected_version": version_text}),
//返回数据的格式 "xml", "html", "script", "json", "jsonp", "text"
datatype: "text",
//ajax请求成功后的事件
success:function(data){
if (data=='Env Is Not Exist'){
alert("亲~ 这个版本的测试环境去哪儿了?");
}
else{
window.location.href = window.location.href;
alert("所选版本的前端性能测试执行完毕");
}
},
//调用出错执行的函数
error: function(XMLResponse){
//请求出错处理
alert(XMLResponse.responseText)
}
});
}
我们从下拉框中选择版本信息时ajax源码如下:
// 刷新数据
function optionChange(){
var select = document.getElementById("project_name");
var options = select.options;
var index = select.selectedIndex;
var version_text = options[index].text;
$.ajax({
//提交数据的类型 POST GET
type:"POST",
//提交的网址
url:"/index",
//提交的数据
data:({"selected_version": version_text}),
//返回数据的格式 "xml", "html", "script", "json", "jsonp", "text"
datatype: "json",
//ajax请求成功后的事件
success:function(data){
$('#weirtable').html($(data).find('#weirtable').html());
$('#data_container').html($(data).find('#data_container').html());
},
//调用出错执行的函数
error: function(XMLResponse){
//请求出错处理
alert(XMLResponse.responseText)
}
});
}
我们可以把上面这两个方法写到Index.html里也可以单独写到一个js文件中然后在Index.html文件中导入js文件即可。
关于Jquery其他的东西 我暂时没用到,同学可以自己扩展这块儿的知识以完善体系。
官网上有教程,自己去搜!
想在windows上发布flask 我去~ 如果你不怕坑太多 那就去鼓捣吧。
我是在CentOS上部署的,下面是部署过程的详细步骤,仅作参考哦~
1. pip install virtualenv
virtualenv venv # 产生虚拟路径 venv
source venv/bin/activate # 启动虚拟环境(deactivate 可退出虚拟环境)
pip install -r requirements.txt # 安装依赖包(将依赖到的包全部写到requirements文件中方便管理)
2. 虚拟路径下安装uwsgi:
(1) 编辑uwsgi配置文件:[uwsgi]
#########config.init start##########
[uwsgi]
# uwsgi 启动时所使用的地址与端口
socket = 127.0.0.1:6000
# 指向网站目录
chdir = /opt/usr/AutoMan
# python 启动程序文件
wsgi-file = run.py
# python 程序内用以启动的 application 变量名
callable = surveillance
# 处理器数
processes = 2
# 线程数
threads = 2
# 记录日志(自己添加)
daemonize = /var/log/auto_man.log
# clear environment on exit
vacuum = true
#状态检测地址
stats = 127.0.0.1:9191
#########config.init end##########
(2) 安装运行uwsgi
(venv) #: pip install uwsgi
(venv) #: uwsgi config.ini
安装 nginx:
# yum install nginx
只需在/etc/nginx/conf.d创建一个AutoMan.conf配置即可:
server {
listen 7000 default_server; # 默认的web访问端口
server_name 192.168.31.61; #公网ip
#charset koi8-r;
include /etc/nginx/default.d/*.conf;
access_log /var/log/nginx/access_airflask.log;
error_log /var/log/nginx/access_airerror.log; #错误日志
location / {
include uwsgi_params; #导入的uwsgi配置
uwsgi_pass 127.0.0.1:6000; #需和uwsgi的配置文件里socket项的地址
uwsgi_param UWSGI_PYHOME /opt/usr/AutoMan /venv; #(虚拟环境下)
相信各位同学对flask有了初步的认识,我个人对这个平台的后续扩展构想如下:
1:添加接口的自动化测试模块,可以在web页面上设计接口用例 所见即所得。
需要用到的技术:requests库,flask-celery-基于后台的作业执行。
2:UI自动化测试模块,目前设想UI这块儿不作为重点,使用RF的ride来设计编写用例,然后将test-suite文件上传到该平台,继而在该平台上选择suites并在后台执行pabot即可。
需要用到的技术:flask中实现多文件上传。
3:安全测试模块,安全测试的水 不是一般的深,敬请期待个人关于安全测试学习的文章。
4:性能测试过程中的监控,这块儿的东西实现起来也是蛮方便的,目前只想到tomcat, os, db的监控。
5:使用python的多线程技术做接口的性能测试。
期待高手和前辈们的指正和沟通~~~~~
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。