python代码学习——mock的使用和自定义函数的扩展

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表格的设计如下:
      在这里插入图片描述
  • 第七步:因为注册的接口,需要用到验证码校验接口的文本内容,因此在响应结果提取字段,们也需要加上相关的判断

  • 注意:手机号码首次存入全局字典可以用函数;但是后续接口中用到手机号码的话,就需要使用全局字典中的变量,保证使用的是一个手机号

  • 代码如下:

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
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值