接口介绍
excel测试用例
代码实操
先跑起来!
import unittest
import ddt
from middleware.handler import Handler
# 初始化
logger = Handler.logger
test_data = Handler.excel.read_data("add")
@ddt.ddt
class LoginTestCase(unittest.TestCase):
@ddt.data(*test_data)
def test_add(self, test_info):
data = test_info["data"]
print(data)
问题1:接口依赖
首先看下接口的业务描述,可以看出这里需要管理员的账号进行登录后才能进行审核操作,所以依赖了一个登录接口:
另外,请求参数中有一个loan_id,表明至少得有一个项目才能进行审核,事先得准备一个项目,所以又依赖了一个添加项目的接口:
而除了管理员登录后获取管理员的id及token之外,添加项目也需要普通用户进行登录才能进行添加,所以需要两个角色都进行登录。
两个角色的用户登录操作可以放到setUpClass中,因为只需要登录一次,就能一直进行添加项目及审核项目。那么添加项目的操作是放到setUp中呢还是setUpClass中呢?答案是setUp,也就是说每一条用例都需要重新生成一个新的项目。这是为什么呢?
我们可以看回excel中的数据。第一条用例生成的项目审核通过后,loan表中的status变为2(竞标中),而第二条用例不新生成一个,继续拿第一个竞标中状态的项目审核是审核不了的;同样第三个用例也是。所以这里需要放到setUp中,以确保每一个测试用例都能够生成一个新的项目进行审核。
问题2:登录、添加项目的前置处理
登录时需要普通用户、管理员用户进行登录。那么这两个角色登录后,我们需要获取什么呢?
- 管理员的token(headers中需要)
- 普通用户的token(headers中需要)
- 普通用户的id(添加项目时的输入参数:member_id)
所以我们可以把管理员的登录及普通用户的登录放到handler中并且获取这三个值,然后在setUpClass中调用handler来获取这三个值。
那么handler中需要分别写普通用户的登录方法及管理员的登录方法吗?不需要。我们只需要一个登录方法就够了,然后这个登录方法最后返回token及id,然后再写几个方法来获取这两个值。
那么如何分辨获取的token是普通用户的token还是管理员的token呢?我们可以分别把管理员的账号及普通用户的账号放到yaml文件中,然后登录时传入这个账号,接着获取token的方法中统一调用登录方法,只不过传入的参数不同。
对于添加项目来说,我们也可以直接写到handler中去。
代码如下: - Handler.py
class Handler():
#通过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
接着在测试类中,直接在前置条件中调用handler中的getXXX函数来获取相应的属性即可:
- test_audit.py
#通过前置类方法获取Handler中的loan_id及admin_token
@classmethod
def setUpClass(cls) -> None:
#cls.loan_id = env_data.get_loanId
cls.admin_token = env_data.get_admin_token
#通过前置实例方法初始化mysql的连接对象,使得每个用例用到的连接对象都是相互独立的
def setUp(self) -> None:
self.loan_id = env_data.get_loanId
self.db = env_data.mysql_class()
问题3:特殊用例——审核的项目不存在
看下excel中的最后一条用例,这里需要审核一个不存在的项目,我们直接拿一个新生成的项目id加上10000就成了一个不存在的项目。那么在python读取这条excel数据时,是用json.loads()呢还是eval()呢?答案是eval,因为这里需要执行字符串里面的运算,而json.loads()只会把字符串转换成字典,loan_id的值还是#loan_id#+10000。
这里总结一下:当excel中的数据全是json格式的数据,则可以使用json.loads()转换成字典;而如果excel中的数据需要进行运算,或者可以完全转换成字典,则可以使用eval。
问题4:特殊用例——审核不在待审核状态
当我们生成一个新的项目时,那么它的状态就是待审核状态。那么现在要找一个不在待审核状态的load_id,有什么办法吗?
方法一:在成功用例的断言中存储一条记录
一个项目审核通过后,状态就变为2(竞标中),符合不在待审核状态的条件。而在进行成功用例的数据库断言中:
#数据库表断言
if actual_result["code"] == 0:
#根据上面的loan_id查询loan表中对应记录的status的值
loan_in_sql = self.db.query("SELECT * FROM futureloan.loan WHERE id={}".format(self.loan_id))
#拿loan表中status的值与预期结果的status的值进行断言
self.assertTrue(loan_in_sql["status"] == expect_result["status"])
如果断言能够通过,则证明这个loan_id是审核通过的,也就可以利用了。这时我们可以通过一个变量来保存这个loan_id,然后在执行到审核不在待审核状态项目的测试用例中再传入即可:
if "#pass_loan_id#" in data:
data = data.replace("#pass_loan_id#", str(env_data.pass_loan_id)
# 省略中间代码
if resp["code"] == 0:
# loan_id = 8, status = 2
loan = self.db.query("SELECT * FROM futureloan.loan WHERE id={};".format(self.loan_id))
self.assertEqual(loan["status"], expected["status"])
#用一个临时变量存储loan_id
env_data.pass_loan_id = loan["id"]
方法二:在loan表中捞一条status!=1的数据
这个很容易理解,就不展开说了,直接上代码:
if "#pass_loan_id#" in data:
pass_loan = self.db.query("SELECT * FROM futureloan.loan WHERE status !=1;")
data = data.replace("#pass_loan_id#", str(pass_loan["id"]))
#省略后面的代码
问题5:审核是否成功的断言
跟以往测试用例不同的是,这里我们虽然可以通过返回码来判断接口是否执行成功,但无法通过接口的返回结果来判断业务上用例是否通过,因为没有哪一个字段来表明审核之后的结果:
所以我们只有通过查询loan表中的status字段来判断是否审核成功:
由表中可以看出,当新增了一个项目时,项目状态为审核中;当approved_or_not设为true时,则项目状态为竞标中;当approved_or_not设为false时,则项目状态为审核不通过。所以当status=2时,才证明审核通过。
代码调试
from middleware.Handler import Handler
from common import request_handler
import ddt
import json
import unittest
env_data = Handler()
#初始化Handler中的yaml_handler
yaml = Handler.yaml
#初始化Handler中的excel_handler
excel = Handler.excel
#厨师化Handler中的logging_handler
log = Handler.logger
#通过excel_handler获取excel中audit表单中的所有数据
excel_data = excel.get_data("audit")
#在需要用到数据驱动的类上定义ddt装饰器
@ddt.ddt
class Test_audit(unittest.TestCase):
#通过前置类方法获取Handler中的loan_id及admin_token
@classmethod
def setUpClass(cls) -> None:
cls.loan_id = env_data.get_loanId
cls.admin_token = env_data.get_admin_token
#通过前置实例方法初始化mysql的连接对象,使得每个用例用到的连接对象都是相互独立的
def setUp(self) -> None:
self.db = env_data.mysql_class()
#在需要用到数据驱动的类方法上定义data装饰器
@ddt.data(*excel_data)
def test_audit(self,cases):
#准备excel中的data并用excel_data变量接收
excel_data = cases["data"]
# 准备excel中的headers并用excel_headers变量接收
excel_headers = cases["headers"]
#替换data中的#loan_id#
if "#loan_id#" in excel_data:
excel_data = excel_data.replace("#loan_id#",self.loan_id)
#替换data中的#pass_loan_id#
if "#pass_loan_id#" in excel_data:
#查询loan表中状态为还款中的记录并取其中一条记录的id替换到#pass_loan_id#
pass_loan_id = self.db.query("SELECT * FROM futureloan.loan WHERE status=3")["id"]
excel_data = excel_data.replace("#pass_loan_id#",pass_loan_id)
# 替换headers中的#admin_token#
if "#admin_token#" in excel_headers:
excel_headers = excel_headers.replace("#admin_token#",self.admin_token)
#将excel中字符串格式的data转换成字典格式
data = eval(excel_data)
#调用request_handler请求接口
actual_result = request_handler.requests_handler(
method = "post",
url = yaml["host"] + cases[url],
json = data,
headers = json.loads(excel_headers)
)
#准备预期结果
expect_result = json.loads(cases["expected"])
#将预期结果与实际结果进行断言
self.assertEqual(expect_result["code"],actual_result["code"])
self.assertEqual(expect_result["msg"],actual_result["msg"])
#数据库表断言
if actual_result["code"] == 0:
#根据上面的loan_id查询loan表中对应记录的status的值
loan_in_sql = self.db.query("SELECT * FROM futureloan.loan WHERE id=".format(self.loan_id))
#拿loan表中status的值与预期结果的status的值进行断言
self.assertTrue(loan_in_sql["status"] == expect_result["status"])
BUG1:TypeError
出错代码如下:
#替换data中的#loan_id#
if "#loan_id#" in excel_data:
excel_data = excel_data.replace("#loan_id#",self.loan_id)
类型转化错误,self.loan_id获得的loan_id为int类型,而替换到excel中的单元格时需要存入字符串类型,所以得通过str()方法把int类型转换成字符串类型。
BUG2:AssertionError
1005 != 0
Expected :0
Actual :1005
<Click to see difference>
Traceback (most recent call last):
File "C:\Program Files\JetBrains\PyCharm Community Edition 2020.1\plugins\python-ce\helpers\pycharm\teamcity\diff_tools.py", line 32, in _patched_equals
old(self, first, second, msg)
File "C:\Users\rainstar\AppData\Local\Programs\Python\Python38\lib\unittest\case.py", line 912, in assertEqual
assertion_func(first, second, msg=msg)
File "C:\Users\rainstar\AppData\Local\Programs\Python\Python38\lib\unittest\case.py", line 905, in _baseAssertEqual
raise self.failureException(msg)
AssertionError: 0 != 1005
During handling of the above exception, another exception occurred:
Traceback (most recent call last):
File "C:\Users\rainstar\AppData\Local\Programs\Python\Python38\lib\unittest\case.py", line 60, in testPartExecutor
yield
File "C:\Users\rainstar\AppData\Local\Programs\Python\Python38\lib\unittest\case.py", line 676, in run
self._callTestMethod(testMethod)
File "C:\Users\rainstar\AppData\Local\Programs\Python\Python38\lib\unittest\case.py", line 633, in _callTestMethod
method()
File "C:\Users\rainstar\PycharmProjects\pyproject\venv\lib\site-packages\ddt.py", line 182, in wrapper
return func(self, *args, **kwargs)
File "C:\Users\rainstar\PycharmProjects\pyproject\API_project_v0\tests\test_audit.py", line 60, in test_audit
self.assertEqual(expect_result["code"],actual_result["code"])
这里我们查到实际结果的状态码为1005,查看接口文档:
可得知这是服务器繁忙,我们可以尝试用postman进行接口访问:
可看到返回的结果依然为1005,而错误信息是:Request method ‘POST’ not supported,也就是post的请求方法是不支持的,这时我们可以看下接口文档:
可看到请求方法为patch,我们把postman的请求方法改成patch即可访问成功:
于是我们把代码中request_handler的请求方法改成patch,错误就没了。
BUG3:pymysql.err.ProgrammingError
Error
Traceback (most recent call last):
File "C:\Users\rainstar\AppData\Local\Programs\Python\Python38\lib\unittest\case.py", line 60, in testPartExecutor
yield
File "C:\Users\rainstar\AppData\Local\Programs\Python\Python38\lib\unittest\case.py", line 676, in run
self._callTestMethod(testMethod)
File "C:\Users\rainstar\AppData\Local\Programs\Python\Python38\lib\unittest\case.py", line 633, in _callTestMethod
method()
File "C:\Users\rainstar\PycharmProjects\pyproject\venv\lib\site-packages\ddt.py", line 182, in wrapper
return func(self, *args, **kwargs)
File "C:\Users\rainstar\PycharmProjects\pyproject\API_project_v0\tests\test_audit.py", line 65, in test_audit
loan_in_sql = self.db.query("SELECT * FROM futureloan.loan WHERE id=".format(self.loan_id))
File "C:\Users\rainstar\PycharmProjects\pyproject\API_project_v0\common\sql_handler.py", line 30, in query
self.cursor.execute(sql)
File "C:\Users\rainstar\PycharmProjects\pyproject\venv\lib\site-packages\pymysql\cursors.py", line 163, in execute
result = self._query(query)
File "C:\Users\rainstar\PycharmProjects\pyproject\venv\lib\site-packages\pymysql\cursors.py", line 321, in _query
conn.query(q)
File "C:\Users\rainstar\PycharmProjects\pyproject\venv\lib\site-packages\pymysql\connections.py", line 505, in query
self._affected_rows = self._read_query_result(unbuffered=unbuffered)
File "C:\Users\rainstar\PycharmProjects\pyproject\venv\lib\site-packages\pymysql\connections.py", line 724, in _read_query_result
result.read()
File "C:\Users\rainstar\PycharmProjects\pyproject\venv\lib\site-packages\pymysql\connections.py", line 1069, in read
first_packet = self.connection._read_packet()
File "C:\Users\rainstar\PycharmProjects\pyproject\venv\lib\site-packages\pymysql\connections.py", line 676, in _read_packet
packet.raise_for_error()
File "C:\Users\rainstar\PycharmProjects\pyproject\venv\lib\site-packages\pymysql\protocol.py", line 223, in raise_for_error
err.raise_mysql_exception(self._data)
File "C:\Users\rainstar\PycharmProjects\pyproject\venv\lib\site-packages\pymysql\err.py", line 107, in raise_mysql_exception
raise errorclass(errno, errval)
pymysql.err.ProgrammingError: (1064, "You have an error in your SQL syntax; check the manual that corresponds to your MariaDB server version for the right syntax to use near '' at line 1")
错误是可能sql写的有问题,对应的代码如下:
#根据上面的loan_id查询loan表中对应记录的status的值
loan_in_sql = self.db.query("SELECT * FROM futureloan.loan WHERE id=".format(self.loan_id))
这里id=后面忘记写{}了,导致系统判断sql写错了。
BUG4:AssertionError
2 != 0
Expected :0
Actual :2
<Click to see difference>
Traceback (most recent call last):
File "C:\Program Files\JetBrains\PyCharm Community Edition 2020.1\plugins\python-ce\helpers\pycharm\teamcity\diff_tools.py", line 32, in _patched_equals
old(self, first, second, msg)
File "C:\Users\rainstar\AppData\Local\Programs\Python\Python38\lib\unittest\case.py", line 912, in assertEqual
assertion_func(first, second, msg=msg)
File "C:\Users\rainstar\AppData\Local\Programs\Python\Python38\lib\unittest\case.py", line 905, in _baseAssertEqual
raise self.failureException(msg)
AssertionError: 0 != 2
During handling of the above exception, another exception occurred:
Traceback (most recent call last):
File "C:\Users\rainstar\AppData\Local\Programs\Python\Python38\lib\unittest\case.py", line 60, in testPartExecutor
yield
File "C:\Users\rainstar\AppData\Local\Programs\Python\Python38\lib\unittest\case.py", line 676, in run
self._callTestMethod(testMethod)
File "C:\Users\rainstar\AppData\Local\Programs\Python\Python38\lib\unittest\case.py", line 633, in _callTestMethod
method()
File "C:\Users\rainstar\PycharmProjects\pyproject\venv\lib\site-packages\ddt.py", line 182, in wrapper
return func(self, *args, **kwargs)
File "C:\Users\rainstar\PycharmProjects\pyproject\API_project_v0\tests\test_audit.py", line 60, in test_audit
self.assertEqual(expect_result["code"],actual_result["code"])
可看到实际结果返回的状态码为2,我们看下接口文档2代表什么:
可看到2表示参数错误,所以有可能是request_handler的传参有问题。
最终代码
from middleware.Handler import Handler
from common import request_handler
import ddt
import json
import unittest
env_data = Handler()
#初始化Handler中的yaml_handler
yaml = Handler.yaml
#初始化Handler中的excel_handler
excel = Handler.excel
#厨师化Handler中的logging_handler
log = Handler.logger
#通过excel_handler获取excel中audit表单中的所有数据
excel_data = excel.get_data("audit")
#在需要用到数据驱动的类上定义ddt装饰器
@ddt.ddt
class Test_audit(unittest.TestCase):
#通过前置类方法获取Handler中的loan_id及admin_token
@classmethod
def setUpClass(cls) -> None:
cls.token = env_data.get_token
cls.admin_token = env_data.get_admin_token
cls.member_id = env_data.get_userId
#通过前置实例方法初始化mysql的连接对象,使得每个用例用到的连接对象都是相互独立的
def setUp(self) -> None:
self.loan_id = env_data.get_loanId
self.db = env_data.mysql_class()
#在需要用到数据驱动的类方法上定义data装饰器
@ddt.data(*excel_data)
def test_audit(self,cases):
#准备excel中的data并用excel_data变量接收
excel_data = cases["data"]
# 准备excel中的headers并用excel_headers变量接收
excel_headers = cases["headers"]
#替换data中的#loan_id#
if "#loan_id#" in excel_data:
excel_data = excel_data.replace("#loan_id#",str(self.loan_id))
#替换data中的#pass_loan_id#
if "#pass_loan_id#" in excel_data:
#查询loan表中状态为还款中的记录并取其中一条记录的id替换到#pass_loan_id#
pass_loan_id = self.db.query("SELECT * FROM futureloan.loan WHERE status=3")["id"]
excel_data = excel_data.replace("#pass_loan_id#",str(pass_loan_id))
# 替换headers中的#admin_token#
if "#admin_token#" in excel_headers:
excel_headers = excel_headers.replace("#admin_token#",str(self.admin_token))
#将excel中字符串格式的data转换成字典格式
data = eval(excel_data)
#调用request_handler请求接口
actual_result = request_handler.requests_handler(
method = "patch",
url = yaml["host"] + cases["url"],
json = data,
headers = json.loads(excel_headers)
)
#准备预期结果
expect_result = json.loads(cases["expected"])
#将预期结果与实际结果进行断言
self.assertEqual(expect_result["code"],actual_result["code"])
self.assertEqual(expect_result["msg"],actual_result["msg"])
#数据库表断言
if actual_result["code"] == 0:
#根据上面的loan_id查询loan表中对应记录的status的值
loan_in_sql = self.db.query("SELECT * FROM futureloan.loan WHERE id={}".format(self.loan_id))
#拿loan表中status的值与预期结果的status的值进行断言
self.assertTrue(loan_in_sql["status"] == expect_result["status"])