Jmeter 自动化二次构建:Jenkins+ant

介绍

性能测试利器——Jmeter,由于工作原因需要执行大量脚本并分析结果,为了直观将结果报告中的数据存储到数据库中并利用数据分析软件进行分析。

性能测试自动化需要的工具 Jmeter+ant+Jenkins,具体的安装教程以及使用教程省略(网上有很多)。

本文先简单讲述了 Jenkins+ant+jmeter 的配置,之后是本文主要的内容——对失败事务的二次构建,即对执行失败的事务针对性的再一次执行脚本。

1、安装工具

安装 Jmeter、ant、Jenkins,版本不限。本人使用的版本为:apache-ant-1.10.12、apache-jmeter-5.4.1。

2、配置文件

apache-jmeter/extras/build.xml 文件是 jmeter 脚本生成结果报告的配置文件,我们需要自己创建一个 build_ant.xml。

<?xml version="1.0" encoding="UTF-8"?>
<project name="ant-jmeter-test" default="run" basedir=".">
	<tstamp>
        <format property="time" pattern="yyyyMMddHHmm" />
    </tstamp>
	<!-- 性能自动化目录-->
	<property name="basedirectory" value="D:\Ginny\jmx_auto" />
    <!-- 需要改成自己本地的 Jmeter 目录-->  
    <property name="jmeter.home" value="D:\software\apache-jmeter-5.4.1" />
    <!-- jmeter生成jtl格式的结果报告的路径--> 
    <property name="jmeter.result.jtl.dir" value="${basedirectory}\jtl-results" />
    <!-- jmeter生成html格式的结果报告的路径-->
    <property name="jmeter.result.html.dir" value="${basedirectory}\html-results" />
	<!-- Name of test (without .jmx) -->
	<property name="before" value="Test"/>
    <!-- 生成的报告的前缀-->  
    <property name="ReportName" value="TestReport" />
    <property name="jmeter.result.jtlName" value="${jmeter.result.jtl.dir}/${test}${time}.jtl" />
    <property name="jmeter.result.htmlName" value="${jmeter.result.html.dir}/${test}${time}.html" />
    <path id="xslt.classpath">
        <fileset dir="${jmeter.home}/lib" includes="xalan*.jar"/>
        <fileset dir="${jmeter.home}/lib" includes="serializer*.jar"/>
    </path>
    <target name="run">
        <antcall target="test" />
        <antcall target="report" />
    </target>
  
    <target name="test">
        <taskdef name="jmeter" classname="org.programmerplanet.ant.taskdefs.jmeter.JMeterTask" />
    <jmeter jmeterHome="${jmeter.home}" resultLog="${jmeter.result.jtlName}">
             <!-- 声明要运行的脚本。"*.jmx"指包含此目录下的所有jmeter脚本-->
            <testplans dir="${basedirectory}\jmx" includes="*.jmx" />
        </jmeter>
    </target>
    <target name="report">
        <tstamp> <format property="report.datestamp" pattern="yyyy/MM/dd HH:mm" /></tstamp>
        <xslt classpathref="xslt.classpath"
              force="true"
              in="${jmeter.result.jtlName}"
              out="${jmeter.result.htmlName}"
              style="${jmeter.home}/extras/jmeter-results-detail-report_21.xsl">
              <param name="dateReport" expression="${report.datestamp}"/>
        </xslt>
        <copy todir="${jmeter.result.html.dir}">
            <fileset dir="${jmeter.home}/extras">
                <include name="collapse.png" />
                <include name="expand.png" />
            </fileset>
        </copy>
    </target>
</project>

搭配 ant 后我们在 windows 端的执行命令为:

D:\software\apache-ant-1.10.12\bin\ant -f D:\Ginny\jmx_auto\build_ant.xml

将上面的执行命令保存到 test.bat 文件中,在 Jenkins 中新建一个 New-Item --》 Freestyle project,只需在 Build 下建一个 Execute Windows batch command,框内命令为:

D:\Ginny\jmx_auto\test.bat

Build Now 即可执行 jmeter 脚本,这里忽略了将结果文件解析到数据库的操作,需要编写代码实现,可以从下面二次构建中的函数提取。

二次构建

上述只是简单说了一下性能自动化环境搭建,忽略了将数据库更新到数据库这一步,但二次构建函数其实涉及到了,所以一并在这里写了。

数据库表的结构:

