Jmeter Element介绍7:Switch

学习jmeter最好的方式还是去看官方方档

switch控制器介绍

switch控制器比较好理解,类似于C/语言中的switch,每一次只运行switch下的一个元件。

  • 当设置switch value为数字时
    1. switch value会被当做子元件的位置来选择需要运行的元件
    2. 子元件排序从0开始
    3. switch value超出元件位置时,switch会选择第0个元件
    4. switch支持有一个名字为default的子元件,但是在用数字来控制选择时,default是无效的,当作一个普通的子元件,超出范围时不选择default,二十选择0
    5. default仅在使用字符串选择时才有效,且default这个子元件不区分大小写
  • 当使用字符串选择时
    1. 字符串是大小写敏感的
    2. 如果字符串和所有元件名称不同,则会选择default(此时大小写不敏感)

用C++来模拟switch控制器

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
#include <iostream>

#define array_len(x) (sizeof(x)/sizeof(x[0]))

void switch_controll(int switch_value) {
switch(switch_value) {
case 0:
DEFAULT_SELECT:
std::cout << "request baidu" << std::endl;
break;
case 1:
std::cout << "request qq" << std::endl;
break;
case 2:
std::cout << "request google" << std::endl;
break;
default:
goto DEFAULT_SELECT;

}
}


void switch_controll(std::string switch_value) {
std::string key_index[] = {
"get_baidu", "get_qq", "get_google"
};

int len = array_len(key_index);
int switch_index = -1;
for (int index=0; index < len; ++index) {
if (key_index[index] == switch_value) {
switch_index = index;
break;
}
}
std::cout << "using :" << switch_value << std::endl;
switch_controll(switch_index);
}

如下图中,我们使用while控制器的来控制switch时,运行结果如下:
switch控制器

switch控制器运行结果

