接口自动化实战之投资接口

接口介绍

在这里插入图片描述

excel测试用例

在这里插入图片描述

代码实操

先跑起来!

from middleware.Handler import Handler
import unittest
import ddt

#初始化Handler
env_data = Handler()
#初始化excel_handler
excel = env_data.excel
#初始化yaml_handler
yaml = env_data.yaml
#初始化logging_handler
log = env_data.logger
#获取excel中的测试用例
excel_data = excel.get_data("invest")

@ddt.ddt
class Test_invest(unittest.TestCase):
    #从Handler中获取excel中需要替换的数据:loan_id、member_id、token
    @classmethod
    def setUpClass(cls) -> None:
        cls.loan_id = env_data.get_loanId
        cls.member_id = env_data.get_userId
        cls.token = env_data.get_token
        #在执行测试用例前,先调用充值接口充值,以防账户余额不够
        env_data.recharge()

    #初始化Handler中的数据库对象
    def setUp(self) -> None:
        self.db = env_data.mysql_class()

    #关闭数据库对象
    def tearDown(self) -> None:
        self.db.close()

    @ddt.data(*excel_data)
    def test_invest(self,cases):
        excel_data = cases["data"]
        print(excel_data)

执行结果如下:
在这里插入图片描述

问题1:接口依赖

投资接口涉及到 个接口依赖,分别是登录接口、充值接口、添加项目接口、审核接口。为什么要依赖这么多接口呢?我们从请求参数说起。
第一个需要的参数为用户ID,自然就需要登录;第二个需要的是项目ID,自然就需要新增项目。而新增的项目状态为审核中,并不能进行投资,所以需要审核接口来把状态改为竞标中,这样就能进行投资了;第三个参数为投资金额,既然要投资,就要保证自己的账户钱是足够的,那怎么保证账户余额是足够用来投资的呢?一个方法是用一个余额很大的账户,投资金额可以填少一点。但这样接口运行很多遍之后,余额用完了怎么办呢?这时就需要更靠谱点的方法,我们需要调用充值接口来为账户充值。
另外还需要考虑到一点是,用户是投资自己的项目还是投资别人的项目?如果是投资别人的项目,就需要准备另一个投资账户(因为之前普通用户只准备了一个,如果添加项目和投资都使用这个账号就区分不开了)。这里我们简单点,投资自己的项目。
在这里插入图片描述
既然需要依赖这么多个接口,我们需要把这些接口写到哪里呢?写到测试模块里自然是不好的,因为这些依赖接口跟投资本身的业务是无关的,投资的测试模块应该只涉及到投资的代码逻辑,因此我们得把这些依赖接口统一放到middleware的Handler中。

  • handler.py
import os
from common import yaml_handler
from common import excel_handler
from common import logging_handler
from common import request_handler
from common.sql_handler import mysql_handler
from config import config
from pymysql.cursors import DictCursor
from jsonpath import jsonpath

class mysqlMiddleHandler(mysql_handler):
    def __init__(self):
        sql_conf = Handler.yaml["db"]
        #调用父类的初始化方法
        super().__init__(
            host=sql_conf["host"],
            port=sql_conf["port"],
            user=sql_conf["user"],
            password=sql_conf["password"],
            charset=sql_conf["charset"],
            cursorclass=DictCursor
        )

