httprunner+extend测试报告模板:定制化机器人发送企业微信

项目:接口自动化测试

使用技术:httprunner+python

测试报告效果如下:

企业微信机器人推送效果如下:

 

1.测试报告

报告如下:

新建一个extent-theme-template.html文件,把下面.html代码复制到文件里,

如上图在自动化测试项目里新建templates文件,extent-theme-template.html放在templates/下

下面测试报告可以根据自己需要定制化编写

<!DOCTYPE html>
<html><head><meta charset='utf-8'/><meta name='description' content=''/><meta name='robots' content='noodp, noydir'/><meta name='viewport' content='width=device-width, initial-scale=1'/><meta id="timeStampFormat" name="timeStampFormat" content='MMM d, yyyy hh:mm:ss a'/><link href='https://fonts.googleapis.com/css?family=Source+Sans+Pro:400,600' rel='stylesheet' type='text/css'><link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet"><link href='http://extentreports.com/resx/dist/css/extent.css' type='text/css' rel='stylesheet'/><title>{{ html_report_name }} - 测试报告</title><style type='text/css'>.node.level-1 ul {display: none;}.node.level-1.active ul {display: block;}.card-panel.environment th:first-child {width: 30%;}</style>
</head><body class='extent standard default hide-overflow dark'>
<div id='theme-selector' alt='切换主题,默认黑色' title='切换主题'><span><i class='material-icons'>desktop_windows</i></span>
</div><nav><div class="nav-wrapper"><a href="#!" class="brand-logo blue darken-3">Report</a><!-- slideout menu --><ul id='slide-out' class='side-nav fixed hide-on-med-and-down'><li class='waves-effect active'><a href='#!' view='test-view'onclick="configureView(0);chartsView('test');"><i class='material-icons'>dashboard</i></a></li><li class='waves-effect'><a href='#!' view='category-view' onclick="configureView(1)"><iclass='material-icons'>label_outline</i></a></li><li class='waves-effect'><a href='#!' onclick="configureView(-1);chartsView('dashboard');"view='dashboard-view'><i class='material-icons'>track_changes</i></a></li></ul><!-- report name --><span class='report-name'>测试报告: {{ html_report_name }}</span><!-- report headline --><span class='report-headline'></span><!-- nav-right --><ul id='nav-mobile' class='right hide-on-med-and-down nav-right'><li><a href='#!'><span class='label suite-start-time blue darken-3'>{{ time.start_datetime }}</span></a></li><li><a href='#!'><span class='label blue darken-3'>HttpRunner {{ platform.httprunner_version }} </span></a></li><li><a href='#!'><span class='label blue darken-3'>{{ platform.python_version }} </span></a></li><li><a href='#!'><span class='label blue darken-3'>{{ platform.platform }}</span></a></li></ul></div>
</nav><!-- container -->
<div class='container'><div id='test-view' class='view'><section id='controls'><div class='controls grey lighten-4'><!-- test toggle --><div class='chip transparent'><a class='dropdown-button tests-toggle' data-activates='tests-toggle' data-constrainwidth='true'data-beloworigin='true' data-hover='true' href='#'><i class='material-icons'>warning</i> 状态</a><ul id='tests-toggle' class='dropdown-content'><li status='pass'><a href='#!'>Pass <i class='material-icons green-text'>check_circle</i></a></li><li status='fail'><a href='#!'>Fail <i class='material-icons red-text'>cancel</i></a></li><li status="skip"><a href="#!">Skip <i class="material-icons cyan-text">redo</i></a></li><li class='divider'></li><li status='clear' clear='true'><a href='#!'>Clear Filters <iclass='material-icons'>clear</i></a></li></ul></div><!-- test toggle --><!-- category toggle --><div class='chip transparent'><a class='dropdown-button category-toggle' data-activates='category-toggle'data-constrainwidth='false' data-beloworigin='true' data-hover='true' href='#'><i class='material-icons'>local_offer</i> 类别</a><ul id='category-toggle' class='dropdown-content'>{% for test_suite_summary in details %}<li><a href='#'>{{ test_suite_summary.name }}</a></li>{% endfor %}<li class='divider'></li><li class='clear'><a href='#!' clear='true'>Clear Filters</a></li></ul></div><!-- category toggle --><!-- clear filters --><div class='chip transparent hide'><a class='' id='clear-filters' alt='Clear Filters' title='Clear Filters'><i class='material-icons'>close</i> Clear</a></div><!-- clear filters --><!-- enable dashboard --><div id='toggle-test-view-charts' class='chip transparent'><a class='pink-text' id='enable-dashboard' alt='Enable Dashboard' title='Enable Dashboard'><i class='material-icons'>track_changes</i> 仪表板</a></div><!-- enable dashboard --><!-- search --><div class='chip transparent' alt='Search Tests' title='Search Tests'><a href="#" class='search-div'><i class='material-icons'>search</i> 搜索</a><div class='input-field left hide'><input style="color: red" id='search-tests' type='text' class='validate browser-default'placeholder='Search Tests...'></div></div><!-- search --></div></section><div id='test-view-charts' class='subview-full'><div id='test-view-charts' class='subview-full'><div id='charts-row' class='row nm-v nm-h'><div class='col s12 m6 l6 np-h'><div class='card-panel nm-v'><div class='left panel-name'>用例</div><div class='chart-box'><canvas id='parent-analysis' width='100' height='80'></canvas></div><div class='block text-small'><span class='tooltipped' data-position='top'><spanclass='strong'>{{ stat.teststeps.successes }}</span>  通过</span><span class='tooltipped' data-position='top'><spanclass='strong'>{{ stat.teststeps.failures }}</span>  失败</span></div><div class='block text-small'><span class='strong tooltipped' data-position='top'>{{ stat.teststeps.errors }}</span>错误<span class='strong tooltipped' data-position='top'>{{ stat.teststeps.skipped }}</span>跳过</div></div></div><div class='col s12 m6 l6 np-h'><div class='card-panel nm-v'><div class='left panel-name'>用例组</div><div class='chart-box'><canvas id='child-analysis' width='100' height='80'></canvas></div><div class='block text-small'><span id="pass_suites" class='tooltipped' data-position='top'></span></div><div class='block text-small'><span id="fail_suites" class='strong tooltipped' data-position='top'></span> 失败</div></div></div></div></div></div><div class='subview-left left'><div class='view-summary'><h5>用例组</h5><ul id='test-collection' class='test-collection'>{% for test_suite_summary in details %}{% if test_suite_summary.success == True %}<li class='test displayed active has-leaf pass' status='pass' bdd='true'test-id='{{ test_suite_summary.name }}_{{ loop.index }}'>{% else %}<li class='test displayed active has-leaf fail' status='fail' bdd='false'test-id='{{ test_suite_summary.name }}_{{ loop.index }}'>{% endif %}<div class='test-heading'><span class='test-name'>{{ test_suite_summary.name }}</span><span class='test-time'>base_url: {{ test_suite_summary.base_url }}</span>{% if test_suite_summary.success == True %}<span class='test-status right pass'>pass</span>{% else %}<span class='test-status right fail'>fail</span>{% endif %}</div><div class='test-content hide'><div class='test-time-info'><!--<span class='label start-time'>{{ test_suite_summary.time.start_at }}</span>--><span class='label start-time'>{{ '%0.3f'| format(test_suite_summary.time.duration|float) }} s</span></div><div class='test-desc'>通过: {{ test_suite_summary.stat.successes }} ;失败: {{ test_suite_summary.stat.failures }} ;错误: {{ test_suite_summary.stat.errors }}跳过: {{ test_suite_summary.stat.skipped }} ;</div><div class='test-attributes'><div class='category-list'><span class='category label white-text'>{{ test_suite_summary.name }}</span><span class='category label white-text'>base_url: {{ test_suite_summary.base_url }}</span></div></div><ul class='collapsible node-list' data-collapsible='accordion'>{% for record in test_suite_summary.records %}{% if  record.status == 'success' %}<li class='node level-1 leaf pass' status='pass'test-id='{{ test_suite_summary.name }}_{{ record.name }}_{{ loop.index }}'>{% elif record.status == 'failure' %}<li class='node level-1 leaf fail' status='fail'test-id='{{ test_suite_summary.name }}_{{ record.name }}_{{ loop.index }}'><!--失败api-->{% elif record.status == 'error' %}<li class='node level-1 leaf error' status='error'test-id='{{ test_suite_summary.name }}_{{ record.name }}_{{ loop.index }}'>{% elif record.status == 'skipped' %}<li class='node level-1 leaf skip' status='skip'test-id='{{ test_suite_summary.name }}_{{ record.name }}_{{ loop.index }}'>{% endif %}<div class='collapsible-header'><div class='node-name'>{{ record.name }}</div><!--errorapinameend--><span class='node-time'>{{ record.start_timestamp }}</span><span class='node-duration'>耗时: {{ record.response_time }} ms</span>{% if  record.status == 'success' %}<span class='test-status right pass'>pass</span>{% elif record.status == 'failure' %}<span class='test-status right fail'>fail</span>{% elif record.status == 'error' %}<span class='test-status right error'>error</span>{% elif record.status == 'skipped' %}<span class='test-status right skip'>skip</span>{% endif %}</div><div class='collapsible-body'>{% for medata in record.meta_datas_expanded %}<div class='category-list right'style="margin-top:20px"><span class='category label white-text'>{{ test_suite_summary.name }}</span><span class='category label white-text'>{{ medata.name }}</span></div><div class='node-steps'><table class='bordered table-results'><thead><tr><th>状态</th><th>参数</th><th>详细信息</th></tr></thead><tbody><tr class='info' status='info'><td class='status info' title='info' alt='info'><iclass='material-icons'>low_priority</i></td><td class='timestamp'>url</td>{% for medatad in medata.data %}<td class='step-details'>{{ medatad.request.url }}</td>{% endfor %}</tr><tr class='info' status='info'><td class='status info' title='info' alt='info'><iclass='material-icons'>low_priority</i></td><td class='timestamp'>method</td>{% for medatad in medata.data %}<td class='step-details'>{{ medatad.request.method }}</td>{% endfor %}</tr><tr class='info' status='info'><td class='status info' title='info' alt='info'><iclass='material-icons'>low_priority</i></td><td class='timestamp'>status_code</td>{% for medatad in medata.data %}<td class='step-details'>{{ medatad.response.status_code }}</td>{% endfor %}</tr>{% for medatad in medata.data %}{% for key, value in medatad.request.items() %}{% if key != 'url' and key != 'method' and key != 'start_timestamp' %}<tr class='log' status='debug'><td class='status debug' title='debug' alt='debug'><iclass='material-icons'>low_priority</i></td><td class='timestamp'>{{ key }}</td><td class='step-details'>{{ value }}</td></tr>{% endif %}{% endfor %}{% for key, value in medatad.response.items() %}{% if key != "json" and key != "elapsed_ms" and key != "response_time_ms" and key != "content_size" and key != "content_type" and key != "status_code" and key != "reason" and key != "ok" and key != "encoding" and key != "url" %}<tr class='log' status='debug'><td class='status debug' title='debug' alt='debug'><iclass='material-icons'>low_priority</i></td><td class='timestamp'>{{ key }}</td><td class='step-details'>{% if key == "content" %}{% if "image" in medata.response.content_type %}<img src="{{ medata.response.content }}"/>{% else %}<pre>{{ medatad.response.text | e }}</pre>{% endif %}{% else %}{{ value }}{% endif %}</td></tr>{% endif %}{% endfor %}{% endfor %}<tr class='log' status='pass'><td class='status pass' title='pass' alt='pass'><iclass='material-icons'>low_priority</i></td><td class='timestamp'>Validators</td><td class='step-details'>{% for validator in medata.validators.validate_extractor %}<div><font color="#00BFFF">{{ validator.check }}&nbsp;&nbsp;&nbsp;</font><strong>{{ validator.comparator }}:</strong>[ {{ validator.expect }},&nbsp;&nbsp;{{ validator.check_value }} ]</div>{% endfor %}</td></tr><tr class='info' status='info'><td class='status info' title='info' alt='info'><iclass='material-icons'>low_priority</i></td><td class='timestamp'>Statistics</td><td class='step-details'><div>content_size(bytes): {{ medata.stat.content_size }}</div><div>response_time(ms): {{ medata.stat.response_time_ms }}</div><div>elapsed(ms): {{ medata.stat.elapsed_ms }}</div></td></tr>{% if record.attachment %}<tr class='log' status='fail'><td class='status fail' title='fail' alt='fail'><iclass='material-icons'>cancel</i></td><td class='timestamp'>exception:</td><td class='step-details'><pre>{{ record.attachment }}</pre></td><td class='timestamp'>url</td>{% for medatad in medata.data %}<!-- failend--><td class='step-details'>{{ medatad.request.url }}</td><!--failend-->{% endfor %}</tr>{% endif %}</tbody></table></div>{% endfor %}</div></li>{% endfor %}</ul></div>{% endfor %}</ul></div></div><!-- subview left --><div class='subview-right left'><div class='view-summary'><h5 class='test-name'></h5><div id='step-filters' class="right"><span class="blue-text" status="info" alt="info" title="info"><iclass="material-icons">info_outline</i></span><span class="green-text" status="pass" alt="pass" title="pass"><i class="material-icons">check_circle</i></span><span class="red-text" status="fail" alt="fail" title="fail"><iclass="material-icons">cancel</i></span><span class="red-text text-darken-4" status="fatal" alt="fatal" title="fatal"><iclass="material-icons">cancel</i></span><span class="pink-text text-lighten-1" status="error" alt="error" title="error"><iclass="material-icons">error</i></span><span class="orange-text" alt="warning" status="warning" title="warning"><iclass="material-icons">warning</i></span><span class="teal-text" status="skip" alt="skip" title="skip"><iclass="material-icons">redo</i></span><span status="clear" alt="Clear filters" title="Clear filters"><iclass="material-icons">clear</i></span></div></div></div></div><!-- subview right --><!-- test view --><div id='category-view' class='view hide'><section id='controls'><div class='controls grey lighten-4'><!-- search --><div class='chip transparent' alt='Search Tests' title='Search Tests'><a href="#" class='search-div'><i class='material-icons'>search</i> 搜索</a><div class='input-field left hide'><input tyle="color: red;" id='search-tests' type='text'class='validate browser-default'placeholder='Search Tests...'></div></div><!-- search --></div></section><div class='subview-left left'><div class='view-summary'><h5>类别</h5><ul id='category-collection' class='category-collection'><li class='category displayed active'><div class='category-heading'><span class='category-name'>用例组</span><span class='category-status right'><span class='label pass'>{{ stat.successes }} </span>{% if stat.failures != 0 %}<span class='label fail'>{{ stat.failures }}</span>{% endif %}{% if stat.errors != 0 %}<span class='label blue lighten-1'>{{ stat.errors }}</span>{% endif %}{% if stat.skipped != 0 %}<span class='label yellow darken-2'>{{ stat.skipped }}</span>{% endif %}</span></div><div class='category-content hide'><div class='category-status-counts'><span class='label green accent-4 white-text'>Passed: {{ stat.successes }}</span><span class='label red lighten-1 white-text'>Failed: {{ stat.failures }}</span><span class='label blue lighten-1 white-text'>Errored: {{ stat.errors }}</span><span class="label yellow darken-2 white-text">Skipped: {{ stat.skipped }}</span></div><div class='category-tests'><table class='bordered table-results'><thead><tr><th>时间</th><th>用例名</th><th>状态</th></tr></thead><tbody>{% for test_suite_summary in details %}<tr style="border: 1px solid #49cc90; background-color: rgba(73, 204, 144, .1)"><td>{{ test_suite_summary.time.start_at }}</td><td class='linked'test-id='{{ test_suite_summary.name }}_{{ loop.index }}'>{{ test_suite_summary.name }}</td>{% if test_suite_summary.success == True %}<td><span class='test-status pass'>pass</span></td>{% else %}<td><span class='test-status fail'>fail</span></td>{% endif %}</tr>{% for record in test_suite_summary.records %}<tr><td>{{ test_suite_summary.time.start_at }}</td><td class='linked'test-id='{{ test_suite_summary.name }}_{{ record.name }}_{{ loop.index }}'>{{ record.name }}</td>{% if record.status == 'success' %}<td><span class='test-status pass'>pass</span></td>{% elif record.status == 'failure' %}<td><span class='test-status fail'>fail</span></td>{% elif record.status == 'error' %}<td><span class='test-status error'>error</span></td>{% elif record.status == 'skipped' %}<td><span class='test-status' style="color: #fbc02d">skip</span></td>{% endif %}</tr>{% endfor %}{% endfor %}</tbody></table></div></div></li>{% for test_suite_summary in details %}<li class='category displayed active'><div class='category-heading'><span class='category-name'>{{ test_suite_summary.name }}</span><span class='category-status right'><span class='label pass'>{{ test_suite_summary.stat.successes }} </span>{% if test_suite_summary.stat.failures != 0 %}<span class='label fail'>{{ test_suite_summary.stat.failures }}</span>{% endif %}{% if test_suite_summary.stat.errors != 0 %}<span class='label blue lighten-1'>{{ test_suite_summary.stat.errors }}</span>{% endif %}{% if test_suite_summary.stat.skipped != 0 %}<span class='label yellow darken-2'>{{ test_suite_summary.stat.skipped }}</span>{% endif %}</span></div><div class='category-content hide'><div class='category-status-counts'><span class='label green accent-4 white-text'>Passed: {{ test_suite_summary.stat.successes }}</span><span class='label red lighten-1 white-text'>Failed: {{ test_suite_summary.stat.failures }}</span><span class='label blue lighten-1 white-text'>Errored: {{ stat.errors }}</span><span class="label yellow darken-2 white-text">Skipped: {{ test_suite_summary.stat.skipped }}</span></div><div class='category-tests'><table class='bordered table-results'><thead><tr><th>Timestamp</th><th>TestName</th><th>Status</th></tr></thead><tbody>{% for record in test_suite_summary.records %}<tr><td>{{ test_suite_summary.time.start_at }}</td><td class='linked'test-id='{{ test_suite_summary.name }}_{{ record.name }}_{{ loop.index }}'>{{ record.name }}</td>{% if record.status == 'success' %}<td><span class='test-status pass'>pass</span></td>{% elif record.status == 'failure' %}<td><span class='test-status fail'>fail</span></td>{% elif record.status == 'error' %}<td><span class='test-status error'>error</span></td>{% elif record.status == 'skipped' %}<td><span class='test-status' style="color: #fbc02d">skip</span></td>{% endif %}</tr>{% endfor %}</tbody></table></div></div></li>{% endfor %}</ul></div></div><div class='subview-right left'><div class='view-summary'><h5 class='category-name'></h5></div></div></div><!-- category view --><div id='dashboard-view' class='view hide'><div class='card-panel transparent np-v'><h5>仪表板</h5><div class='row'><div class='col s2'><div class='card-panel r'>通过<div class='panel-lead'>{{ stat.teststeps.successes }}</div></div></div><div class='col s2'><div class='card-panel r'>失败<div class='panel-lead'>{{ stat.teststeps.failures }}</div></div></div><div class='col s2'><div class='card-panel r'>错误<div class='panel-lead'>{{ stat.teststeps.errors }}</div></div></div><div class='col s2'><div class='card-panel r'>跳过<div class='panel-lead'>{{ stat.teststeps.skipped }}</div></div></div><div class='col s2'><div class='card-panel r'>开始时间<div class='panel-lead'>{{ time.start_datetime }}</div></div></div><div class='col s2'><div class='card-panel r'>耗时<div class='panel-lead'>{{ '%0.3f'| format(time.duration|float) }} s</div></div></div><div class='col s4'><div class='card-panel'><span class='right label cyan white-text'>类别</span><p>&nbsp;</p><table><tr><th>名称</th><th>通过</th><th>失败</th><th>错误</th><th>跳过</th></tr><tr><td>用例组</td><td class="pass">{{ stat.successes }}</td><td class="fail">{{ stat.failures }}</td><td class="error">{{ stat.errors }}</td><td class="skip">{{ stat.skipped }}</td></tr>{% for test_suite_summary in details %}<tr><td>{{ test_suite_summary.name }}</td><td class="pass">{{ test_suite_summary.stat.successes }}</td><td class="fail">{{ test_suite_summary.stat.failures }}</td><td class="error">{{ test_suite_summary.stat.errors }}</td><td class="skip">{{ test_suite_summary.stat.skipped }}</td></tr>{% endfor %}</table></div></div></div></div></div><!-- dashboard view --><!-- testrunner-logs view --><!-- container -->
</div>
<script>var test_suite_success = 0;{%for test_suite_summary in details %}{% if test_suite_summary.success == True %}test_suite_success = test_suite_success + 1;{% endif %}{% endfor %}var statusGroup = {passParent: {{ stat.teststeps.successes }},failParent: {{ stat.teststeps.failures }},fatalParent: 0,errorParent: {{ stat.teststeps.errors }},warningParent: 0,skipParent: {{ stat.teststeps.skipped }},exceptionsParent: 0,passChild: test_suite_success,failChild: {{ details|length }} -test_suite_success,fatalChild: 0,errorChild: 0,warningChild: 0,skipChild: 0,infoChild: 0,exceptionsChild: 0,passGrandChild: 0,failGrandChild: 0,fatalGrandChild: 0,errorGrandChild: 0,warningGrandChild: 0,skipGrandChild: 0,infoGrandChild: 0,exceptionsGrandChild: 0,};document.getElementById('pass_suites').innerHTML = "<span class='strong'>" + test_suite_success + "</span>  通过";//找到id为'myId'的标签内插入html变量的值document.getElementById('fail_suites').innerText = {{ details|length }} -test_suite_success;//找到id为'myId'的标签替换它的内容为html的值</script><script src='http://extentreports.com/resx/dist/js/extent.js' type='text/javascript'></script><script type='text/javascript'>$(window).off("keydown");
</script>
</body></html>

