接口介绍

对接话费充值java_json

excel测试用例

对接话费充值java_用例_02

代码实操

先跑起来!

首先先通过Handler准备好三大组件的初始化(yaml_handler、excel_handler、logging_handler),接着通过excel_handler获取recharge工作表的所有数据:

  • test_recharge.py
import ddt
import unittest
from middleware.Handler import Handler

#初始化yaml_handler
yaml = Handler.yaml
#初始化excel_handler
excel = Handler.excel
#初始化logging_handler
logger = Handler.logger
#准备好excel测试数据
excel_data = excel.get_data("recharge")

#在需要使用到数据的类上定义ddt装饰器
@ddt.ddt
class Test_recharge(unittest.TestCase):
    # #在类中需要使用数据的方法上准备数据
    @ddt.data(*excel_data)
    def test_recharge(self,cases):
        print(cases)

if __name__ == '__main__':
    unittest.main()

执行结果如下:

对接话费充值java_数据_03

问题1:接口依赖

业务流程上,进行充值前要先进行登录,一是为了获得登录接口返回结果的token放入headers,二是充值接口本身的请求参数中有一个member_id,它也是登录成功后接口返回的数据。因此在进行实际测试之前,需要先调用登录接口获取token及user_id。
这里我们可以在excel测试用例中对token和member_id进行参数化。那么能直接写死吗?member_id可以写死,因为只需登录一次即可获取member_id,在测试用例从开始执行到结束这个member_id都不会变;而token是无法写死的,因为token是有时限的,你如果写死了,下次再拿出来用就已经失效了,所以我们得需要一个新鲜的token。而我们在登录所获取的token_id足够我们使用到执行完所有14条测试用例,所以所有的测试用例均用同一个token_id即可。
接下来再想想,调用充值接口前调用登录接口仅仅只是充值接口的特权吗?其他接口如:提现、投资等就不需要登录吗?当然需要!所以我们需要单独把它拿出来封装成方法,然后供其他测试类调用。
那么需要在哪封装登录方法呢?common吗?自然是不行的,因为common下的都是跟业务不相关的模块,而登录已经涉及到了登录业务,自然不能放到common下,因此应该放到middleware下。

  • Handler.py
#省略之前的代码...
    #由于多个接口都需要依赖登录接口来登录,所以这里准备登录的接口请求
    def login(self):
        #通过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 = self.yaml["user"]
        )
        #获取返回结果中的token。由于token_info中的token_type和token的内容是分开的,所以需要拼接起来。另外需要注意取值时的层级关系
        token_type = req["data"]["token_info"]["token_type"]
        token_content = req["data"]["token_info"]["token"]
        token = " ".join([token_type, token_content])
        #获取返回结果中的用户id,因为充值接口需要用到member_id
        user_id = req["data"]["id"]
        #返回token及user_id
        return {"token":token,"user_id":user_id}

handlers中封装完登录接口后,就可以直接在测试类中调用该方法获取token和member_id了。这里我们可以考虑通过setUpClass来获取这两个信息,一是本来调用登录接口获取这两个参数本来就是测试充值接口前的前置动作(先登录后充值);二是我们只需登录一次,获取token放入headers,获取member_id作为请求参数传给充值接口,不需要每一条excel用例都执行一遍,而setUpClass在运行所有的excel测试用例过程中只执行一次,所以应该用setUpClass。
在获取了token和member_id后,在测试方法中我们需要在调用接口前替换excel测试用例中的#member_id#和#token#

  • test_recharge.py
#在需要使用到数据的类上定义ddt装饰器
@ddt.ddt
class Test_recharge(unittest.TestCase):
    #前置方法,调用登录接口获得user_id和token
    @classmethod
    def setUpClass(cls) -> None:
        # 调用Handler的login方法,获取返回值中的user_id及token
        login_info = Handler.login()
        cls.member_id = login_info["user_id"]
        cls.token = login_info["token"]

    #在类中需要使用数据的方法上准备数据
    @ddt.data(*excel_data)
    def test_recharge(self,cases):
        #用变量接收excel里data字段的数据,后面请求接口传入请求参数时更方便
        data = cases["data"]
        #替换data中含有#member_id#的地方,替换成前置方法中的member_id
        if "#member_id#" in data:
            data = data.replace("#member_id#",self.member_id)
        #用变量接收excel里headers字段的数据
        headers = cases["headers"]
        #替换headers中含有#token#的地方,替换成前置方法中的token
        if "#token#" in cases["headers"]:
            headers = headers.replace("#token#",self.token)