class Handler():
    #用变量接收config模块
    __conf = config
    #用变量接收CONFIG_PATH变量
    __config_path = __conf.CONFIG_PATH
    #初始化yaml_handler
    __yaml_path = os.path.join(__config_path,"config.yml")
    yaml = yaml_handler.read_yaml(__yaml_path)
    #初始化excel_handler
    __excel_conf = config.DATA_PATH
    __excel_name = yaml["excel"]["name"]
    __excel_path = os.path.join(__excel_conf,__excel_name)
    excel = excel_handler.excel_handler(__excel_path)
    #初始化logging_handler
    __logger_conf = yaml["log"]
    __logger_path = __conf.LOG_PATH
    logger = logging_handler.logging_handler(
        logger_name=__logger_conf["name"],
        logger_level=__logger_conf["logger_level"],
        stream_level=__logger_conf["stream_level"],
        file_name=os.path.join(__logger_path, __logger_conf["file"]),
        file_level=__logger_conf["file_level"]
    )
    """将mysqlMiddleHandler类当成对象赋值给变量mysql,这样在别的地方就不用再导入mysqlMiddleHandler类,
    而直接使用Handler类下的mysql_class类属性即可初始化一个mysqlMiddleHandler类的对象
    """
    mysql_class = mysqlMiddleHandler
    #通过get_token方法获取login方法中返回结果中的token的值
    #获取普通用户的token
    @property
    def get_token(self):
        user = self.login(self.yaml["user"])
        return user["token"]
    #获取管理员用户的token
    @property
    def get_admin_token(self):
        admin_user = self.login(self.yaml["admin_user"])
        return admin_user["token"]
    # 通过get_userId方法获取login方法中返回结果中的user_id的值
    @property
    def get_userId(self):
        user = self.login(self.yaml["user"])
        return user["user_id"]

    #通过get_loanId方法获取add_loan方法中的返回值
    @property
    def get_loanId(self):
        loan = self.add_loan()
        return loan

    #由于多个接口都需要依赖登录接口来登录,所以这里准备登录的接口请求
    def login(self,user):
        #通过request_handler请求登录接口,其中url可以从yaml中获取host再拼接上登录的url,json可以直接取yaml中的user配置项
        req = request_handler.requests_handler(
            url = self.yaml["host"] + "/member/login",
            method = "post",
            headers = {"X-Lemonban-Media-Type": "lemonban.v2 "},
            json = user
        )
        #获取返回结果中的token。由于token_info中的token_type和token的内容是分开的,所以需要拼接起来。另外需要注意取值时的层级关系
        token_type = jsonpath(req,"$..token_type")[0]
        token_content = jsonpath(req,"$..token")[0]
        token = " ".join([token_type, token_content])
        #获取返回结果中的用户id,因为充值接口需要用到member_id
        user_id = jsonpath(req,"$..id")[0]
        #返回token及user_id
        return {"token":token,"user_id":user_id}

    #由于其他有些接口需要依赖添加项目的接口,所以在这里准备添加项目的接口请求方法
    def add_loan(self):
        #准备headers
        token = self.get_token
        headers = {"X-Lemonban-Media-Type":"lemonban.v2","Authorization":token}
        #准备url
        url = self.yaml["host"]+"/loan/add"
        #准备请求参数
        data = {"member_id":1,"title":"买波音757","amount":2000,"loan_rate":12.0,"loan_term":3,"loan_date_type":1,"bidding_days":5}
        #调用request_handler请求add接口
        req = request_handler.requests_handler(
            url = url,
            method = "post",
            headers = headers,
            json = data
        )
        #获取返回结果中的id的值
        loan_id = jsonpath(req,"$..id")[0]
        return loan_id

    #审核项目
    def audit(self):
        #准备url
        url = Handler.yaml["host"] + "/loan/audit"
        #准备传入接口的json数据
        data = {"loan_id":str(self.get_loanId),"approved_or_not":True}
        #准备headers
        headers = {"X-Lemonban-Media-Type":"lemonban.v2","Authorization":self.get_admin_token}
        #调用request_handler请求接口
        req = request_handler.requests_handler(
            url = url,
            method = "patch",
            headers = headers,
            json = data
        )

    #充值金额到用户账户
    def recharge(self):
        url = self.yaml["host"]+"/member/recharge"
        data = {"member_id":str(self.get_userId),"amount":50000}
        headers = {"X-Lemonban-Media-Type":"lemonban.v2","Authorization":self.get_token}
        req = request_handler.requests_handler(
            url = url,
            method = "post",
            headers = headers,
            json = data
        )

接着回到测试模块,我们需要引入依赖接口。
先看一遍请求参数:
在这里插入图片描述
请求参数需要member_id及loan_id,因此我们需要把Handler的get_userId及get_loanId方法返回的值引入过来。而get_loanId返回ID对应的项目是审核中状态并不能进行投资,所以还需要调用audit方法来进行审核。另外,每个接口(除注册外)的请求头中都需要传入token,所以还需要引入get_token方法的返回值。最后,为了确保账户余额充足,我们还需要调用recharge接口对账户进行充值。以上的这些Handler返回的数据我们都可以放到前置方法中:

  • test_invest.py
from middleware.Handler import Handler
import unittest
import ddt
import json
from common.request_handler import requests_handler

#初始化Handler
env_data = Handler()
#初始化excel_handler
excel = env_data.excel
#初始化yaml_handler
yaml = env_data.yaml
#初始化logging_handler
log = env_data.logger
#获取excel中的测试用例
excel_data = excel.get_data("invest")