本例中测试用例

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
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
<?xml version="1.0" encoding="UTF-8"?>
<jmeterTestPlan version="1.2" properties="5.0" jmeter="5.1.1 r1855137">
<hashTree>
<TestPlan guiclass="TestPlanGui" testclass="TestPlan" testname="Test Plan" enabled="true">
<stringProp name="TestPlan.comments"></stringProp>
<boolProp name="TestPlan.functional_mode">false</boolProp>
<boolProp name="TestPlan.tearDown_on_shutdown">true</boolProp>
<boolProp name="TestPlan.serialize_threadgroups">false</boolProp>
<elementProp name="TestPlan.user_defined_variables" elementType="Arguments" guiclass="ArgumentsPanel" testclass="Arguments" testname="User Defined Variables" enabled="true">
<collectionProp name="Arguments.arguments"/>
</elementProp>
<stringProp name="TestPlan.user_define_classpath"></stringProp>
</TestPlan>
<hashTree>
<ThreadGroup guiclass="ThreadGroupGui" testclass="ThreadGroup" testname="Thread Group" enabled="true">
<stringProp name="ThreadGroup.on_sample_error">continue</stringProp>
<elementProp name="ThreadGroup.main_controller" elementType="LoopController" guiclass="LoopControlPanel" testclass="LoopController" testname="Loop Controller" enabled="true">
<boolProp name="LoopController.continue_forever">false</boolProp>
<stringProp name="LoopController.loops">1</stringProp>
</elementProp>
<stringProp name="ThreadGroup.num_threads">1</stringProp>
<stringProp name="ThreadGroup.ramp_time">1</stringProp>
<boolProp name="ThreadGroup.scheduler">false</boolProp>
<stringProp name="ThreadGroup.duration"></stringProp>
<stringProp name="ThreadGroup.delay"></stringProp>
</ThreadGroup>
<hashTree>
<ResultCollector guiclass="ViewResultsFullVisualizer" testclass="ResultCollector" testname="View Results Tree" enabled="true">
<boolProp name="ResultCollector.error_logging">false</boolProp>
<objProp>
<name>saveConfig</name>
<value class="SampleSaveConfiguration">
<time>true</time>
<latency>true</latency>
<timestamp>true</timestamp>
<success>true</success>
<label>true</label>
<code>true</code>
<message>true</message>
<threadName>true</threadName>
<dataType>true</dataType>
<encoding>false</encoding>
<assertions>true</assertions>
<subresults>true</subresults>
<responseData>false</responseData>
<samplerData>false</samplerData>
<xml>false</xml>
<fieldNames>true</fieldNames>
<responseHeaders>false</responseHeaders>
<requestHeaders>false</requestHeaders>
<responseDataOnError>false</responseDataOnError>
<saveAssertionResultsFailureMessage>true</saveAssertionResultsFailureMessage>
<assertionsResultsToSave>0</assertionsResultsToSave>
<bytes>true</bytes>
<sentBytes>true</sentBytes>
<url>true</url>
<threadCounts>true</threadCounts>
<idleTime>true</idleTime>
<connectTime>true</connectTime>
</value>
</objProp>
<stringProp name="filename"></stringProp>
</ResultCollector>
<hashTree/>
<WhileController guiclass="WhileControllerGui" testclass="WhileController" testname="whileswitch" enabled="true">
<stringProp name="WhileController.condition">${__javaScript(${__jm__whileswitch__idx}!=5)}</stringProp>
</WhileController>
<hashTree>
<SwitchController guiclass="SwitchControllerGui" testclass="SwitchController" testname="Switch Controller" enabled="true">
<stringProp name="SwitchController.value">${__jm__whileswitch__idx}</stringProp>
</SwitchController>
<hashTree>
<HTTPSamplerProxy guiclass="HttpTestSampleGui" testclass="HTTPSamplerProxy" testname="request_qq" enabled="true">
<elementProp name="HTTPsampler.Arguments" elementType="Arguments" guiclass="HTTPArgumentsPanel" testclass="Arguments" testname="User Defined Variables" enabled="true">
<collectionProp name="Arguments.arguments"/>
</elementProp>
<stringProp name="HTTPSampler.domain">www.qq.com</stringProp>
<stringProp name="HTTPSampler.port"></stringProp>
<stringProp name="HTTPSampler.protocol">https</stringProp>
<stringProp name="HTTPSampler.contentEncoding"></stringProp>
<stringProp name="HTTPSampler.path"></stringProp>
<stringProp name="HTTPSampler.method">GET</stringProp>
<boolProp name="HTTPSampler.follow_redirects">false</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/>
<HTTPSamplerProxy guiclass="HttpTestSampleGui" testclass="HTTPSamplerProxy" testname="request_baidu" enabled="true">
<elementProp name="HTTPsampler.Arguments" elementType="Arguments" guiclass="HTTPArgumentsPanel" testclass="Arguments" testname="User Defined Variables" enabled="true">
<collectionProp name="Arguments.arguments"/>
</elementProp>
<stringProp name="HTTPSampler.domain">www.baidu.com</stringProp>
<stringProp name="HTTPSampler.port"></stringProp>
<stringProp name="HTTPSampler.protocol">https</stringProp>
<stringProp name="HTTPSampler.contentEncoding"></stringProp>
<stringProp name="HTTPSampler.path"></stringProp>
<stringProp name="HTTPSampler.method">GET</stringProp>
<boolProp name="HTTPSampler.follow_redirects">false</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/>
<HTTPSamplerProxy guiclass="HttpTestSampleGui" testclass="HTTPSamplerProxy" testname="request_163" enabled="true">
<elementProp name="HTTPsampler.Arguments" elementType="Arguments" guiclass="HTTPArgumentsPanel" testclass="Arguments" testname="User Defined Variables" enabled="true">
<collectionProp name="Arguments.arguments"/>
</elementProp>
<stringProp name="HTTPSampler.domain">www.163.com</stringProp>
<stringProp name="HTTPSampler.port"></stringProp>
<stringProp name="HTTPSampler.protocol">https</stringProp>
<stringProp name="HTTPSampler.contentEncoding"></stringProp>
<stringProp name="HTTPSampler.path"></stringProp>
<stringProp name="HTTPSampler.method">GET</stringProp>
<boolProp name="HTTPSampler.follow_redirects">false</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/>
<HTTPSamplerProxy guiclass="HttpTestSampleGui" testclass="HTTPSamplerProxy" testname="default" enabled="true">
<elementProp name="HTTPsampler.Arguments" elementType="Arguments" guiclass="HTTPArgumentsPanel" testclass="Arguments" testname="User Defined Variables" enabled="true">
<collectionProp name="Arguments.arguments"/>
</elementProp>
<stringProp name="HTTPSampler.domain">www.163.com</stringProp>
<stringProp name="HTTPSampler.port"></stringProp>
<stringProp name="HTTPSampler.protocol">https</stringProp>
<stringProp name="HTTPSampler.contentEncoding"></stringProp>
<stringProp name="HTTPSampler.path"></stringProp>
<stringProp name="HTTPSampler.method">GET</stringProp>
<boolProp name="HTTPSampler.follow_redirects">false</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/>
</hashTree>
</hashTree>
</hashTree>
</hashTree>
</hashTree>
</jmeterTestPlan>

Centos7下安装docker以及基于docker的数据库

平常开发或测试过程中,总要搭建自己的开发或者测试环境。比较常用的数据库mysql/mongo/redis等总是需要安装部署的,而通过docker来部署这些服务,将会大大的减少工作量。

安装docker

1
2
3
4
sudo -i
yum install -y docker
service docker start
chkconfig docker on

使用docker安装mysql 5.6

1
2
docker pull mysql:5.6  # 如果不带版本号,直接使用`docker pull mysql`会拉取最新版本
docker run --name vm.mysql --restart=always -p 3306:3306 -e MYSQL_ROOT_PASSWORD=Test123456 -d mysql:5.6