执行时报了个错:

对接话费充值java_对接话费充值java_04


这说的是setUpClass返回的member_id是一个list类型而不是str类型,所以我们需要用str()方法把list类型转换成str类型:

#替换data中含有#member_id#的地方,替换成前置方法中的member_id
        if "#member_id#" in data:
            data = data.replace("#member_id#",str(self.member_id))
        #用变量接收excel里headers字段的数据
        headers = cases["headers"]
        #替换headers中含有#token#的地方,替换成前置方法中的token
        if "#token#" in cases["headers"]:
            headers = headers.replace("#token#",str(self.token))

问题2:多层级json的值的提取

在Handler的登录方法中,我们获取接口返回的token和user_id时是这样的:

#获取返回结果中的token。由于token_info中的token_type和token的内容是分开的,所以需要拼接起来。另外需要注意取值时的层级关系
        token_type = req["data"]["token_info"]["token_type"]
        token_content = req["data"]["token_info"]["token"]
        token = token_type + " " + token_content
        #获取返回结果中的用户id,因为充值接口需要用到member_id
        user_id = req["data"]["id"]

由于token_type和token在第三层比较深,所以我们需要连续获取三次显得有点繁琐,这里我们可以通过引入jsonpath模块来简化多层json中获取某一层值的步骤。
接下来讲解一下jsonpath的简单用法:

data = {
    "code": 0,
    "msg": "OK",
    "data": {
        "id": 1,
        "code": 1,
        "leave_amount": 7029504.48,
        "mobile_phone": "13241562152",
        "reg_name": "SS01",
        "reg_time": "2020-10-11 16:38:11.0",
        "type": 1,
        "token_info": {
            "token_type": "Bearer",
            "expires_in": "2020-10-14 20:58:52",
            "token": "ewefwefwefwfgwg.e3f3fC1S4s2c77-Tc8Q"
        }
    },
    "copyright": "Copyright 人生有限责任公司"
}

如上面所示,data是一个3层深度的json。我们要获取第一层中的code,可以这样写,其中一个.表示子层级,即第一层级。

code = jsonpath(data,"$.code")
print(code)

打印如下:

对接话费充值java_对接话费充值java_05


打印出来是一个列表,而我们想获取里面的值,可以再通过索引来获取:

code = jsonpath(data,"$.code")[0]
print(code)

打印结果如下:

对接话费充值java_用例_06


接下来如果我们想获取第三层级的token,可以这样写,其中两个.表示子孙层级,即包括第一层级及后面的所有层级

token = jsonpath(data,"$..token")[0]
print(token)

打印如下:

对接话费充值java_用例_07


因此,如果我们想获取某一层级的参数值又懒得去数在哪个层级,直接打两个点就行了。

因此Handler中登录方法的token及user_id的获取我们可以写成下面这样:

#获取返回结果中的token。由于token_info中的token_type和token的内容是分开的,所以需要拼接起来。另外需要注意取值时的层级关系
        token_type = jsonpath(req,"$..token_type")
        token_content = jsonpath(req,"$..token")
        token = token_type + " " + token_content
        #获取返回结果中的用户id,因为充值接口需要用到member_id
        user_id = jsonpath(req,"$..id")

问题3:Handler优化

在Handler定义了登录方法后,我们在test_recharge的测试模块中获取token和member_id时是这样的:

#前置方法,调用登录接口获得user_id和token
    def setUpClass(cls) -> None:
        # 调用Handler的login方法,获取返回值中的user_id及token
        login_info = Handler.login()
        cls.member_id = login_info["user_id"]
        cls.token = login_info["token"]

这里我们可以再对Handler简化一下,在Handler方法中分别定义get_token和get_userId方法获取到token和user_id的值,然后直接通过Handler对象调用这两个方法获取到这两个值:

  • Handler.py