@ddt.ddt
class Test_invest(unittest.TestCase):
    #从Handler中获取excel中需要替换的数据:loan_id、member_id、token
    @classmethod
    def setUpClass(cls) -> None:
        cls.loan_id = env_data.get_loanId
        cls.member_id = env_data.get_userId
        cls.token = env_data.get_token
        #由于get_loanId返回ID对应的项目是审核中状态并不能进行投资,所以还需要调用audit方法来进行审核
        env_data.audit()
        #在执行测试用例前,先调用充值接口充值,以防账户余额不够
        env_data.recharge()

    #初始化Handler中的数据库对象
    def setUp(self) -> None:
        self.db = env_data.mysql_class()

    #关闭数据库对象
    def tearDown(self) -> None:
        self.db.close()

    @ddt.data(*excel_data)
    def test_invest(self,cases):
        excel_data = cases["data"]
        pass

由于请求参数的member_id及loan_id都需要动态生成,所以我们需要在excel测试用例中对这几个参数进行参数化,另外还需要进行参数化的是headers中的token:
在这里插入图片描述
既然是参数化数据,我们从excel中读出来之后是用不了的,需要用前置条件中从Handler引入的数据来替换上去:

  • test_invest.py
#省略前面的代码
    @ddt.data(*excel_data)
    def test_invest(self,cases):
        excel_data = cases["data"]
        excel_headers = cases["headers"]
        #替换excel里data的#loan_id#
        if "#loan_id#" in excel_data:
            excel_data = excel_data.replace("#loan_id#",self.loan_id)

        #替换excel里data的#member_id#
        if "#member_id#" in excel_data:
            excel_data = excel_data.replace("#member_id#",self.member_id)

        #替换excel里headers的#token#
        if "#token#" in excel_headers:
            excel_headers = excel_headers.replace("#token#",self.token)

接着就可以用request_handler来调用接口了:

  • test_invest.py
#省略前面的代码
    #调用request_handler请求接口
        actual_result = requests_handler(
            url = yaml["host"] + cases["url"],
            method = "post",
            headers = json.loads(excel_headers),
            json = json.loads(excel_data)
        )

问题2:断言

对接口预期结果及实际结果的断言是比较简单的,我们只需要用调用request_handler后返回值中的code、msg和预期结果中的code、msg两者进行比较就OK了。但对投资成功的用例来说,仅仅保证接口返回结果与预期结果均为成功,就能保证投资的业务流程一定是成功吗?不行!我们还要保证接口返回成功后,在invest表中插入了一条新纪录,也就是我们需要进行数据库表的断言。
我们先来看下invest表:
在这里插入图片描述
投资成功后,就会往这张表里新增一条记录。
我们需要同时对invest表、member表同时进行断言:
一、member表:投资前余额 - 投资金额 = 投资后的余额
二、invest表:投资前member_id的记录数 + 1 = 投资后member_id的记录数
为什么要同时对这两张表进行断言呢?我们分析下投资成功后对数据库表进行了什么操作:

  • 对于member表:投资成功后,由于是拿用户的余额进行投资,所以member表中的amount会减少。如果余额没减少,那么就算项目投资成功了,业务逻辑还是有问题的,因为拿用户余额进行投资,投资后余额并没有减少。所以需要对member表中的投资前后的余额进行断言;
  • 对于invest表:投资成功后,会往invest表中新增一条记录。如果投资成功后,invest表没有新增记录,那么代码逻辑肯定是有问题的。所以需要对invest表中是否新增新纪录进行断言。
    总结一下,对于一个有效测试用例(业务上执行成功)的接口,当业务成功后,如果对同一个表中多个字段或多个表都进行了操作,则这同一个表的多个字段或多个表都需要进行断言。

问题3:excel测试用例中参数化的替换

我们来看下test_invest中替换excel参数化数据部分的代码:

#省略前面代码
    @ddt.data(*excel_data)
    def test_invest(self,cases):
        excel_data = cases["data"]
        excel_headers = cases["headers"]
        #替换excel里data的#loan_id#
        if "#loan_id#" in excel_data:
            excel_data = excel_data.replace("#loan_id#",self.loan_id)

        #替换excel里data的#member_id#
        if "#member_id#" in excel_data:
            excel_data = excel_data.replace("#member_id#",self.member_id)

        #替换excel里headers的#token#
        if "#token#" in excel_headers:
            excel_headers = excel_headers.replace("#token#",self.token)
#省略后面代码         