参数说明:

  • –name 指定容器名称
  • -p 3306:3306 指定mysql使用3306端口,并映射到宿主机器3306端口
  • -e MYSQL_ROOT_PASSWORD=Test123456 初始化root用户密码
  • -d 以守护进程方式运行
  • mysql:5.6 需要运行的镜像
  • –restart=always 指定docker启动时自动运行服务,如果run的时候没有加这个参数,也可以使用docker update --restart=always vm.mysql来更新

使用docker安装/配置mongodb

1
2
3
4
5
6
7
8
9
docker pull mongo
docker run --name=vm.mongod --privileged=true -v $PWD/data:/data/db -v $PWD/etc:/etc/mongo -p 27017:27017 -d mongo --config /etc/mongo/mongod.conf

# 打开vm.mongod的shell操作
docker exec -it vm.mongod bash

mongo
use admin
db.createUser({user: "userAdmin", pwd: "Test123456", roles: [{role: "root", db: "admin"}]})

参数说明:

  • -v $PWD/data:/data/db 将当前目录下data目录挂载到docker /data/db目录下作为数据库存储路径
  • -v $PWD/etc:/etc/mongo 将当前目录下etc目录挂载到docker的/etc/mongo下
  • –config /etc/mongo/mongodb.conf 指定使用/etc/mongo/mongodb.conf文件作为配置文件启动服务

使用docker安装redis

1
2
docker pull redis
docker run --name vm.redis -p 6379:6379 -v $PWD/data:/data -v $PWD/etc:/etc/redis -d redis redis-server /etc/redis/redis.conf

Jmeter性能测试实例

前面一段时间公司开发新网站,用于展示一些足球想关数据,网站由PHP7开发接口,使用node调用接口渲染页面,并由websocket发送实时事件信息。需要对网站做一个压力测试,考虑了一下,最终选用jmeter来完成这个测试。

接口

  • betcompany:一个简单的数据接口,获取支持的赔率数据的公司
  • matchday:获取比赛日信息
  • conerOddsDetail:单个比赛角球赔率信息
  • detailed:单个比赛的详细信息
  • handicap:所有盘口数据(这个接口计算量非常大,且为实时数据,对服务器CPU消耗会非常高)
  • matchlist:所有比赛列表(这个接口的数据量比较大且服务器端计算压力也比较高,更多的是需要访问数据库的次数也很多)。
  • matchlist_origin:所有比赛列表,因服务器架构设计原因,实际上处理借口是matchlist_origin,而matchlist是对matchlist_origin一次转发

目的

  1. 测试出在中等压力或低压力下测试接口响应速度
  2. 测试出服务器并发能力

第一次测试

方案

每一个loop,对每一个接口作一次GET(两个GET之间随机sleep 1~500ms),共循环500次统计测试结果(此时handicap还没有开发完成)。

从下图可以看出来,一开始测试的时候,无压力matchlist接口都在1秒以上,完全无法接受,甚至后面几个接口都有错误。

第一次测试结果

第二次测试

服务器开发对matchlist接口进行优化(合并)以后,我们单独对这个接口做一下压力测试。限定jmeter的QPS,单独测试接口性能。

限定QPS CPU使用率(约) 平均相应时间 服务器实际吞吐量(平均)
40 80% 3244.59ms 24.18/s
20 70% 1001.85ms 19.96/s
15 50% 767.34ms 14.99/s

从上面表格中可以看出:

  1. 第二次测试时,matchlist接口,实际处理能力差不多也就在20个/秒左右,再增加压力,实际并发能力并没有提高,而响应时间却大幅度提升。压力增加到40个请求每秒的时候,实际处理能力也只是增加了4个每秒,然而响应时间已经增加到3秒多(用户体验会很差),CPU已经到80%以上(服务器运维一般报警设置为70~80%)。
  2. 请求数在15个一下时,并没有太高的服务起压力,而700多毫秒的速度和我们单个请求matchlist的响应速度基本相等

分析

实际上第二次测试时,matchlist接口像比较第一次测试已经有很大的优化,但是仍然不能接受,并发能力太弱,20个用户就能把网站高挂掉。

再次分析matchlist接口后发现,因为数据量很大,而且数据存储比较分散,这个接口需要多次请求数据库,并且还需要对数据进行一定量的计算,是导致这个接口慢的很大原因。还有原来设计也有一些不足,于是决定将很多分散的数据合并存储,并且更改架构,放弃原来转发机制等。

第三次测试

经过第二次测试以后,开发/SA再次花时间优化代码,以及服务器端配置,然后我们准备第三次测试。经过简单接以后发现matchlist接口性能确实有大幅度的提升,所以,此次测试方案选择所有接口开100个虚拟用户,每个用户不简隔的调用接口500次,共50000次(除handicap接口因为并发原因,未测完50000次),统计平均响应时间。具体测试数据如下表:

