mock的介绍
定义
- mock是python中一个用于支持的测试的库,他的主要功能是使用mock对象来替换掉指定的python对象,以达到模拟对象的目的。即模拟一下无条件进行操作的接口
- 使用场景:接口需要第三方接口来完成某个功能,例如电商的充值,不可能真金白银的去直接充值,正式环境的接口也没有超时的测试,因此使用mock来模拟实现。
- 安装:python3.3以上的直接使用
使用
定义mock类
class Mock(spec=None,side_effect=None,return_value =DEFAULT,name = None)
- spec :定义mock对象的属性,可以是一个列表,字符串,甚至是一个对象或者实例
- side_effect 可以用来抛出异常或者动态改变返回值,他必须是一个iterator,他可以覆盖return_value
- return_value 定义mock方法的返回值,他可以是一个值,也可以是一个对象
- name:作为mock对象的一个标识,在print时可以看到
- side_effect和return_value是定义Mock的返回值,但是side_effect更高级,如果两个都传了参数,默认使用side_effect,side_effect一定要是一个可迭代对象(元组、列表、字符串),注意Buer不是一个可迭代对象
参数的含义
- 使用:
pythonmock.Mock(spec=None,side_effect=None,return_value =DEFAULT,name =None)
- spec :定义mock对象的属性,可以是一个列表,字符串,甚至是一个对象或者实例
- side_effect 可以用来抛出异常或者动态改变返回值,他必须是一个iterator,他可以覆盖return_value
- return_value 定义mock方法的返回值,他可以是一个值,也可以是一个对象
name:作为mock对象的一个标识,在print时可以看到
代码示例:
- 需要测试的代码(可能调用不通的接口):
import requests
class Payment: #支付类
def requestOutoSystem(self,card_num,amount):
#请求第三方外部支付接口,并的返回响应码
#param card_num
#param amount
#return 返回状态码,200代表成功,500代表失败
url = "http://third.payment.pay" #第三方接口地址
data = {"card_num":card_num,"amount":amount}
try:
reponse = requests.post(url,data=data,timeout =1)#获取状态码
return reponse.status_code
except Exception as e:
return 404
def doPay(self,user_id,card_num,amount):
#支付
# user_id 用户ID
#card_num:卡号
#amount 支付金额
try:
#调用第三方接口请求真实扣款
resp = self.requestOutoSystem(card_num,amount)
except TimeoutError:
#如果超时,就重新的调用一次
resp = self.requestOutoSystem(card_num, amount)
if resp==200:
print("{0}支付成功".format(user_id))
return "success"
else:
print("{0}支付失败".format(user_id))
return "fail"
- mock的代码示例:
import unittest
from Basics_Python.class_mock_1 import Payment
from unittest import mock
class PaymentTest(unittest.TestCase):
def setUp(self):
self.payment = Payment()
def test_cases_success(self):
self.payment.requestOutoSystem = mock.Mock(return_value = 200)
#创建实例化对象并模拟requestOutoSystem的返回值为200
#使用mock后,requests的请求没有在运行
resp = self.payment.doPay(user_id =1,card_num='1234567',amount=100)
self.assertEqual('success',resp,'测试支付成功')
def test_cases_fail(self):
self.payment.requestOutoSystem = mock.Mock(return_value = 404)
#创建实例化对象并模拟requestOutoSystem的返回值为404
resp = self.payment.doPay(user_id =2,card_num='7890123',amount=10000)
self.assertEqual('fail',resp,'测试支付失败')
def test_retry_success(self):
self.payment.requestOutoSystem = mock.Mock(return_value = 500,side_effect=[TimeoutError,200] )
#创建实例化对象并模拟requestOutoSystem,返回两个值,
# side_effect必须是可迭代对象,在side_effect存在的情况下return_value不起作用
resp = self.payment.doPay(user_id =3,card_num='7890123',amount=10000)
self.assertEqual('success', resp, '测试支付成功')
def tearDown(self):
pass
mock的断言
- 作用:定义了mock对象或者方法的一些行为
- assert_called(*args,**kwargs) 判断mock的对象或者方法至少被调用一次
- assert_called_once(*args,**kwargs) 判断mock的对象或者方法只被调用一次
- assert_called_with(*args,**kwargs) 判断mock方法调用时使用了正确的参数
- assert_called_once_with(*args,**kwargs) 判断mock方法调用使用参数且只被调用一次
- assert_any_call(*args,**kwargs) 判断mock对象曾经被调用过
- assert_has_calls(calls,any_order=False) 判断多次被调用
- assert_not_called() 判断从未被调用过
- 使用mock断言的时候,传参方式的区别也会导致断言失败,例如一个位置传参,一个关键字传参,也会报错,如图
- 代码示例:
import unittest
from Basics_Python.class_mock_1 import Payment
from unittest import mock
class PaymentTest(unittest.TestCase):
def setUp(self):
self.payment = Payment()
def test_cases_success(self):
self.payment.requestOutoSystem = mock.Mock(return_value = 200)
#创建实例化对象并模拟requestOutoSystem的返回值为200
#使用mock后,requests的请求没有在运行
resp = self.payment.doPay(user_id =1,card_num='1234567',amount=100)
self.assertEqual('success',resp,'测试支付成功')
def test_cases_fail(self):
self.payment.requestOutoSystem = mock.Mock(return_value = 404)
#创建实例化对象并模拟requestOutoSystem的返回值为404
resp = self.payment.doPay(user_id =2,card_num='7890123',amount=10000)
self.assertEqual('fail',resp,'测试支付失败')
self.payment.requestOutoSystem.assert_called() #至少调用一次
self.payment.requestOutoSystem.assert_called_once()#只被调用一次
self.payment.requestOutoSystem.assert_any_call("7890123", 10000) # 判断曾经被调用
self.payment.requestOutoSystem.assert_has_calls('7890123',10000)#被多次调用
def test_retry_success(self):
self.payment.requestOutoSystem = mock.Mock(return_value = 500,side_effect=[TimeoutError,200] )
#创建实例化对象并模拟requestOutoSystem,返回两个值,
# side_effect必须是可迭代对象,在side_effect存在的情况下return_value不起作用
resp = self.payment.doPay(user_id =3,card_num='7890123',amount=10000)
self.assertEqual('success', resp, '测试支付成功')
#mock断言
self.payment.requestOutoSystem.assert_called_with('7890123',10000)#是否传参
self.payment.requestOutoSystem.assert_not_called() # 没有被调用
self.payment.requestOutoSystem.assert_called_once_with('7890123', 10000) # 判断调用次数
def tearDown(self):
pass
- 运行结果
2支付失败
Failure
Traceback (most recent call last):
File "D:\pycharm36\lib\unittest\case.py", line 59, in testPartExecutor
yield
File "D:\pycharm36\lib\unittest\case.py", line 605, in run
testMethod()
File "E:\BaiduNetdiskDownload\StudyPython\Basics_Pyrhon\Basics_Python\class_test_mock.py", line 39, in test_cases_fail
self.payment.requestOutoSystem.assert_has_calls('7890123',10000)#被多次调用
File "D:\pycharm36\lib\unittest\mock.py", line 860, in assert_has_calls
) from cause
AssertionError: ('7', '8', '9', '0', '1', '2', '3') not all found in call list
1支付成功
3支付成功
Failure
Traceback (most recent call last):
File "D:\pycharm36\lib\unittest\case.py", line 59, in testPartExecutor
yield
File "D:\pycharm36\lib\unittest\case.py", line 605, in run
testMethod()
File "E:\BaiduNetdiskDownload\StudyPython\Basics_Pyrhon\Basics_Python\class_test_mock.py", line 51, in test_retry_success
self.payment.requestOutoSystem.assert_not_called() # 没有被调用
File "D:\pycharm36\lib\unittest\mock.py", line 777, in assert_not_called
raise AssertionError(msg)
AssertionError: Expected 'mock' to not have been called. Called 2 times.
Ran 3 tests in 0.008s
FAILED (failures=2)
Process finished with exit code 1
mock的统计
- called : mock对象是否被调用
- call_count :mock对象被调用的次数
- called_args: 获取最近调用时使用的参数
- called_args_list 调用所有参数列表
- method_calls : 测试当前mock对象都调用了那些方法
- 代码示例:
import unittest
from Basics_Python.class_mock_1 import Payment
from unittest import mock
class PaymentTest(unittest.TestCase):
def setUp(self):
self.payment = Payment()
def test_retry_fail(self):
self.payment.requestOutoSystem = mock.Mock(side_effect=[TimeoutError,500] )
#创建实例化对象并模拟requestOutoSystem,返回两个值,
# side_effect必须是可迭代对象,在side_effect存在的情况下return_value不起作用
resp = self.payment.doPay(user_id =4,card_num='7890123',amount=10000)
self.assertEqual('fail', resp, '测试支付失败')
print("是否被调用:",self.payment.requestOutoSystem.called)
print("调用次数:", self.payment.requestOutoSystem.call_count)
print("调用参数:", self.payment.requestOutoSystem.call_args)
print("调用所有参数列表:", self.payment.requestOutoSystem.call_args_list)
print("mock调用方法:", self.payment.requestOutoSystem.method_calls)
def tearDown(self):
pass
# 结果:
4支付失败
是否被调用: True
调用次数: 2
调用参数: call('7890123', 10000)
调用所有参数列表: [call('7890123', 10000), call('7890123', 10000)]
mock调用方法: []
Ran 1 test in 0.003s
OK
Process finished with exit code 0
自定义函数的扩展
- 我们在进行接口自动化用例编写的时候,有些参数是需要通过复杂的操作来生成的
-
- 例如未注册的手机号码,用户名,或者登录/支付时要使用到的加密密码
-
- 实现方式:通过excel中,调用封装好的函数来使用
- 关于手机号码和用户名的解决方式详见文章
实现
-
第一步:封装好函数:faker_get_phone
-
第二步:在excel中,直接将某个函数作为标记参数写在请求参数中,为了和变量做区分,需要加特殊的标记(变量中没有的),例如末尾加一个括号,
{"mobile": "${faker_get_phone()}"}
,例如:
-
第三步:需要将封装好的函数独放置,例如common下的
PyAndFaker_operate
-
第四步:在替换函数中添加函数的识别代码,如果要替换的变量为
)
结尾,就使用eval执行函数 -
第五步:注意,函数执行需要将相关的方法导入文件,如果函数过多,建议用
*
-
第6步:为了兼容带参数的函数,需要对带参数的函数,是要再次使用正则匹配,使用if判断正则匹配结果,结果为True的情况下进入判断先替换参数
-
- 注意参数的替换要在eval执行函数之前
-
- excel表格的设计如下:
- excel表格的设计如下:
-
第七步:因为注册的接口,需要用到验证码校验接口的文本内容,因此在响应结果提取字段,们也需要加上相关的判断
-
注意:手机号码首次存入全局字典可以用函数;但是后续接口中用到手机号码的话,就需要使用全局字典中的变量,保证使用的是一个手机号
-
代码如下:
from loguru import logger
from API.CommonPy import FilePath
from API.CommonPy.PyAndFaker_operate import *
import requests
import json
import jsonpath
import re
#全局的环境变量的区域,用于存放从res中得到的jsonpath对应的结果
png_path = FilePath.png_path
global_data = {"path_png":f"{png_path}"}
class BasicOperate:
def __init__(self,casedatas:dict):
# 设置测试环境的域名
self.casedatas = casedatas
self.url_pre= "http://shop.lemonban.com:8107"
self.title = self.casedatas.get("title")
def send_api_requests(self):
# get请求方式,param作为字典传给data,不要直接拼接在url上
headers, param = self.casedatas.get("headers", "{}"), self.casedatas.get("param", "{}")
method =self.casedatas.get("method")
if self.casedatas.get("ID") != 9:
url = self.url_pre + self.casedatas.get("url")
else:
url = self.casedatas.get("url")
if self.casedatas.get("headers") is None:
headers = "{}"
if self.casedatas.get("param") is None :
param = "{}"
logger.info(f"============{self.title}请求信息==============")
logger.info(f"请求方法:{method}")
# 请求头的替换,请求参数的替换
param= self.re_sub_data(param)
headers = self.re_sub_data(headers)
# 替换json——path中的动态参数
jsonpath_data = self.re_sub_data(self.casedatas.get("jsonpath_data"))
url = self.re_sub_data(url)
self.casedatas["jsonpath_data"] = jsonpath_data
logger.info(f"请求头:{headers},{type(headers)}")
logger.info(f"请求参数:{param},{type(param)}")
logger.info(f"请求链接:{url}")
try:
if method.lower() == 'get':
res = requests.get(url, params=json.loads(param), headers=json.loads(headers))
self.extract_reponse(res)
return res
# post方式传参,需要判断请求头中的传参方式,然后传递给不同的参数
elif method.lower() == "post":
if "application/json" in headers:
res = requests.post(url=url, json=json.loads(param),headers=json.loads(headers))
#调用方法,提取字段对应的数据
self.extract_reponse(res)
# self.assert_db()
return res
elif "application/x-www-form-urlencoded" in headers:
res = requests.post(url, data=json.loads(param), headers=json.loads(headers))
self.extract_reponse(res)
return res
elif "multipart/form-data" in headers:
# 如果文件上传接口存在Content_type的话,接口会包500,因此这里要将请求头去掉
json.loads(headers)
headers = json.loads(headers)
headers.pop("Content-Type")
res = requests.post(url, files=eval(param), headers=headers)
self.extract_reponse(res)
return res
elif method.lower() == "put":
res = requests.put(url, json=json.loads(param),headers=json.loads(headers))
self.extract_reponse(res)
return res
except Exception as e:
logger.error(f"requests请求失败:{e}")
def response_assert(self,res):
# 拿到json形式的期望结果
expected_result = self.casedatas.get("expected_result")
if expected_result == None:
logger.error("断言预期结果为空,请检查excel表格中的数据")
return
expected_result = json.loads(expected_result)
logger.info(f"============={self.title}断言结果信息===============")
logger.info(f"断言text:{res.text}")
# logger.info(f"接口响应时间:{res.elapsed.total_seconds()}s")
# k变量名,v是jsonpath或者预期结果数据
for k,v in expected_result.items():
# k代表预期结果中的key,v代表预期结果中的value
if k == "status_code":
try:
assert res.status_code == v
logger.info(f"预期状态码:{v},实际状态码:{res.status_code}")
except Exception:
logger.error(f"预期状态码:{v},实际状态码:{res.status_code}")
raise
# 判断字段名是否正确,jsonpath表达式作为key
# 判断值作为value,jsonpath表达式以¥开头
elif k[0] == "$":
try:
assert jsonpath.jsonpath(res.json(),k)[0] == v
logger.info(f"预期验证字段:{v},实际验证字段:{jsonpath.jsonpath(res.json(), k)[0]}")
except Exception:
logger.error(f"预期验证字段:{v},实际验证字段:{jsonpath.jsonpath(res.json(),k)[0]}")
raise
elif k == "body_text":
try:
assert res.text == v
logger.info(f"预期验证文本:{v},实际验证文本:{res.text}")
except Exception:
logger.error(f"预期验证文本:{v},实际验证文本:{res.text}")
raise
# 提取响应的函数
def extract_reponse(self,res):
jsonpath_data = self.casedatas.get("jsonpath_data", "{}")
# 如果jsonpath_data中无数据,就直接返回
if jsonpath_data is None:
return
jsonpath_data = json.loads(jsonpath_data)
print(jsonpath_data)
for k,v in jsonpath_data.items():
if v[0] == "$":
# k代表存储环境变量中的变量名;v代表jsonpath表达式
value = jsonpath.jsonpath(res.json(),v)[0] #提取res中的值
elif v=="body_text":
value = res.text
logger.info(f"============{self.title}获取到的提取信息============")
logger.info(f"添加到全局字典中的jsonpath信息:{k}={value}")
global_data[k] = value
def re_sub_data(self,data):
if data is None:
return
str_pattern = '\\${(.*?)}\\$'
while re.search(str_pattern, data):
kname_result = re.search(str_pattern, data) # 找到的正则表达式匹配的字段
# 这里的key是获取到表格中的标记变量,可能是变量名,也可能是自定义函数名,因此下面要加if判断
fuction_or_item_name = kname_result.group(1)# 如果是是带参数的函数,这里的获取到的结果样例:${get_sql_valid("##{faker_get_phone}##")}
if fuction_or_item_name.endswith(")"):# 判断获取到的变量是不是以括号结尾,为了兼容传参数的情况,只判断最后半个括号
# 如果函数带参数,那么在执行之前要将函数的参数做替换,例如带手机号码查询数据库
# 因为是嵌套,所以需要再使用正则去进行匹配
str_pattern_1 = '\\${(.*?)}'
paramer_name = re.search(str_pattern_1, fuction_or_item_name) # 这里得到的结果为:##{faker_get_phone}##
if paramer_name is not None:
paramer_name = paramer_name.group(1)
paramer_value = str(global_data.get(paramer_name))
fuction_or_item_name = re.sub(str_pattern_1, paramer_value, fuction_or_item_name, count=1)
# 这里,替换后的结果为:get_sql_valid(18000000001)
#是以括号结尾,就是自定义函数,需要执行
data_value = eval(fuction_or_item_name)
# 然后将函数对应的结果保存到global字典中,格式:去除括号之后的函数名=值
fuction_or_item_name = fuction_or_item_name.replace("()","") # 去掉括号
global_data[fuction_or_item_name] = data_value # 存入字典的只是函数名
print(global_data)
else: # 如果不是以括号结尾
data_value = str(global_data.get(fuction_or_item_name))
# 获取全局变量字典中的值,转换为字符串
data = re.sub(str_pattern, data_value, data, count=1)
print("替换后的结果:",data)
return data