class Handler():
"""省略前面的代码"""
    #通过get_token方法获取login方法中返回结果中的token的值
    def get_token(self):
        user = self.login()
        return user["token"]
    # 通过get_userId方法获取login方法中返回结果中的user_id的值
    def get_userId(self):
        user = self.login()
        return user["user_id"]

接着在test_recharge测试模块中调用这两个方法获取token和member_id的值:

  • test_recharge
#初始化Handler对象
handler = Handler()
#在需要使用到数据的类上定义ddt装饰器
@ddt.ddt
class Test_recharge(unittest.TestCase):
    @classmethod
    #前置方法,调用登录接口获得user_id和token
    def setUpClass(cls) -> None:
        # 调用Handler的login方法,获取返回值中的user_id及token
        cls.member_id = handler.get_userId()
        cls.token = handler.get_token()

接下来还可以再优化一下,我们可以把方法变成一个类属性,直接通过对象.属性就可获取token和user_id的值而不需要通过对象.方法来获取。

  • Handler.py
#通过get_token方法获取login方法中返回结果中的token的值
    @property
    def get_token(self):
        user = self.login()
        return user["token"]
    # 通过get_userId方法获取login方法中返回结果中的user_id的值
    @property
    def get_userId(self):
        user = self.login()
        return user["user_id"]

在test_recharge模块中我们直接通过对象.属性来获取值:

  • test_recharge.py
class Test_recharge(unittest.TestCase):
    @classmethod
    #前置方法,调用登录接口获得user_id和token
    def setUpClass(cls) -> None:
        # 调用Handler的login方法,获取返回值中的user_id及token
        cls.member_id = handler.get_userId
        cls.token = handler.get_token

而如果我们像之前那样通过对象.方法来获取值的话就会报错:

对接话费充值java_json_08


意思是Handler的get_userId返回的值是list类型的,而list类型是没有方法可调用的。

问题4:断言失败的排查

在解决了接口依赖及Handler优化的问题后,接下来我们就可以通过request_handler来请求接口获取实际结果,然后再拿实际结果来和预期结果进行断言

  • test_recharge.py
@ddt.data(*excel_data)
    def test_recharge(self,cases):
        #用变量接收excel里data字段的数据,后面请求接口传入请求参数时更方便
        data = cases["data"]
        #替换data中含有#member_id#的地方,替换成前置方法中的member_id
        if "#member_id#" in data:
            data = data.replace("#member_id#",str(self.member_id))
        #用变量接收excel里headers字段的数据
        headers = cases["headers"]
        #替换headers中含有#token#的地方,替换成前置方法中的token
        if "#token#" in cases["headers"]:
            headers = headers.replace("#token#",str(self.token))
        #通过request_handler来请求接口获取实际结果
        actual_result = requests_handler(
            method = "post",
            url = yaml["host"] + cases["url"],
            json = json.loads(data),
            headers = json.loads(headers)
        )
        #用变量获取excel中的预期结果
        expected_result = json.loads(cases["expected"])
        #实际结果与预期结果进行断言
        self.assertEqual(expected_result["code"],actual_result["code"])
        self.assertEqual(expected_result["msg"],actual_result["msg"])

在执行时报了个错,断言失败——2!= 0:

对接话费充值java_json_09


但我们通过postman访问是返回0的:

对接话费充值java_对接话费充值java_10

问题5:【业务】充值成功断言