接口 平均响应时间 90th percentile 吞吐量(个/秒)
matchday 3.76 ms 4.00 ms 495.56
conerOddsDetail 52.35 ms 86.00 ms 415.49
detailed 22.86 ms 27.00 ms 456.77
handicap 5391 ms 7497 ms 17.05
matchlist 308.7 ms 890 ms 200.01
betcompany 33.13 ms 48.00 ms 1063.44

从表中我们可以看出,除了刚刚完成功能测试的handicap接口外,所有接口性能都已经大幅度提升,matchlist,也可以保证在CPU压力在80%以下,能够并发200个/秒,已经基本上可以符合要求了。但handicap仍然需要优化。

handicap这个接口的优化的难点在于,其本身计算量非常大,特表消耗CPU资源,且需要返回实时数据。因为实时数据,所以原来并没有加缓存。

第四次测试

再次分析过需求以后,讨论后认为handicap这个实时数据其实可以加一个缓存,过期时间设定为1秒中,同时再配合一些其他的优化。matchlist接口同样加上缓存。开发再进一步优化代码,我们进行第四次测试(只对matchlist和handicap重新测试)

接口 平均响应时间 90th percentile 吞吐量(个/秒)
matchday 3.50 ms 5.00 ms 495.56
conerOddsDetail 52.35 ms 86.00 ms 415.49
detailed 22.86 ms 27.00 ms 456.77
handicap 80.53 ms 142.00 ms 373.43
matchlist 145.17 ms 453.00 ms 403.44
betcompany 33.13 ms 48.00 ms 1063.44

至此,所有接口基本上性能都已经达到预定目标

下面时每一个接口的测试报告:

betcompany接口

competitions接口

conerOddsDetail接口

detailed接口

handicap接口

matchday接口

matchlist接口

Jmeter各Element介绍6:随机控制器、随机顺序控制器、Runtime控制器

随机控制器介绍

随机控制器(官方文档)功能 上和交错控制器有些类似,但是,每次迭代时,不是顺序的执行子组件,而是在每次迭代时,随机执行一个其下的组件。

从下图可看出,每次调用随机控制器时,只会执行其中1个子组件 ,且http1、http2、http3这3个子是随机出现的,并没有顺序

随机控制器

随机顺序控制器介绍

随机顺序控制器(官方文档)功能上和简单控制器有些类似,但其下子组件的执行顺序是随机的。

从下图可看出,第次执行到随机顺序控制器时,其下的组件都会被执行,但是http1和http2哪一个先执行,完全是随机的

随机顺序控制器

随机控制器和随机顺序控制器,虽然都有“随机”功能 ,但是其主要功能相差比较大。也可参考简单控制器交错控制器的区别

Runtime控制器介绍

Runtime控制器(官方文档)可控制本身执行时长,如果子组件执行时间没有达到设定的时长,循环执行;如果子组件的执行时间超过设定时间,则在最后一个请过完成后直接跳出,不再执行后续的请求(其内部按顺顺执行)。

从下图1中可看出,当sub_http1和sub_http2总请求时间不超过设定时长时,会循环运行sub_http1和sub_http2,直到消耗完设定时长时跳 出

Runtime控制器1

从下图2中可看出,当sub_http1请求时间超过设定时长时,根本不会执行到sub_http2

Runtime控制器2

Jmeter各Element介绍5:交错控制器

交错控制器介绍

交错控制器(官方文档),被线程访问时时,会交替的、让其下的其他组件轮流被执行(按顺序),每执行一个组件,越过其他子组件,直接退出交错组件;等下一次被秩代时,会执行另一个子组件,往复循环。如下图,多次访问interleave_main时,实际上baidu和qq是交替执行的

交错控制器

这玩意儿有啥用

这玩意比较适合,有一个或多个接口(功能)需要在其他接口中交错出现时,比如清除动作/日志动作;或者多个相似场景但是初始化方式不同等状况

Jmeter各Element介绍4:ForEach控制器

ForEach介绍

ForEach控制器(官方文档)是jmeter提供的另一种循环控制器。遍历一个变量集合,循环运行被其包裹的其他控制器,用shell脚本模拟ForEach:

1
2
3
4
5
6
7
8
9
10
#!/usr/bin/bash

__jm__foreach_1__idx=0 # Jmeter暴露出来的循环计数
inputVar=`seq 5`

for _ in $inputVar; do
# do somthing & returnVar=xxxx
returnVar="return ${__jm__foreach_1__idx}"
__jm__foreach_1__idx=`expr ${__jm__foreach_1__idx} + 1`
done

ForEach遍历变量时所有变量名应该符合这个规则:变量名前缀_数字(注:现在下划线已变更为可选项),在ForEach控制器的设置中,设置好前缀和起,止编号,Jmeter会自动根据规则引用变量。

ForEach控制器和While控制器一样,可以通过变量__jm__controller_name__idx来获取当前循环计数,计数从0开始。如下图例子中__jm__foreach_1__idx