2.httprunner版本

不要用太高的版本,2.5.7到3.0.0都可以支持上面1中的测试报告模板

3.机器人推送报告脚本.py

#生产key
wx_api_key = "xxxxx" #写机器人的webhook地址#获取报告路径
def getreportpath():filePath = 'reports/'for i, j, k in os.walk(filePath):kstrpath = "reports/" + k[0]print(strpath)return strpath#发送报告
def upload_file(file_path, wx_upload_url):file_name = file_path.split("/")[-1]with open(file_path, 'rb') as f:length = os.path.getsize(file_path)data = f.read()headers = {"Content-Type": "application/octet-stream"}params = {"filename": file_name,"filelength": length,}file_data = copy(params)file_data['file'] = (file_path.split('/')[-1:][0], data)encode_data = encode_multipart_formdata(file_data)file_data = encode_data[0]headers['Content-Type'] = encode_data[1]r = requests.post(wx_upload_url, data=file_data, headers=headers)print(r.text)media_id = r.json()['media_id']print(media_id)return media_iddef qi_ye_wei_xin_file(wx_url, media_id):headers = {"Content-Type": "text/plain"}data = {"msgtype": "file","file": {"media_id": media_id}}r = requests.post(url=wx_url,headers=headers, json=data)print(r.text)test_report = getreportpath()
wx_upload_url = "https://qyapi.weixin.qq.com/cgi-bin/webhook/upload_media?key={}&type=file".format(wx_api_key)
wx_url = 'https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key={}'.format(wx_api_key)
media_id = upload_file(test_report, wx_upload_url)
qi_ye_wei_xin_file(wx_url, media_id)

4.批量执行测试集.py

下图文件可配置下jekins上批量执行整个项目的测试集合

# coding:utf-8
import shutil#需要加个base.py,定义全局项目路径
import basefrom httprunner.api import HttpRunner
from httprunner.report import gen_html_report
if __name__=='__main__':runner = HttpRunner(failfast=False, log_level='INFO')shutil.rmtree('reports/')runner.run('testsuite/*.yml')gen_html_report(runner._summary, report_template="templates/extent-theme-template.html")

base.py如下:

4.jekins配置

加上最后一步,再配置下定时执行表达式。就搞定啦

本文链接:https://my.lmcjl.com/post/12838.html

展开阅读全文

4 评论

留下您的评论.