在断言接口实际返回结果与预期结果一致时,就能证明充值成功了吗?并不能,我们还得确保数据库表中的数据是正确的。在调用充值接口返回充值成功后,按理说member表中的leave_amount数字是有增加的,而增加的金额恰好就是请求参数的amount对应的值。这样一来我们可以通过下面的表达式来断言member表数据是否正确:
充值前的金额 + 充值金额(接口请求参数中的amount) = 充值后的金额(member表中的leave_amount)
这样一来就有个问题:如何获取充值前的金额?
我们可以在调用接口前查一次数据库,获取member表中的leave_amount。
这样我们就可以写出一条简单的sql查询语句:
select leave_amount from futureloan.member where ?
这里的where条件要怎么写呢?我们可以根据config.yml中的mobile_phone来查询,也可以通过请求参数中的member_id来查询。
另外又有一个问题:是不是所有的测试用例都得查询?
其实只需要对充值成功的用例进行数据库查询就行了,因为其他异常用例都因为接口的各种返回失败的情况而没有碰到数据库表那一层。那我们怎么判断哪条用例是成功的用例呢?我们可以在用例中设置一个标识字段(如sql_execute),当这个字段等于1时,则这条用例就查询数据库;否则不查询数据库。另外我们也可以把要执行的sql直接写到这个字段上,然后判断这个字段中有值,就执行对应的sql;这个字段值为空,则不执行sql。
另外,sql连接对象的初始化我们可以放到前置方法中。那是放到setUpClass还是放到setUp中呢?那么我们就要弄清楚连接对象我们是需要每一条用例都用不同的连接对象还是所有的用例都用同一个连接对象?很显然每一个用例都需要初始化一个连接对象,因为如果我们第一个测试用例执行完毕后,第二个测试用例还用上一个用例的sql连接对象,那么游标对象也还是跟上一个用例的一样,而上一个用例的游标执行完毕后它是标记在结果那一行的,并没有重置,所以再用这个游标对象是得不到想要的值的。因此我们需要每个用例都初始化一遍sql连接对象,所以应该放到setUp方法中。

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

#初始化Handler对象
handler = Handler()
#初始化yaml_handler
yaml = handler.yaml
#初始化excel_handler
excel = handler.excel
#初始化logging_handler
logger = handler.logger
#准备好excel测试数据
excel_data = excel.get_data("recharge")

#在需要使用到数据的类上定义ddt装饰器
@ddt.ddt
class Test_recharge(unittest.TestCase):
    @classmethod
    #前置方法,调用登录接口获得user_id和token
    def setUpClass(cls) -> None:
        # 调用Handler的login方法,获取返回值中的user_id及token
        cls.member_id = handler.get_userId
        cls.token = handler.get_token

    #前置方法,初始化sql连接对象
    def setUp(self) -> None:
        self.db = handler.mysql_class()

    #在类中需要使用数据的方法上准备数据
    @ddt.data(*excel_data)
    def test_recharge(self,cases):
        #用变量接收excel里data字段的数据,后面请求接口传入请求参数时更方便
        data = cases["data"]
        #替换data中含有#member_id#的地方,替换成前置方法中的member_id
        if "#member_id#" in data:
            data = data.replace("#member_id#",str(self.member_id))
        #用变量接收excel里headers字段的数据
        headers = cases["headers"]
        #替换headers中含有#token#的地方,替换成前置方法中的token
        if "#token#" in cases["headers"]:
            headers = headers.replace("#token#",str(self.token))
        #执行查询语句,查询调用充值接口前member表的余额为多少
        execute_01 = self.db.query("SELECT * FROM futureloan.member WHERE id={}".format(self.member_id))
        before_amount = execute_01["leave_amount"]
        #通过request_handler来请求接口获取实际结果
        actual_result = requests_handler(
            method = "post",
            url = yaml["host"] + cases["url"],
            json = json.loads(data),
            headers = json.loads(headers)
        )
        #用变量获取excel中的预期结果
        expected_result = json.loads(cases["expected"])
        #实际结果与预期结果进行断言
        self.assertEqual(expected_result["code"],actual_result["code"])
        self.assertEqual(expected_result["msg"],actual_result["msg"])
        #如果返回结果为成功
        if actual_result["code"] == 0:
            #断言member表中的leave_amount数据是否正确
            execute_02 = self.db.query("SELECT * FROM futureloan.member WHERE id={}".format(self.member_id))
            after_amount = execute_02["leave_amount"]
            self.assertTrue(before_amount + data["amount"] == after_amount)

if __name__ == '__main__':
    unittest.main()

在执行时又报错了,这里又报了个错:

对接话费充值java_数据_11


看得出来这是sql语句的问题,异常定位在执行sql的这一句:

execute_01 = self.db.query("SELECT * FROM futureloan.member WHERE id={}".format(self.member_id))

首先先从语法上来看,这句话没有语法问题,因为直接放到工具上是可以执行的:

对接话费充值java_对接话费充值java_12