ForEach控制器

这玩意儿有啥用

ForEach控制器特别适用同一个接口,同一参数,但是需要验证参数各种取值(或比较多的取值)时,可将参数存在一个set/一组变量中,然后使用ForEach控制器

ApacheJmeter各Element介绍(三):循环控制器和while控制器

学习jmeter最好的方式还是去看官方方档

控制器(controllers)

Jmeter控制有两种控制器:逻辑控制器(Logical Controllers)和取样器(Samplers),Jmeter靠各种控制的组合来驱动测试

逻辑控制器

逻辑控制器让用户组织,控制虚拟用户的时间/顺序/次数等。逻辑控制器控制被其包裏的所有控制器

简单控制器

简单控制器,实际上没有任何作用,仅让测试人员组织其他控制器而已,如下图,将http-get-1、http-get-2分别放在No.1和No.2两个不同的简单控制器中和直接放在线程组下,在执行时没有任何区别(相当于C语言中直接使用一组大括号括出一个域{}的语句)。
简单控制器

循环控制器

循环控制器介绍

循环控制器可控制其所包裹的其他控制器的执行次数。循环次数由测试人员指出(相当于C语言中for (int loop=5; loop>0; --loop) {}的语句)。当线程组中只有某一个或某一些请求需要多次运行时,此时可以使用循环控制器。

如下图,图中http-get-1,在线程组的一个循环内,将会被执行5次。如果钩选“永远”(即loop=-1)循环控制器将按死循环运行。
循环控制器

这玩意有啥用

循环控制器特别适用于明确知道需要运行多次(注意:次数明确知道)的场景

while控制器

while控制器介绍

while控制器,同样是控制它包裹的其他控制器循环。但它条件接受一个变量或函数,直到指定的条件为false时停止。

while循环条件取值:

  1. 为空时:当循环中最后一个取样器失败时,退出
  2. 为字符串“LAST”时:当循环中最后一个取样器失败时,退出;但当while循环控制器前面一个采样器失败时,不会进入while循环
  3. 为变量或其他函数:变量或函数返回值为false时退出while循环

注意: while控制器和C语言中的while循环有些不同:jmeter while控制器条件会在进入循环后被检查一次,在循环语句完成后再被检查一次,一共检查两次,所以,如果条件中放入非幂等函数,可能会出错

使用C语言来模拟while控制器:

1
2
3
4
5
6
7
8
9
10
11
12
13
bool condition(){
//do somthing
return false;
}

while (condition()) {
//do somthing

//正常do-while循环不需要调用下面这个判断,模拟jmeter while控制器时
if (!condition()) {
break;
}
}

从上面伪代码中可以看出,每一个循环,都要检查两次condition,所以,condition为非幂等函数时请特别注意。

while控制器会通过一个变量来告诉我们当前循环次数,变量名规则为:__jm__控制器名字__idx,如下图,我们while控制器名字为“while1”,则变量名字是:${__jm__while1__idx}。循环次数从0开始计数。

whil控制器

按照上图配置,我们运行jmeter,从下图的日志可以看出一个循环内流程如下:

  1. 检查条件
  2. 请求百度
  3. 请求QQ
  4. 再检查一次条件

whil控制器流程日志

这玩意有啥用

While控制器比较适用于有循环运行需求,但是循环数数/条件是动态变化的、由其他接口或场景控制的状况。

使用Python进行接口自动化测试工程组织模板

进行接口测试的时候,我们有很多工具可以使用,但是更加灵活可控的,仍然是自己写代码进行测试。这篇是介绍一个使用Python进行接口自动化测试的工程组织模板,从原来一个“Wallet”项目中精简而来,所以代码中含有很多wallet这个单词的变量或类,在自己工程中注意改名字

代码组织结构

代码组织结构

各主要目录说明

  1. \cases:这个目录中存放所有测试用例相关文档,如规划多少测试用例,测试用例描述等,以方便其他参与人员,可以根据文档去写case实现代码
  2. \config:这个目录下存放各种配置文件,最好可以区分不同的文件(需要在读取配置文件的代码中支持),对于不同的需要/作用的配置使用不同的文件区分
  3. \src:所有代码文件都在这个目录下
  4. \src\cases:所有测试用例实现文件存放在这个目录下
  5. \src\cases\base.py:这个问价下封装一个基类(如TestWallet),这个基类继承自unittest.TestCase,实现当前工程所有测试用例可能会使用到的一些公共函数,如登陆,注册,获取获取验证吗,检查点检查等等
  6. \src\cases\case_*.py:约定cases目录下所有以case_开头的文件为测试用例实现文件,每一个文件中一个测试用例,测试用例继承自TestWallet。如果是直接继承自unittest.TestCase,那么对于工程公共函数,将需要在每一个测试用例中都去实现
  7. \db:这个目录下封装数据库操作相关函数
  8. \utils:这个目录下封装一些基本公共函数,类库等,如md5计算,加解密函数,配置文件读取,单利模式装饰器等等杂项
  9. \reports:这个目录下,存放测试后生成的测试报告
  10. \logs:这个目录下存放测试过程中生成的日志文件
  11. \run.py:这个文件中,运行所有需要测试的测试用例