CREATE TABLE `test` (
  `report_date` datetime DEFAULT NULL,
  `步骤名` text,
  `samples` int(11) DEFAULT NULL,
  `failures` int(11) DEFAULT NULL,
  `success_Rate` float DEFAULT NULL,
  `average_Time(ms)` int(11) DEFAULT NULL,
  `min_Time(ms)` int(11) DEFAULT NULL,
  `max_Time(ms)` int(11) DEFAULT NULL,
  `id` int(11) NOT NULL AUTO_INCREMENT,
  PRIMARY KEY (`id`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=0 DEFAULT CHARSET=utf8;

每次执行脚本后,查看数据库会发现很多用例的 success_Rate 为 0,即对应脚本中的事务控制器/HTTP提取器执行失败(非脚本问题导致的失败,若脚本有问题当然是修 jmeter 脚本),需要手动操作来测试再将正确性能数据更新到数据库中。

但这样一来,如果失败的用例很多,手动确认变很繁琐,如果重新跑一次可能不仅费时还可能会产生同样的结果,这些失败的原因大部分可能是环境因素。

因此本人想到根据数据库中的结果用代码实现对 jmx 文件进行修改,修改之后使得再次执行时只执行失败用例的部分。

.jmx 文件解析

首先需要对 jmeter 脚本进行解析。

Jmter 非 GUI 模式下录制脚本:

Jmeter 自动化二次构建:Jenkins+ant

用文本编辑器打开这个 .jmx 文件,查看后发现其格式为 xml,观察一个事务控制器的文本:

<TransactionController guiclass="TransactionControllerGui" testclass="TransactionController" testname="次管2登录" enabled="true">
          <boolProp name="TransactionController.includeTimers">false</boolProp>
          <boolProp name="TransactionController.parent">true</boolProp>
        </TransactionController>
        <hashTree>
          <HTTPSamplerProxy guiclass="HttpTestSampleGui" testclass="HTTPSamplerProxy" testname="619 /webroot/decision/login" enabled="true">
            <boolProp name="HTTPSampler.postBodyRaw">true</boolProp>
            <elementProp name="HTTPsampler.Arguments" elementType="Arguments">
              <collectionProp name="Arguments.arguments">
                <elementProp name="" elementType="HTTPArgument">
                  <boolProp name="HTTPArgument.always_encode">false</boolProp>
                  <stringProp name="Argument.value">{"username":"2","password":"1","validity":-1,"sliderToken":"","origin":"","encrypted":false}</stringProp>
                  <stringProp name="Argument.metadata">=</stringProp>
                </elementProp>
              </collectionProp>
            </elementProp>
            <stringProp name="HTTPSampler.domain">${IP}</stringProp>
            <stringProp name="HTTPSampler.port">${Port}</stringProp>
            <stringProp name="HTTPSampler.protocol">http</stringProp>
            <stringProp name="HTTPSampler.contentEncoding"></stringProp>
            <stringProp name="HTTPSampler.path">/webroot/decision/login</stringProp>
            <stringProp name="HTTPSampler.method">POST</stringProp>
            <boolProp name="HTTPSampler.follow_redirects">true</boolProp>
            <boolProp name="HTTPSampler.auto_redirects">false</boolProp>
            <boolProp name="HTTPSampler.use_keepalive">true</boolProp>
            <boolProp name="HTTPSampler.DO_MULTIPART_POST">false</boolProp>
            <stringProp name="HTTPSampler.embedded_url_re"></stringProp>
            <stringProp name="HTTPSampler.connect_timeout"></stringProp>
            <stringProp name="HTTPSampler.response_timeout"></stringProp>
          </HTTPSamplerProxy>
          <hashTree>
            <HeaderManager guiclass="HeaderPanel" testclass="HeaderManager" testname="HTTP信息头管理器" enabled="true">
              <collectionProp name="HeaderManager.headers">
                <elementProp name="Referer" elementType="Header">
                  <stringProp name="Header.name">Referer</stringProp>
                  <stringProp name="Header.value">http://${IP}:${Port}/webroot/decision/login</stringProp>
                </elementProp>
                <elementProp name="Accept-Language" elementType="Header">
                  <stringProp name="Header.name">Accept-Language</stringProp>
                  <stringProp name="Header.value">zh-CN,zh;q=0.9</stringProp>
                </elementProp>
                <elementProp name="Origin" elementType="Header">
                  <stringProp name="Header.name">Origin</stringProp>
                  <stringProp name="Header.value">http://${IP}:${Port}</stringProp>
                </elementProp>
                <elementProp name="X-Requested-With" elementType="Header">
                  <stringProp name="Header.name">X-Requested-With</stringProp>
                  <stringProp name="Header.value">XMLHttpRequest</stringProp>
                </elementProp>
                <elementProp name="Content-Type" elementType="Header">
                  <stringProp name="Header.name">Content-Type</stringProp>
                  <stringProp name="Header.value">application/json</stringProp>
                </elementProp>
                <elementProp name="Accept-Encoding" elementType="Header">
                  <stringProp name="Header.name">Accept-Encoding</stringProp>
                  <stringProp name="Header.value">gzip, deflate</stringProp>
                </elementProp>
                <elementProp name="Accept" elementType="Header">
                  <stringProp name="Header.name">Accept</stringProp>
                  <stringProp name="Header.value">application/json, text/javascript, */*; q=0.01</stringProp>
                </elementProp>
                <elementProp name="User-Agent" elementType="Header">
                  <stringProp name="Header.name">User-Agent</stringProp>
                  <stringProp name="Header.value">Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/85.0.4183.121 Safari/537.36</stringProp>
                </elementProp>
              </collectionProp>
            </HeaderManager>
            <hashTree/>
            <RegexExtractor guiclass="RegexExtractorGui" testclass="RegexExtractor" testname="正则表达式提取器" enabled="true">
              <stringProp name="RegexExtractor.useHeaders">false</stringProp>
              <stringProp name="RegexExtractor.refname">token_value</stringProp>
              <stringProp name="RegexExtractor.regex">accessToken":"(.*?)"</stringProp>
              <stringProp name="RegexExtractor.template">$1$</stringProp>
              <stringProp name="RegexExtractor.default"></stringProp>
              <stringProp name="RegexExtractor.match_number"></stringProp>
            </RegexExtractor>
            <hashTree/>
            <ResponseAssertion guiclass="AssertionGui" testclass="ResponseAssertion" testname="响应断言" enabled="true">
              <collectionProp name="Asserion.test_strings">
                <stringProp name="153190744">{"data":{"username":"2",</stringProp>
              </collectionProp>
              <stringProp name="Assertion.custom_message"></stringProp>
              <stringProp name="Assertion.test_field">Assertion.response_data</stringProp>
              <boolProp name="Assertion.assume_success">false</boolProp>
              <intProp name="Assertion.test_type">16</intProp>
            </ResponseAssertion>
            <hashTree/>
          </hashTree>
        </hashTree>

通过分析,我们需要处理的信息提取如下:

  1. * 每个事务控制控制器对应 * 事务控制器的名称对应:testname="..." * 事务控制器的启用/禁用:enabled="true" /enabled="false" * 脚本执行后结果呈现单位为事务控制器还是HTTP取样器:true
  2. * 每个事务控制控制器对应 * HTTP 取样器的名称对应:testname="..." * HTTP 取样器的启用/禁用:enabled="true" /enabled="false"
  3. * 每个正则表达式提取器对应

通过 1 的第四条判断我们处理(禁用)的单位是事务控制器 or HTTP 取样器,禁用数据库中成功的单位,包含正则表达式提取器的单位不可禁用。

由于参数较多,附加了一个配置文件 config.ini:

; 性能自动化

[db]
; comment = '自动化脚本执行后存储结果的数据库信息'
host = localhost
user = root
password = root
database = finework
table = auto_results

[html]
; comment = 'html_folder:jmeter 脚本执行的 html 结果存放地址'
html_folder = D:\Ginny\jmx_auto\html_results

[jmx]
; comment = 'jmx_folder:jmeter 脚本存放地址;jmx_exc:jmeter 脚本执行的 os 命令'
jmx_folder = D:\Ginny\jmx_auto\jmx
jmx_exc = D:\Ginny\jmx_auto\test.bat

下面是流程图:

Jmeter 自动化二次构建:Jenkins+ant

工程代码:github:jmxReconstruction.py

Jenkins 同样建一个 Freestyle project,在 Build 下建一个 Execute Windows batch command:

python jmxReconstruction.py "config.ini" "2021-12-28 20:54:00"

同一批次执行的脚本存储到数据库中的 report_date 是一样的,所以二次构建根据时间来分组。

Build now 执行结果:

Jmeter 自动化二次构建:Jenkins+ant

上一篇:go-admin的Ant Design Pro实践


下一篇:Jmeter+ant+Jenkins 搭建需要配置的文件