接着我们可以尝试把id直接写死,像这样:

execute_01 = self.db.query("SELECT * FROM futureloan.member WHERE id=1")

再运行一次,没报sql语句异常的错误了,所以可以断定问题出在传过来的member_id值有问题,极有可能是传过来的类型与数据库表中的类型对不上。
接着我们定位到获取member_id的前置方法上,并没有发现什么问题:

def setUpClass(cls) -> None:
        # 调用Handler的login方法,获取返回值中的user_id及token
        cls.member_id = handler.get_userId

接着我们再继续定位到Handler中从登录接口返回结果中获取user_id的那一段代码:

  • Handler.py
#获取返回结果中的用户id,因为充值接口需要用到member_id
        user_id = jsonpath(req,"$..id")

在这里我们发现了问题的所在,原来是user_id没有通过索引获取值,因而传到test_recharge中的值的类型为list,从而导致与member表中id的类型int对不上。

加上索引[0]之后,运行时没再报sql异常了,但又报了一个类型不匹配的异常:

对接话费充值java_用例_13


异常定位到下面这段代码上:

self.assertTrue(before_amount + data["amount"] == after_amount)

而异常的意思是字符串的下标必须为数字,而data[“amount”]这里我们的下标为字符串。这里我们通过具体的字段作为索引是没问题的,问题就出在data为什么是字符串类型,我们想要的data是一个字典类型。
我们接着定位到data的定义:

data = cases["data"]

可以看到data没有进行json.loads的转换,因此直接从excel中读取过来的数据就是字符串类型了。所以这里我们需要通过json.loads转换一下:

#省略前面的代码
json_data = json.loads(data)
#省略前面的代码
self.assertTrue(before_amount + json_data["amount"] == after_amount)

问题6:sql数据更新(提交事务)

在调试过程中我们发现,充值前金额在加上充值金额前就已经等于充值后金额了,这是不合理的,那么问题出在哪呢?

对接话费充值java_用例_14


这是因为sql的事务没更新。

在初始化一个sql连接对象之后,它就会生成一个副本一样的东西,你执行的sql只是相当于在这副本上进行操作,而真正的member表是没有任何变化的。就跟git的本地仓库和远程仓库一样,你在本地仓库做了操作,远程仓库并没有任何变化,只有当你本地仓库提交时才会把数据真正地更新到远程仓库中。类比过来,初始化一个sql连接对象相当于建立一个本地仓库,在上面执行的sql增删改查操作并不会对真正的member表产生任何变化,只有当提交事务之后,本地仓库的变更才会真正同步到member表中。

因此sql_handler中得进行事务的提交:

  • sql_handler.py
#省略前面的代码
    def query(self,sql,is_one=True):
        #把最新的数据进行更新(提交事务)
        self.conn.commit()
        #调用游标对象的execute方法执行查询的sql
        self.cursor.execute(sql)
#省略后面的代码

问题7:精度控制

在第二条用例运行时报了个错:

对接话费充值java_对接话费充值java_15


异常定位到的位置是下面这段代码:

self.assertTrue(before_amount + json_data["amount"] == after_amount)

异常的意思是decimal(双精度)与float(单精度)不能相加。
关于单精度和双精度的区别见下面的引用部分:

我们提到圆周率 π 的时候,它有很多种表达方式,既可以用数学常数3.14159表示,也可以用一长串1和0的二进制长串表示。
圆周率 π 是个无理数,既小数位无限且不循环。因此,在使用圆周率进行计算时,人和计算机都必须根据精度需要将小数点后的数字四舍五入。
在小学的时候,小学生们可能只会用手算的方式计算数学题目,圆周率的数值也只能计算到小数点后两位——3.14;而高中生使用图形计算器可能会使圆周率数值排到小数点后10位,更加精确地表示圆周率。在计算机科学中,这被称为精度,它通常以二进制数字来衡量,而非小数。
对于复杂的科学模拟,开发人员长期以来一直都依靠高精度数学来研究诸如宇宙大爆炸,或是预测数百万个原子之间的相互作用。
数字位数越高,或是小数点后位数越多,意味着科学家可以在更大范围内的数值内体现两个数值的变化。借此,科学家可以对最大的星系,或是最小的粒子进行精确计算。
但是,计算精度越高,意味着所需的计算资源、数据传输和内存存储就越多。其成本也会更大,同时也会消耗更多的功率。
由于并非每个工作负载都需要高精度,因此 AI 和 HPC 研究人员可以通过混合或匹配不同级别的精度的方式进行运算,从而使效益最大化。NVIDIA Tensor Core GPU 支持多精度和混合精度技术,能够让开发者优化计算资源并加快 AI 应用程序及其推理功能的训练。
单精度、双精度和半精度浮点格式之间的区别