代码

相关代码可以看作者Github上项目

使用gitlfs存储大文件

我们知道git是为了管理代码而开发出来的,擅长的也是文本文件,但是我们项目中很难保证没有一些二进制文件甚至一些很大的二进制文件,而这些文件就有可能很大。如果需要将大文件传输到GitHub中,那么100M的限制,就需要想其他办法了,好在GitHub现在提供了git lfs服务。
PS:尽管有git lfs,但是严重不鼓励使用git来存储二进制文件,不鼓励用git存储大文件

不用git lfs状况

下面这张图就是作者想将项目提交到GitHub上时遇到的错误large file detected:
large_file_detected

这个时候我们一般处理方法都是把大文件找出来,放在其他地方,git中只存放代码,但这样作很麻烦,项目clone下来后,还要再根据文档,再去找以来的二进制文件;现在又另一种方案就是使用git lfs

安装使用

安装直接去官方下载页下载相对应的安装包即可,挥着如下:

1
2
3
4
# only for cent os7
curl -s https://packagecloud.io/install/repositories/github/git-lfs/script.rpm.sh | sudo bash
sudo yum install git-lfs
git lfs install

安装完成以后,使用命令 git lfs track "*.model" 来跟踪所有”.model”文件(这里是笔者项目中大文件),会生成一个”.gitattributes”的新文件,将这个文件加入项目中,再次commit以及push就可以了

Python3 RSA+AES混合加密(二)——加检验

前文(Python实现rsa_aes混合加密)中已经实现了RSA+AES混合加密,但是这个加密是单向且没有进行安全校验的,也意味着,任何人都可以伪装成发送者向接收着发送伪装的信息,因为公钥是公开的。改进的方式就是发送方使用自已的么钥对数据进行签名

我们假定服务方名称为server,用户方为client

一般逻辑(与前文稍有不同,注意粉红色的地方)

加密过程(以server为例)

  1. 准备好client的RSA公钥(注意,server使用client公钥:client_key_rsa_pub),用于对AES的密钥加密
  2. 准备好server的RSA私钥(server_key_rsa_pri),用于对数据进行签名
  3. 使用server_key_rsa_pri对原始数据进行签名得到sign
  4. 随机生成AES加密密钥:key_aes_rand(这个会经过RSA加密发送给接收方)
  5. 使用key_aes_rand对需要发送的数据msg进行AES加密(加密时需要对数据进行填充),得到encrypted_msg
  6. 第3步aes加密后的数据有些是不可见数据,网络传输时可能会出错,此时会再进一步进行base64编码,得到base64ed_encrypted_msg
  7. 对key_aes_rand使用key_rsa_pub进行RSA加密、base64,得到base64ed_encrypted_key_aes_rand
  8. sign、base64ed_encrypted_msg和base64ed_encrypted_key_aes_rand发送给接收方即可

解密过程

  1. 准备好client端自已RSA私钥(client_key_rsa_pri),用于对AES的密钥进行解密
  2. 准备好server的RSA公钥(server_key_rsa_pub),用于对进行签名校验
  3. 对接收到的数据拆分,得到sign、base64ed_encrypted_msg和base64ed_encrypted_key_aes_rand
  4. 对base64ed_encrypted_key_aes_rand进行base64解密,得到encrypted_key_aes_rand
  5. 对encrypted_key_aes_rand,使用RSA私钥key_rsa_pri解密,得到key_aes_rand
  6. 对base64ed_encrypted_msg进行base64解密,得到encrypted_msg
  7. 对encrypted_msg,使用key_aes_rand进行AES解密得到msg
  8. 使用server_key_rsa_pub对encrypted_msg和sign进行签名校验,校验成功说明不是伪造的数据

上代码

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
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
import base64
import os

# Crypto安装命令:`pip3 install -i https://pypi.douban.com/simple pycryptodome`
from Crypto import Random
from Crypto.Hash import SHA256
from Crypto.PublicKey import RSA
from Crypto.Signature import PKCS1_v1_5
from Crypto.Cipher import PKCS1_v1_5 as Cipher_pkcs1_v1_5
from Crypto.Cipher import AES


# AES根据16位对齐(即AES128)
BLOCK_SIZE = 16


# 伪随机数生成器
random_generator = Random.new().read


class RSAAES:
def __init__(self):
# 这里注意,发送方需要配置:发送方密钥,接收方公钥
# 接收方需要配置:接收方密钥,发送方公钥
self.key_private = b"" # 此为发送方私钥
self.key_public = b"" # 此为发送方公钥