光是替换参数化数据的代码就占了6行。虽然看起来并不算多,但我们可以用更简练的代码去替换。由于这三个if都是当excel中的data及headers中出现#XXX#时,就用前置方法中对应的值去替换它。这里我们可以把#号中的参数化名称与前置方法中的变量名保持一样,通过封装某种方法,每次传入excel中的data或headers的值进去,当出现#号时,自动匹配#号内的名称与前置方法中哪个变量名一致,一致的话就替换成对应变量名的值。举个例子,如传入的数据是这样:{“member_id”:#member_id#, “loan_id”:#loan_id#,“amount”:300},这里找到了#号,它里面的名称为member_id,刚好与前置方法中的member_id相吻合,就替换成前置方法中member_id变量的值。
把所有#号内的名称抽象出来,形成一种统一的格式,再去匹配具体#号内的值。这里我们就需要用到正则表达式。
这里我们先把#号的所有名称全列出来:
#member_id#、#loan_id#、#token#
我们先定义一个起始位置,可以用\w或.来匹配第一个字符,接着需要一直匹配到结束的#号前那个字符为止的所有字符,我们可以用*。但表示匹配前面字符的0次或多次,至于0次还是任意次由系统决定,这是不稳定的匹配,当系统决定只匹配0次,那就匹配不到什么了,所以号后面得加+,+表示匹配前面字符的一次或多次(即贪婪模式),至于一次还是任意次由系统决定,这就确定下来是尽可能多地匹配了。
Handler中替换参数化数据的方法代码如下:

  • Handler.py
    #用正则表达式匹配excel中的参数化数据并进行替换

    def replace_data(self,data):
        import re
        #准备正则表达式
        pattern = r"#(.*+)#"
        #当excel中所有的#号数据全部替换完毕,再无参数化数据时,则退出循环
        while re.search(pattern,data):
            #用正则表达式匹配excel中的参数化数据,并用下标1取出#号里括号内匹配到的数据
            key = re.search(pattern,data).group(1)
            #返回对象的属性值,相当于self.get_loanId.但由于属性是以字符串形式存在变量中的,如:key = "get_admin_token",
            #所以需要通过getattr方法获取以变量形式存在的属性值
            #getattr(对象,对象的属性名,如果找不到该属性替换的变量值)
            value = getattr(self,key,"")
            #用正则表达式去替换excel中的#号的值
            data = re.sub(pattern,str(value),data,1)

在test_invest.py模块中,直接调用Handler中的replace_data方法替换excel中的参数化#即可:

  • test_invest.py
        #替换excel里data中的#XXX#
        excel_data = cases["data"]
        excel_data = env_data.replace_data(excel_data)
        excel_data = eval(excel_data)
        # 替换excel里headers中的#XXX#
        excel_headers = cases["headers"]
        excel_headers = env_data.replace_data(excel_headers)
        excel_headers = json.loads(excel_headers)

问题4:审核通过的项目与投资的项目不是同一个

在代码中打印actual_result,执行测试用例时,出现了如下的报错:
在这里插入图片描述
打印的actual_result中,msg提示:该项目不在竞标中状态。这就很奇怪了,我们在投资前,不是已经审核项目成功了吗?为什么还是提示项目不在竞标状态?
我们先从业务逻辑上分析。从业务上来说,一个完整的投资流程是:登录 - 添加项目 - 审核项目 - 投资项目。而在投资项目前,我们已经进行添加项目及审核项目的操作了,且审核项目后,按理说项目的状态就是竞标中,而在对这个项目进行投资时却提示项目不在竞标状态,说明我们投资的项目有可能并不是之前审核通过的项目,因为如果是之前审核通过的项目,那项目审核通过后状态就是竞标中,就可以进行投资,也就不会报这个错。
在判断出投资的项目与审核通过的项目可能不是同一个后,我们再从代码逻辑上分析。分析下test_invest中级Handler中跟审核项目及投资项目相关的代码。
先看下test_invest.py。在前置条件中,我们先获取Handler模块中的loan_id属性:
在这里插入图片描述
再看到Handler.py中,loan_id的属性方法中调用了add_loan方法:
在这里插入图片描述
这里看上去是没问题的,我们添加一个项目,添加成功后获取这个项目的loan_id。接着回到test_invest.py中再看看。在获得添加成功后的项目id后,接下来就继续调用Handler中的audit方法对项目进行审核:
在这里插入图片描述
看到Handler.py中的audit方法,在准备调用接口传入的json时,又调用了loan_id来生成一个新的项目:
在这里插入图片描述
接着到了test_invest.py中,替换动态参数时,又会去调用一次loan_id获取属性,又生成了一个新的项目:
在这里插入图片描述

