准备工作
首先需要解决两个问题:
1、用什么发请求(目前是http)
2、测试结果的统计(如响应时间,失败次数,表格等等),毕竟有清晰的结果统计和展示,对于性能的分析十分有参考意义
先从一个简单的例子看下这两个问题怎样解决,以经典的访问百度为例
demo.py
from locust import task, SequentialTaskSet
from locust.contrib.fasthttp import FastHttpUser
import time, requests
class MockUserTask(SequentialTaskSet):
@task
def curl_baidu(self):
self.client.get("http://www.baidu.com/")
@task
def sleep_to_end(self):
requests.get("http://www.baidu.com/")
time.sleep(100)
class UserInfo(FastHttpUser):
tasks = [MockUserTask]
抓包结果如下:
可以看到有两次目标为百度的get的请求
换到locust统计图上看:
locust只统计了一次,即自己调用requests发的请求是不会被收入统计的
实际上从官方文档上实现自定义的协议测试时就可以了解到统计成功和响应时间的实现了,
self.locust_events.request_success.fire
self.locust_events.request_failure.fire
因此两个问题的答案如下:
1、如果是http请求,使用self.client即可
2、继承的FastHttpUser中实现了结果的统计
同时可以扩展下:
非http请求,可以参考官方的实现,定义一个你要用到的协议的AgreementUser
也可以直接使用第三方模块(我自己用的websocket就是这样),使用locust_events.request_failure.fire来统计时间,成功失败次数(会在websocket中讲解)
【至于要不要改写成高性能(gevent实现)的协议客户端,则是看你需求了,可参看社区中locust-plugins】
现在问题解决了,就可以完成项目中的登录功能了
实现登录
在实际项目中,由于登录会在各处都需要使用,而且登录中一些拼接规则也是通用的,所以我们可以实现一个get_token的公共类,
实现刷新token
项目中需要定时刷新token,且后续请求都要换成新token,可以实现一个刷新token的线程类
import threading, time
import sys
sys.path.append('../')
from test_model_global import box_login, get_cfg, general_request, get_token
class GetIdAndRefreshTokenThread(threading.Thread):
box_token = ''
box_id = ''
refresh_token_url = "/mhauth/refresh"
def __init__(self, **kargs):
super().__init__()
self.token_refresh_time = kargs.get("token_refresh_time")
self.user_name = kargs.get("user_name")
self.http_client = kargs.get("http_client")
self.stop_flag = False
def setStopFlag(self, flag):
self.stop_flag == flag
def getBoxToken(self):
return self.box_token
def getBoxId(self):
return self.box_id
def run(self):
cfg_mes = get_cfg.GetCfgMes('../test_model_global/global_cfg.ini').getGlobalCfgLogin()
box_login_account_cfg = {"server_ip": cfg_mes[0],
"user_name": self.user_name,
"appkey": cfg_mes[2],
"http_client": self.http_client }
ret = box_login.BoxLogin(**box_login_account_cfg).boxLogin()
if ret == -1:
print("error:login default")
return False
self.box_token = ret[3]
self.box_id = ret[1]
payload = {"refresh_token":ret[0]}
cfg_mes = get_cfg.GetCfgMes('../test_model_global/global_cfg.ini').getGlobalCfgTest()
while True:
if self.stop_flag:
break
else:
box_refresh_token_cfg = {"server_ip": cfg_mes[0],
"realtime_token": self.box_token,
"ruquest_url": self.refresh_token_url,
"http_client": self.http_client }
time.sleep(int(self.token_refresh_time)*60)
self.box_token = general_request.GeneralRequest(**box_refresh_token_cfg).generalPost(payload, get_ret=True)["token"]
其中初始化参数中需要传入self.client发起http请求
在run中先登录拿到第一个token,再实现一个死循环,隔一段时间调用一次刷新token的接口
通过一个开关来退出死循环(测试结束时关闭请求发送)
定义一个getBoxToken的函数来返回token
登录代码如下:
import requests, json, hashlib, re, base64, time
class GetToken():
def __init__(self, **kargs):
self.server_ip = kargs.get("server_ip")
self.user_name = kargs.get("user_name")
self.user_password = kargs.get("user_password")
self.appkey = kargs.get("appkey")
self.http_client =kargs.get("http_client")
def myMd5(self, md5_str):
m = hashlib.md5()
b = md5_str.encode(encoding = "utf-8")
m.update(b)
ha1_md5 = m.hexdigest()
return ha1_md5
def getAuthorization(self, web_token):
web_authorization = base64.b64encode(bytes(web_token + ":" + str(int(time.time())) + ":v1", encoding = "utf-8"))
return web_authorization
def loginDevice(self, login_url):
device_login_url = self.server_ip + login_url
with self.http_client.post(device_login_url, data = json.dumps({"username":self.user_name, "type":"box", "appkey":self.appkey, "isandroid":True}), catch_response=True) as response:
if response.status_code == 401:
response.success()
else:
response.failure("error code: "+ str(response.status_code))
return -1
r = response
r_authenticate = r.headers['Www-Authenticate']
digest_realm = eval(r_authenticate.split(',')[0].split('=')[1])
nonce = eval(r_authenticate.split(',')[1].split('=')[1])
password = ""
for i in self.user_name[-8:]:
password = password + nonce[int(i)]
response = self.myMd5(self.myMd5(self.user_name + ":" + digest_realm + ":" + password) + nonce)
auth_str = "Digest username="+ "\"" + self.user_name + "\"" + "," + "realm="+ "\"" + digest_realm + "\""+ "," + "nonce="+ "\"" + nonce + "\"" + "," + "response="+ "\"" + response + "\""
headers = {
"Authorization": auth_str
}
r1 = self.http_client.post(device_login_url, headers = headers)
if r.text != None:
r1_text_json = json.loads(r1.text)
if r1.status_code == 200 and "token" in r1_text_json:
r1_text_eval = eval(r1.text)
return r1_text_eval["refresh_token"], r1_text_eval["userid"], r1_text_eval["expires"], r1_text_eval["token"]
else:
return -1
else:
return -1
def loginWeb(self, login_url):
weblogin_payload = {
"username": self.user_name,
"type": "web",
"appkey": self.appkey
}
headers = {"Content-Type": "application/json;charset=UTF-8"}
web_url = self.server_ip + login_url
# print("o(╥﹏╥)o")
r = requests.post(web_url, data=json.dumps(weblogin_payload), timeout=60)
# print(r.status_code)
response_data = json.loads(r.text)
# print(response_data)
str_pat = re.compile(r'\"(.*)\"')
# print(response_data["realm"], response_data["nonce"])
md5_realm = str_pat.findall(response_data["realm"])
md5_nonce = response_data["nonce"]
# print(md5_realm[0])
# print(md5_nonce)
md5_str = self.user_name + ":" + md5_realm[0] + ":" + self.user_password
# print(md5_str)
response = self.myMd5(self.myMd5(md5_str) + md5_nonce)
auth_str = 'Digest realm=' + "\"" + md5_realm[0] + "\"" + ',' + "username=" + "\"" + self.user_name + "\"" + "," + \
"nonce=" + "\"" + md5_nonce + "\"" + ',' + 'response=' + "\"" + response + "\""
# print(auth_str)
headers = {
"Authorization": auth_str
}
# print( md5_realm[0],str(md5_nonce), response)
r1 = requests.post(web_url, headers=headers)
# print(r1.status_code)
r1_text_json = json.loads(r1.text)
# print(r1_text_json)
if "token" in r1_text_json:
# print(r1_text_json["token"])
return r1_text_json["token"]
一个典型的挑战登录,根据实际项目中业务流程来即可(如果用jmeter的话 上述流程估计无法这么直接的完成,得点好多次按钮和一堆可读性差的beanshell)
封装的常用请求
业务中需要带鉴权的部分将其写到请求中,后续请求只用关注接口链接和参数即可
import requests, json
import sys
sys.path.append('../')
from test_model_global import get_token
class GeneralRequest(get_token.GetToken):
def __init__(self, **kargs):
super().__init__(**kargs)
self.server_ip = kargs.get("server_ip")
self.realtime_token = kargs.get("realtime_token")
self.ruquest_url = kargs.get("ruquest_url")
self.http_client = kargs.get("http_client")
def generalPost(self, payload, get_ret = False):
headers = {"Content-Type": "application/json;charset=UTF-8"}
headers["Authorization"] = 'Basic {}'.format(str(super().getAuthorization(self.realtime_token), encoding="utf-8"))
r = self.http_client.post(self.server_ip + self.ruquest_url, headers = headers, json = payload)
if get_ret == True:
if r.text != None:
r_text_json = json.loads(r.text)
return r_text_json
else:
return None
if get_ret == False:
pass
def generalGet(self, params, get_ret = False):
headers = {"Content-Type": "application/json;charset=UTF-8"}
headers["Authorization"] = 'Basic {}'.format(str(super().getAuthorization(self.realtime_token), encoding="utf-8"))
r = self.http_client.get(self.server_ip + self.ruquest_url, headers = headers, params=params)
if get_ret == True:
r_text_json = json.loads(r.text)
return r_text_json
def generalPostData(self, payload, get_ret = False):
headers = {"Content-Type": "application/json;charset=UTF-8"}
headers["Authorization"] = 'Basic {}'.format(str(super().getAuthorization(self.realtime_token), encoding="utf-8"))
r = self.http_client.post(self.server_ip + self.ruquest_url, headers = headers, data = json.dumps(payload))
if get_ret == True:
r_text_json = json.loads(r.text)
return r_text_json
完成登录task
class MockUserTask(SequentialTaskSet):
cfg_mes = get_cfg.GetCfgMes('../test_model_global/global_cfg.ini').getGlobalCfgTest()
t_get_refresh_token = ''
box_sn = ''
# 开户销户放到单独的模块
# 业务流程1:登录并定时获取新的token,要用单独一个线程刷新token
# 将user的fasthttp client作参数传给其他类中,可以将代码分离或者复用之前的requests模块的业务
@task
def get_refresh_token_task(self):
self.box_sn = self.user.sn_queue.get()
box_refresh_token_cfg = {"server_ip": self.cfg_mes[0],
"user_name": self.box_sn,
"token_refresh_time": self.cfg_mes[4],
"http_client": self.client}
t_token = get_box_id_token_t.GetIdAndRefreshTokenThread(**box_refresh_token_cfg)
t_token.setDaemon(False)
t_token.start()
self.t_get_refresh_token = t_token
class UserInfo(FastHttpUser):
sn_queue = queue.Queue()
for i in range(0, 400):
sn_queue.put_nowait(str(999999900000 + int(i)))
tasks = [MockUserTask]
这样在task中,我们关注的就是task本身的业务描述,第一个task就是完成登录并且提供了一个定时刷新token的引用调用getBoxToken: t_get_refresh_token.getBoxToken()(即线程类的对象),后续的请求中如:
@task
def heart_beat_notify(self):
send_heart_beat_cfg = {"basic_authorization":'Basic {}'.format(str(get_token.GetToken().getAuthorization(self.t_get_refresh_token.getBoxToken()), encoding="utf-8")),
"locust_events":events}
t_notify = send_heart_beat_and_notify_t.SendHeartBeatAndNotifyThread(**send_heart_beat_cfg)
t_notify.setDaemon(False)
t_notify.start()
self.t_heart_beat_notify = t_notify
t_get_refresh_token.getBoxToken()则是传入刷新后的token