@staticmethod
def pad(s: bytes):
return s + (BLOCK_SIZE - len(s) % BLOCK_SIZE) * \
chr(BLOCK_SIZE - len(s) % BLOCK_SIZE).encode()

@staticmethod
def unpad(s: bytes):
return s[:-ord(s[len(s) - 1:])]

@staticmethod
def get_random_aes_key(key_size: int = 16):
"""
生成随机密钥
"""
return base64.b64encode(os.urandom(int(key_size/4*3)))

@staticmethod
def decode_base64(data):
missing_padding = 4 - len(data) % 4
if missing_padding:
data += b'=' * 3
return base64.urlsafe_b64decode(data)

@staticmethod
def encode_base64(data):
data = base64.urlsafe_b64encode(data)
for i in range(3):
if data.endswith(b'='):
data = data[:-1]
return data

def sign_rsa(self, message: bytes):
"""
非对称签名(此函发送方调用)
:param message: 原始消息
:return:
"""
signer = PKCS1_v1_5.new(RSA.importKey(self.key_private))
signature = signer.sign(SHA256.new(message))
return self.encode_base64(signature)

def verify_rsa(self, message: bytes, signature: bytes):
"""
非对称验签(此函数接收方调用)
:param message: 原始消息
:param signature: 签名信息
:return:
"""
verifier = PKCS1_v1_5.new(RSA.importKey(self.key_public))
signature = signature.rstrip(b'')
if verifier.verify(SHA256.new(message), self.decode_base64(signature)):
return True
return False

def encrypt(self, msg: bytes):
# 生成随机AES密钥
key_aes_rand = self.get_random_aes_key(BLOCK_SIZE)

# 对原始信息进行AES加密再base64(需要网络传输选择urlsafe_b64encode)
encrypted_msg = AES.new(
key_aes_rand, AES.MODE_ECB).encrypt(self.pad(msg))
base64ed_encrypted_msg = base64.urlsafe_b64encode(encrypted_msg)

# 对AES随机密钥进行RSA加密
encrypted_key_aes_rand = Cipher_pkcs1_v1_5.new(
RSA.importKey(self.key_public)).encrypt(key_aes_rand)

base64ed_encrypted_key_aes_rand = base64.urlsafe_b64encode(
encrypted_key_aes_rand)

sign = self.sign_rsa(msg)
return {
"sign": sign.decode(),
"msg": base64ed_encrypted_msg.decode(),
"key": base64ed_encrypted_key_aes_rand.decode()
}

def decrypt(self, source: dict):
# 对接收到的数据拆分
sign, base64ed_encrypted_msg, base64ed_encrypted_key_aes_rand = \
source["sign"].encode(), source["msg"].encode(),\
source["key"].encode()

# 对base64ed_encrypted_key_aes_rand进行base64解密
encrypted_key_aes_rand = base64.urlsafe_b64decode(
base64ed_encrypted_key_aes_rand)

# 对encrypted_key_aes_rand,使用RSA私钥key_rsa_pri解密,得到key_aes_rand
key_aes_rand = Cipher_pkcs1_v1_5.new(
RSA.importKey(self.key_private)).decrypt(
encrypted_key_aes_rand, random_generator)

encrypted_msg = base64.urlsafe_b64decode(base64ed_encrypted_msg)
msg = self.unpad(AES.new(key_aes_rand, AES.MODE_ECB).decrypt(
encrypted_msg))

assert self.verify_rsa(msg, sign)
return msg


class Sender(RSAAES):
def __init__(self):
super().__init__()
self.key_public = b"""-----BEGIN PUBLIC KEY-----
MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDW1aA3SG/czhFO+57bFVIMx390
TMWXUY1ZTHW6gXTaK1gBfd7rHVaxLhmOsfIBaxBFfXg6lOfBQ+9mOZDY0LPzRRP2
1pmW+DW/QvrAO1xL1t04zKzPNkslCpjQ3fSwwIyJnfQsRQxUxUv9j/OJdWh97XNc
K4EyACqRC9eO9vJ18QIDAQAB
-----END PUBLIC KEY-----"""
self.key_private = b"""-----BEGIN PUBLIC KEY-----
MIICXQIBAAKBgQCsgzVEoL6Mwj92VH/ERMmyjC4dmG31WDAedALib1Z3mVCKOk5n
azTWJESgDp9UFV18LBveeEA1JRg5lk1wXRNShQ4A/q6FzIZ+vTB7FbXJC4vXzeq5
wor8Zj1j6JqQvz8sDnfvvdQqGfm3Rque51LWxeEcd+vRRDgLYqvsbwapDQIDAQAB
AoGAF5aFNRIJm/N/e+2H3s1NCuXR9GgAOPjK848HSfDRUN8cvRnF2Kw2+ETTQVNe
g7+8HZtmYB/vH5Un38/mXMPNPVRJK925kLC2/Qv2hzIX9x3KLzMeYpJ8FyE3MmWi
bHiKVOOB/cZ0LMQtjFgRYx8R162yJ9JiZ6/ddlMIlaf8kWECQQDwV6xC2VtAM8ZG
2mnL6RkkCywZ4ubsatO6SGiAq+4Y7uEtHgoc6f/rs9ftrQhcm+1lcY4julWmv5wn
ysY1SWw5AkEAt8BMBfvHUpiMU2F9g/0MEoveqQssxeFDOz70z8EYNYDYo/Yg7GwH
mmUN1tESzPcclYyCZocSP2tm5wjIRt/LdQJBAONeHZG0LHZFRKsczv9fyi/l/deT
Z2B7A0f0XiB0BjBCNHXJOEn4OOqTXY/0pLdvr5rLXWuBSKwSErk2RGJ+zkkCQCuv
hyOBCZFkfTAxpGKl3aHnKQethXaCKLbEL/XYpYXK3TaWBJvQzznwvoqM6Fhcg6o2
XqY7hKYZRby1xM+80yUCQQCFFLfjxTpVFXtExUKYHDMiREVOcOgfdiigBCztcC4W
W/YB6P7Tm9rGIapw2QqBRaHnAaeHmACKvdtnb1csdNA4
-----END PUBLIC KEY-----"""