那么问题的根源就找到了,首先我们通过loan_id方法,添加项目成功后获取到项目的id,假设id为1;接着调用audit方法, 在准备调用接口时传入的json时又调用了一次loan_id,这次的loan_id就不是1了,先假设为2。接着在替换excel中的动态参数时,又去调用了一次loan_id方法,又生成了一个新的loan_id,这里记为3:在这里插入图片描述
接着在调用request_handler时,传入接口的loan_id就是id=3的项目,这个项目由于是新生成未经过审核,所以它的状态是待审核,所以调用接口后返回的msg就提示:该项目不在竞标中状态。
如果上面分析代码不容易看出来,我们也可以通过debug查看:
先在test_invest.py前置条件中的调用Handler的审核方法及替换动态参数的这两段代码打2个断点:
在这里插入图片描述
接着在Handler.py的审核方法中调用request_handler这一段代码中再打个断点:
在这里插入图片描述
进行debug。调出计算器,输入env_data.loan_id,得到的loan_id数值为16672:
在这里插入图片描述

接着我们按“step over”按钮,进入Handler.py中的audit方法内部中,一直step over,直到print语句,可以看到loan_id变成了16676:
在这里插入图片描述
然后再按resume program,进入到下一个断点,也就是替换动态参数的那一段:
在这里插入图片描述
在step over调用replace_data方法后,又要去获取一次loan_id的属性:
在这里插入图片描述
此时又生成了一个新的loan_id:16684:
在这里插入图片描述
从debug的结果也可以看出,调用Handler的loan_id方法获取loan_id时,id值为16672;接着在调用Handler的审核方法时,又调用loan_id方法又生成了一个新的loan_id:16676;然后在替换excel的动态参数时,又去调用了一次loan_id方法,新生成的loan_id为:16684。
那么这个问题怎么解决呢?其实问题的根源就是这一行代码:
在这里插入图片描述
我们在获取loan_id、审核项目、替换excel中的动态参数#loan_id#时都需要调用这个方法,从而每次都生成一个新的loan_id。那么我们可以把它去掉,设置一个类属性:loan_id,接着在test_invest.py中,也把调用Handler的loan_id方法去掉,转而直接调用Handler的add_loan方法。然后我们可以调用setattr函数,给Handler对象的loan_id属性赋值,赋的值就是add_loan方法中返回的结果,此时假设loan_id的值为1。在后面调用Handler.py的audit、replace_data中,获取的loan_id的值也是1。这样就保证了在添加项目、审核项目、替换excel中的动态参数#loan_id#时使用到的都是同一个loan_id。

跟loan_id相关的变更代码如下:

  • test_invest.py
class Test_invest(unittest.TestCase):
    #从Handler中获取excel中需要替换的数据:loan_id、member_id、token
    @classmethod
    def setUpClass(cls) -> None:
        #cls.loan_id = env_data.loan_id
        cls.member_id = env_data.member_id
        cls.token = env_data.token
        #由于get_loanId返回ID对应的项目是审核中状态并不能进行投资,所以还需要调用audit方法来进行审核
        env_data.audit()
        #在执行测试用例前,先调用充值接口充值,以防账户余额不够
        env_data.recharge()

    #初始化Handler中的数据库对象
    def setUp(self) -> None:
        self.db = env_data.mysql_class()

    #关闭数据库对象
    def tearDown(self) -> None:
        self.db.close()

    @ddt.data(*excel_data)
    def test_invest(self,cases):
        #替换excel里data中的#XXX#
        excel_data = cases["data"]
        excel_data = env_data.replace_data(excel_data)
  • Handler.py

class mysqlMiddleHandler(mysql_handler):
class Handler():
    #初始化loan_id属性
    loan_id = None

    # #通过get_loanId方法获取add_loan方法中的返回值
    # @property
    # def loan_id(self):
    #     loan = self.add_loan()
    #     return loan


    def add_loan(self):
        #获取返回结果中的id的值
        loan_id = jsonpath(req,"$..id")[0]
        return loan_id

    #审核项目
    def audit(self):
        #准备传入接口的json数据
        data = {"loan_id":str(self.loan_id),"approved_or_not":True}
        req = request_handler.requests_handler(
            json = data
        )


    #用正则表达式匹配excel中的参数化数据并进行替换
    def replace_data(self,data):
            value = getattr(self,key,"")

报错时的分析

在执行代码报错时,应从以下两个角度分析错误出在哪:

  • 业务逻辑。分析接口的业务流程,即依赖接口及当前接口的执行顺序是怎样的,代码有没有按照业务逻辑来写。一般业务逻辑出错并不会报错,但根据接口返回的错误msg进行业务逻辑分析可能会找出原因出在哪,比如说投资接口,根据返回的msg:该项目不在竞标中状态可推断出投资的项目与审核通过的项目可能不是同一个。
  • 代码逻辑。先根据Traceback或相关线索定位到出错的某一行代码,如果该行代码是调用了别的函数,则查看该函数的代码逻辑;如果不是调用函数的代码,则根据上下文进行分析。如果实在找不出原因出在哪,可打断点进行调试。
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值