对接话费充值java_用例_16


IEEE 浮点算术标准是用来衡量计算机上以二进制所表示数字精度的通用约定。在双精度格式中,每个数字占用64位,单精度格式占用32位,而半精度仅16位。


要了解其中工作原理,我们可以拿圆周率举例。在传统科学记数法中,圆周率表示为3.14 x100。但是计算机将这些信息以二进制形式存储为浮点,即一系列的1和0,它们代表一个数字及其对应的指数,在这种情况下圆周率则表示为1.1001001 x 21。


在单精度32位格式中,1位用于指示数字为正数还是负数。指数保留了8位,这是因为它为二进制,将2进到高位。其余23位用于表示组成该数字的数字,称为有效数字。


而在双精度下,指数保留11位,有效位数为52位,从而极大地扩展了它可以表示的数字范围和大小。半精度则是表示范围更小,其指数只有5位,有效位数只有10位。


圆周率在每个精度级别表现如下:IEEE 浮点算术标准是用来衡量计算机上以二进制所表示数字精度的通用约定。在双精度格式中,每个数字占用64位,单精度格式占用32位,而半精度仅16位。


要了解其中工作原理,我们可以拿圆周率举例。在传统科学记数法中,圆周率表示为3.14 x100。但是计算机将这些信息以二进制形式存储为浮点,即一系列的1和0,它们代表一个数字及其对应的指数,在这种情况下圆周率则表示为1.1001001 x 21。


在单精度32位格式中,1位用于指示数字为正数还是负数。指数保留了8位,这是因为它为二进制,将2进到高位。其余23位用于表示组成该数字的数字,称为有效数字。


而在双精度下,指数保留11位,有效位数为52位,从而极大地扩展了它可以表示的数字范围和大小。半精度则是表示范围更小,其指数只有5位,有效位数只有10位。


圆周率在每个精度级别表现如下:IEEE 浮点算术标准是用来衡量计算机上以二进制所表示数字精度的通用约定。在双精度格式中,每个数字占用64位,单精度格式占用32位,而半精度仅16位。


要了解其中工作原理,我们可以拿圆周率举例。在传统科学记数法中,圆周率表示为3.14 x100。但是计算机将这些信息以二进制形式存储为浮点,即一系列的1和0,它们代表一个数字及其对应的指数,在这种情况下圆周率则表示为1.1001001 x 21。


在单精度32位格式中,1位用于指示数字为正数还是负数。指数保留了8位,这是因为它为二进制,将2进到高位。其余23位用于表示组成该数字的数字,称为有效数字。


而在双精度下,指数保留11位,有效位数为52位,从而极大地扩展了它可以表示的数字范围和大小。半精度则是表示范围更小,其指数只有5位,有效位数只有10位。


圆周率在每个精度级别表现如下:

对接话费充值java_数据_17


下面来看关于decimal的几个例子:

from decimal import Decimal

#双精度+整数
a = Decimal(0.3)
b = 2
print("双精度与整数的加法:0.3(双精度)+2(整数)={}".format(a+b))
#双精度+双精度
c = Decimal(0.2)
print("双精度与双精度的加法:0.3(双精度)+0.2(双精度)={}".format(a+c))
d = 0.2
#print("双精度与单精度的加法:3.1(双精度)+2.1(单精度)={}".format(a+d))

执行结果如下:

对接话费充值java_数据_18


执行第三个例子时,执行时报错:

对接话费充值java_json_19


因此双精度与单精度是不能进行相加的,所以json_data[“amount”]应转换成双精度:

self.assertTrue(before_amount + Decimal(str(json_data["amount"])) == after_amount)