class Receiver(RSAAES):
def __init__(self):
super().__init__()
self.key_private = b"""-----BEGIN RSA PRIVATE KEY-----
MIICWwIBAAKBgQDW1aA3SG/czhFO+57bFVIMx390TMWXUY1ZTHW6gXTaK1gBfd7r
HVaxLhmOsfIBaxBFfXg6lOfBQ+9mOZDY0LPzRRP21pmW+DW/QvrAO1xL1t04zKzP
NkslCpjQ3fSwwIyJnfQsRQxUxUv9j/OJdWh97XNcK4EyACqRC9eO9vJ18QIDAQAB
AoGARzKGZbaJ7AFsVw1TY4PjV1Izo9VgPnp2f7uz4IV6tmWwRX5I3F6IFoa9TZRy
LD970FZ5UTYmwDQa03lhzu5g/t4MBGZ+dEJ7hmcR4PJuLZSl59E2TzkEo9hE3DQ7
bq4wSIeBcY61hiwGf8tCnWetJbAvj+HS29HIB8sl1fu0jekCQQDgwiJm3HkgkoNq
mx5WliKvcGi+/eB9FXar9jn6762+mkge7htZbrM4tpIHl+5Vx+bn8E159FTcmgCS
My/WCb37AkEA9LJbn9GLvxupAcXq35snnq5xkNAsgHJHQaeu/VT0T2Gz5pUO3iB4
GLpWlia/E9eIDWj399Zs089AT72dAab0AwJAPtTmqxy9W+a5iEbe/1OvVJ43GhV8
+VrTtxT5dnYkeyFEQilMSf8RaSxYvHizrxVYLsTV098DDjybJkPa/pnwmwJAXeJk
5y/l92AseyKt2DdWfzqdFhvZRzsRfe5RZJ+I0UBCXxEH0FAS5CHygM/C9mD2sXZ5
1ZxuyuG04iN1LyIYcwJAULISlX5ZR8y+V6B1E/vndPu4BdfX5QORqzIwTAWsAt8k
uMsm5ubz+EEf4klTSVLhRZrxquVnTaBgiib6ngc9iA==
-----END RSA PRIVATE KEY-----"""
self.key_public = b"""-----BEGIN RSA PRIVATE KEY-----
MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCsgzVEoL6Mwj92VH/ERMmyjC4d
mG31WDAedALib1Z3mVCKOk5nazTWJESgDp9UFV18LBveeEA1JRg5lk1wXRNShQ4A
/q6FzIZ+vTB7FbXJC4vXzeq5wor8Zj1j6JqQvz8sDnfvvdQqGfm3Rque51LWxeEc
d+vRRDgLYqvsbwapDQIDAQAB
-----END RSA PRIVATE KEY-----"""


if __name__ == "__main__":
import json

sender, receiver = Sender(), Receiver()

message = "这是一个server主动发送给client的测试数据"
print("原始数据: {origin_msg}".format(origin_msg=message))
value = sender.encrypt(message.encode("utf-8"))
print("密文数据: {encrypt_msg}".format(
encrypt_msg=json.dumps(value, indent=4)))
msg = receiver.decrypt(value)
print("解密数据: {decrypt_msg}".format(decrypt_msg=msg.decode("utf-8")))

print("\n\n")
message = "这是一个client返回给server的信息"
print("原始数据: {origin_msg}".format(origin_msg=message))
value = receiver.encrypt(message.encode("utf-8"))
print("密文数据: {encrypt_msg}".format(
encrypt_msg=json.dumps(value, indent=4)))
msg = sender.decrypt(value)
print("解密数据: {decrypt_msg}".format(decrypt_msg=msg.decode("utf-8")))

运行上面代码

RSA/AES混合加密