接口介绍
excel测试用例
代码实操
问题1:excel中数据类型的转换
- test_register.py
import unittest
import ddt
import os
from common.excel_handler import excel_handler
from common.request_handler import requests_handler
from config import config
#准备数据驱动
@ddt.ddt
class Test_demo(unittest.TestCase):
"""
由于测试数据来源于data下的excel文件,所以我们需要提前编写接口的测试用例。
"""
#接口测试用例放在data中,所以我们需要提取到测试用例的路径,这时我们就需要调用config.py下的DATA_PATH
cases_path = os.path.join(config.DATA_PATH,"cases.xlsx")
#我们需要读取cases.xlsx里面的所有数据,所以我们需要调用excel_handler来帮我们获取excel数据
test_cases = excel_handler(cases_path).get_data("register_v0")
#将test_cases作为数据源放入data装饰器中
@ddt.data(*test_cases)
#在需要使用数据的方法中用形参来接收数据
def test_register(self,test_data):
#在调用接口传入参数前,我们需要把测试数据用变量定义出来,包括请求url、请求方法、请求参数、请求头、预期结果
url = test_data["url"]
method = test_data["method"]
json = test_data["data"]
headers = test_data["headers"]
expected = test_data["expected"]
#调用common下的request_handler来调用接口,传入上面准备好的数据
resp = requests_handler(
url = url,
method = method,
headers = headers,
data = json
)
执行上面写好的代码,报了个错:
这个出错是因为一个字符串类型的数据,我们却调用了它的items属性,而items属性是只有字典才有的。接着我们定位到出错的代码:
这表明其中有某个变量是字符串类型,而我们却把它当成了字典类型,错误地调用了items属性。
我们可以打个断点进行调试:
调试结果如下,我们可看到所有的变量返回的都是字符串类型的数据:
我们再来看下excel中的数据:
可看到data、headers、expected字段的数据我们本来就是用字典来存放的,但是这里就有一个问题:excel中的数据,不管你里面用什么看似字典、列表等这些数据类型存放,到了python这边拿过来都统一当成字符串处理,所以刚刚的’str’ object has no attribute 'items’问题原因就出来了:我们excel中的数据用的是字典存放,用excel_handler读取过来我们想通过字典的items属性取值,但实际上读取过来的数据类型是字符串类型。
这时我们就需要使用eval方法。eval是直接把字符串中的引号去掉,然后执行字符串中的内容。在这里test_data中的数据读取过来就是字符串,而字符串的内容就是一个字典,所以eval函数就是去掉字符串的引号,就成了一个字典。
def test_register(self,test_data):
#在调用接口传入参数前,我们需要把测试数据用变量定义出来,包括请求url、请求方法、请求参数、请求头、预期结果
url = test_data["url"]
method = test_data["method"]
#修改后————
json = eval(test_data["data"])
headers = eval(test_data["headers"])
expected = eval(test_data["expected"])
#调用common下的request_handler来调用接口,传入上面准备好的数据
resp = requests_handler(
url = url,
method = method,
headers = headers,
json = json
)
再次执行后,可成功:
接着我们继续往下写。
问题2:断言
- test_register.py
import unittest
import ddt
import os
from common.excel_handler import excel_handler
from common.request_handler import requests_handler
from config import config
#准备数据驱动
@ddt.ddt
class Test_demo(unittest.TestCase):
"""
由于测试数据来源于data下的excel文件,所以我们需要提前编写接口的测试用例。
"""
#接口测试用例放在data中,所以我们需要提取到测试用例的路径,这时我们就需要调用config.py下的DATA_PATH
cases_path = os.path.join(config.DATA_PATH,"cases.xlsx")
#我们需要读取cases.xlsx里面的所有数据,所以我们需要调用excel_handler来帮我们获取excel数据
test_cases = excel_handler(cases_path).get_data("register_v0")
#将test_cases作为数据源放入data装饰器中
@ddt.data(*test_cases)
#在需要使用数据的方法中用形参来接收数据
def test_register(self,test_data):
#在调用接口传入参数前,我们需要把测试数据用变量定义出来,包括请求url、请求方法、请求参数、请求头、预期结果
url = test_data["url"]
method = test_data["method"]
json = eval(test_data["data"])
headers = eval(test_data["headers"])
expected = eval(test_data["expected"])
#调用common下的request_handler来调用接口,传入上面准备好的数据
resp = requests_handler(
url = url,
method = method,
headers = headers,
json = json
)
#拿预期结果与实际结果进行断言
self.assertEqual(expected,resp)
加上断言后,再次执行时又出错了:
我们可以看到,调用接口得到的实际返回结果有一堆数据:code、copyright、data、msg,而我们测试数据里的预期结果只有code和msg。
这里我们说下断言的两种类型:全量断言和部分断言。
全量断言:所有的数据都需要进行比对,一字不差;
部分断言:只比对部分字段。
这里我们准备的预期结果只有msg和code这两个字段,所以我们只需要部分断言,只需要拿实际结果的msg和code这两个字段出来比对就行了。
方法一
#拿预期结果中的code与实际结果的code进行断言
self.assertEqual(expected["code"],resp["code"])
#拿预期结果中的msg与实际结果的msg进行断言
self.assertEqual(expected["msg"],resp["msg"])
方法二
"""
key = code,value = 1,resp["code"] = 1;
key = msg,value = "手机号为空,"resp["msg"] = "手机号为空"
"""
for key,value in expected.items():
self.assertEqual(value,resp[key])
问题3:yaml配置文件的写入与读取
其实在接口自动化里,我们完全可以不用yaml,将所有需要配置的项目统一放到一个py文件里面也可以。但我们之所以用yaml,第一是因为它的跨语言特性,它不仅支持python,还支持java、JavaScript、C等语言,是所有语言都通用的;第二是因为公司的需要。所以我们需要将一些配置项放到yaml中,然后在代码中需要用到的地方再读取出来。
一个需要配置到yaml的例子就是测试用例excel的文件名。这个是一个静态数据,不会有大的变化,所以我们可以直接放在yaml中;另一个可以放到yaml中的配置项是logging_handler进行初始化时传入的参数:name、file、logger_level、stream_level、file_level;还有一个经常放到配置文件的配置就是数据库的配置,包括host、port、username、password。
配置好的yaml如下:
- config.yml
excel:
name: "cases.xlsx"
log:
name: "logger"
file: "test_log.txt"
logger_level: "DEBUG"
stream_level: "DEBUG"
file_level: "INFO"
db:
host: "135.178.41.56"
port: 3306
user: "tom"
password: "123456"
charset: 'utf8'
接着我们以excel的配置项为例,来实现在测试用例中通过yaml_handler导入配置项。
以前:
- test_register.py
#接口测试用例放在data中,所以我们需要提取到测试用例的路径,这时我们就需要调用config.py下的DATA_PATH
cases_path = os.path.join(config.DATA_PATH,"cases.xlsx")
通过yaml_handler读取yaml配置文件的测试用例名称:
#通过yaml_handler的read_yaml方法读取yaml配置文件中的测试用例名称
yaml_path = os.path.join(config.CONFIG_PATH,"config.yml")
yaml_data = read_yaml(yaml_path)
case_file = yaml_data["excel"]["name"]
#准备数据驱动
@ddt.ddt
class Test_demo(unittest.TestCase):
"""
由于测试数据来源于data下的excel文件,所以我们需要提前编写接口的测试用例。
"""
#接口测试用例放在data中,所以我们需要提取到测试用例的路径,这时我们就需要调用config.py下的DATA_PATH
cases_path = os.path.join(config.DATA_PATH,case_file)
#我们需要读取cases.xlsx里面的所有数据,所以我们需要调用excel_handler来帮我们获取excel数据
logger.info("正在读取excel测试用例")
test_cases = excel_handler(cases_path).get_data("register_v0")
#后续代码省略
可以看到,通过yaml读取反而代码还更长了,非常麻烦。现在先留着,等下再改进下。
问题4:json格式与字典格式的相互转换
上面(问题1)我们通过eval将测试用例中的data、header、expected这些字符串格式的数据转换成字典是没问题的。但有可能如果excel中的某些数据用到了json的格式,而json格式中的null、true、false,这样就没法通过eval方法进行转换了。见如下的例子:
a = '{"name":null,"sex":"female","is_student":true,"is_city":false}'
print(eval(a))
打印结果如下:
那么为什么我们要在excel中写入json格式的数据呢,直接写入字典形式的数据,然后不就能通过eval方法正确地转换过来了嘛?这样是可以,但不方便。一般像data、expected这样的数据,我们都是直接通过postman工具请求接口,得到想要的结果之后,把请求参数(data)、返回结果(expected)直接复制到excel里面,而复制过来的数据就是json形式的数据。这时为了这些数据提取到python中能通过eval方法识别出来,我们又要修改个别的参数,把null改成None,true改成True,false改成False,这样大可不必!
那么我们就维持现状,直接在excel中维护json格式的数据。那么为什么要将json转换成字典呢?当然是为了取值方便,我们可以直接通过key来获取value然后在代码中进行操作。
那重点来了!如何将json格式的数据转换成字典呢?我们可以使用json模块下的load方法。
import json
a = '{"name":null,"sex":"female","is_student":true,"is_city":false}'
json_data = json.loads(a)
print(json_data)
执行结果如下:
因此,我们可以把之前准备excel数据时用到的eval方法替换成json.loads方法:
def test_register(self,test_data):
#在调用接口传入参数前,我们需要把测试数据用变量定义出来,包括请求url、请求方法、请求参数、请求头、预期结果
url = test_data["url"]
method = test_data["method"]
json_data = json.loads(test_data["data"])
headers = json.loads(test_data["headers"])
expected = json.loads(test_data["expected"])
问题5:打日志
代码中我们需要对阶段性的代码进行标记(如:准备测试数据的地方、请求接口的地方、进行断言的地方等),又或者需要对一些可能会发生错的地方记录日志,此时我们需要初始化logging_handler来帮我们记录日志:
- test_register.py
#初始化logging_handler
log_config = yaml_data["log"]
logger = logging_handler.logging_handler(
logger_name=log_config["name"],
logger_level=log_config["logger_level"],
stream_level=log_config["stream_level"],
file_name=os.path.join(config.LOG_PATH,log_config["file"]),
file_level=log_config["file_level"]
)
#在一些需要打标记的地方进行标记
#省略前后代码
#对读取测试用例打日志
logger.info("正在读取excel测试用例")
test_cases = excel_handler(cases_path).get_data("register_v0")
#对调用接口打日志
logger.info("正在请求接口")
resp = requests_handler(
url = url,
method = method,
headers = headers,
json = json_data
)
#对断言打日志
logger.info("正在进行断言")
try:
for key,value in expected.items():
self.assertEqual(value,resp[key])
logger.info("测试用例通过!")
except AssertionError as e:
logger.error("测试用例不通过:{}".format(e))
raise e
执行结果如下:
这里要注意的是,对断言结果进行日志跟踪时,通过的测试用例用info级别就行了,没通过(也就是会抛出断言异常)的用例就需要用error级别。
问题6:查询数据库
接口执行成功后,经常会与数据库产生交互。而验证与数据库产生交互的接口有没有执行成功,我们需要去查询数据库对应的表中数据有没有发生变更。比如注册接口,我们输入手机号、密码之后,调用接口如果返回执行成功,那么按理说数据库中就已经插入了该手机号、密码,所以我们需要去查询数据库表有没有新增对应的记录。
- test_register.py
import unittest
import ddt
import os
import json
from common.excel_handler import excel_handler
from common.request_handler import requests_handler
from common.yaml_handler import read_yaml
from common.sql_handler import mysql_handler
from config import config
from common import logging_handler
#通过yaml_handler的read_yaml方法读取yaml配置文件中的测试用例名称
yaml_path = os.path.join(config.CONFIG_PATH,"config.yml")
yaml_data = read_yaml(yaml_path)
#从yaml配置文件中读取excel测试用例的名称
case_file = yaml_data["excel"]["name"]
#从yaml中读取log配置
log_config = yaml_data["log"]
#初始化logging_handler
logger = logging_handler.logging_handler(
logger_name=log_config["name"],
logger_level=log_config["logger_level"],
stream_level=log_config["stream_level"],
file_name=os.path.join(config.LOG_PATH,log_config["file"]),
file_level=log_config["file_level"]
)
#从yaml中读取db配置
db_config = yaml_data["db"]
#初始化mysql_handler
mysql_handler = mysql_handler(
host=db_config["host"],
port=db_config["port"],
user=db_config["user"],
password=db_config["password"],
charset=db_config["charset"]
)
#准备数据驱动
@ddt.ddt
class Test_demo(unittest.TestCase):
"""
由于测试数据来源于data下的excel文件,所以我们需要提前编写接口的测试用例。
"""
#接口测试用例放在data中,所以我们需要提取到测试用例的路径,这时我们就需要调用config.py下的DATA_PATH
cases_path = os.path.join(config.DATA_PATH,case_file)
#我们需要读取cases.xlsx里面的所有数据,所以我们需要调用excel_handler来帮我们获取excel数据
logger.info("正在读取excel测试用例")
test_cases = excel_handler(cases_path).get_data("register_v0")
#将test_cases作为数据源放入data装饰器中
@ddt.data(*test_cases)
#在需要使用数据的方法中用形参来接收数据
def test_register(self,test_data):
# 先将excel测试用例中的data单独用变量存起来,后面读取data里的手机号时需要用
excel_data = json.loads(test_data["data"])
#在调用接口传入参数前,我们需要把测试数据用变量定义出来,包括请求url、请求方法、请求参数、请求头、预期结果
url = test_data["url"]
method = test_data["method"]
json_data = excel_data
headers = json.loads(test_data["headers"])
expected = json.loads(test_data["expected"])
#调用common下的request_handler来调用接口,传入上面准备好的数据
logger.info("正在请求接口")
resp = requests_handler(
url = url,
method = method,
headers = headers,
json = json_data
)
"""
key = code,value = 1,resp["code"] = 1;
key = msg,value = "手机号为空,"resp["msg"] = "手机号为空"
"""
logger.info("正在进行断言")
try:
for key,value in expected.items():
self.assertEqual(value,resp[key])
#如果接口执行成功,那么就要去数据库中查询有没有插入新的手机号
if resp["code"] == 0:
# 准备查询语句,用于查询注册成功后,excel_data里的手机号是否已经存入数据库
query_sql = "SELECT * FROM futureloan.member WHERE mobile_phone={} LIMIT 5;".format(
excel_data["mobile_phone"]
)
#调用mysql_handler的查询方法
result = mysql_handler.query(query_sql)
#对数据库返回的结果进行断言.如果有返回结果,就是true;无返回结果就是false
self.assertTrue(result)
logger.info("测试用例通过!")
except AssertionError as e:
logger.error("测试用例不通过:{}".format(e))
raise e
这里需要特别注意的是,必须先判断用例执行成功(调用接口后返回成功的message或code),然后再去进行sql操作看下数据有没有发生变化,因为用例执行失败(接口执行失败),有可能未与数据库发生交互。而你此时去用sql查询数据库表时,这张表的数据很可能是没有发生变化的(因为接口执行失败,所以很可能没对这张表改动过),从而导致用查询sql返回的结果进行断言不准确。以注册接口为例,如果不加if resp[“code”] == 0,那么无论接口执行成功与否你都拿data里的mobile_phone去查询member表中是否有该手机号存在。这时会有两种结果:1.在数据库中没找到这个手机号,那你就认为用例执行不通过,这样看起来也没什么问题;2.在数据库中找到了该手机号,那你就认为测试用例通过,这就有问题了。因为如果本来member表中就有这个手机号,接口执行失败,你拿data中的手机号去查询表中有没有这个手机号,结果是有的,你就断言说测试用例通过,这是不对的,因为这个存在表中的手机号并不是你接口执行成功后而生成的记录。
所以,需要特别注意这条逻辑,执行接口成功后,数据库表才可能发生相应的数据变化。所以得先判断接口返回值是成功的,再去执行sql,用sql执行的结果去断言,这样才准确。
问题7:【业务】特殊用例:注册成功
先说下注册的逻辑:首先,拿一个未注册的正确的手机号去注册,是能注册成功的;但如果你再拿这个已经注册成功的手机号再去注册一遍,那就注册失败了。
excel测试用例中有一条注册成功的用例,如果把手机号写死,那第一次执行时是会成功的,第二次及后面的执行都拿这个已注册的手机号去注册,那肯定是会注册失败的。所以这里我们得对手机号进行参数化,使得每次注册的手机号都不一样。
那么问题来了,如何使得每次注册的手机号都不一样呢?这里我们可以写一个自动生成手机号的方法,当每次获取excel中的数据请求接口之前,我们可以调用这个方法自动生成手机号,然后把手机号存入excel测试用例中。
- test_register.py
def radom_phone(self):
while True:
#准备手机号前2位
phone = "13"
#依次填补手机号后9位
for i in range(9):
#从0到9随机取一个数字
num = random.randint(0,9)
#把取到的随机数加到phone后。注意phone为str,而随机数为int,所以得转换成str
phone += str(num)
print(phone)
#查询生成的随机手机号是否在member表中存在
query_sql = "SELECT * FROM futureloan.member WHERE mobile_phone={} LIMIT 5;".format(phone)
mysql = Handler.mysql_class()
result = mysql.query(query_sql)
#如果手机号在表中存在,则继续生成手机号(回到while);如果不存在,则返回该手机号
if not result:
return phone
mysql.close()
接着,在获取excel数据前,调用该方法,把生成的手机号替换到测试用例中带有#phone#的部分:
def test_register(self,test_data):
# 如果data中的mobile_phone有占位符#phone#,则随机生成手机号替换该占位符
if "#phone#" in test_data["data"]:
new_phone = self.radom_phone()
test_data["data"] = test_data["data"].replace("#phone#", new_phone)
# 先将excel测试用例中的data单独用变量存起来,后面读取data里的手机号时需要用
excel_data = json.loads(test_data["data"])
这里我们可以总结一下:
- 当同一条测试用例每次都要求请求的数据不一样时,我们可以在excel中对数据进行参数化,然后再在python中用代码生成数据,然后再塞入excel中。
- 当要断言成功用例时要进行两层断言:
- 接口返回结果断言。判断返回结果是否符合预期;
- 数据库表结果断言。判断表中的记录变更是否符合预期。
问题8:两次数据库操作均使用同一个游标对象
在sql1中打断点,这种情况返回None是对的,因为从代码逻辑来说,用生成的手机号去查询数据库,如果有记录则继续生成;如果没记录则返回手机号。所以最终生成的手机号在数据库表中是不存在的,所以查询结果返回None。
在sql2中打断点,可看到返回的结果也是None:
这个结果是错的,因为我们通过sql工具执行这个sql是有返回结果的。另外外,从代码逻辑来看,这是在注册成功后返回状态码为0时,再用已经注册成功的手机号去数据库查询有没有对应的记录,正常情况下注册成功后是会往表中插入一入一条新纪录的(排除掉无法插入记录的异常情况),所以这里不应该返回None。
出现以上结果的原因是第一次查询数据库表与第二次查询时用的是同一个数据库连接对象,而查询时需要用到的游标对象也是由同一个连接对象生成的。第一次查询时,游标到了对应结果的那一行;而第二次查询时并没有新建另一个游标对象,而是用旧的游标对象,而旧的游标对象还是在之前查询结果的那一行,所以就会导致第二次查询的结果有问题。
解决方案一 每次要使用sql操作数据库时创建一个数据库连接对象:
import unittest
import ddt
import os
import json
from common.excel_handler import excel_handler
from common.request_handler import requests_handler
from common.yaml_handler import read_yaml
from common.sql_handler import mysql_handler
from config import config
from common import logging_handler
import random
#通过yaml_handler的read_yaml方法读取yaml配置文件中的测试用例名称
yaml_path = os.path.join(config.CONFIG_PATH,"config.yml")
yaml_data = read_yaml(yaml_path)
#从yaml配置文件中读取excel测试用例的名称
case_file = yaml_data["excel"]["name"]
#从yaml中读取log配置
log_config = yaml_data["log"]
#初始化logging_handler
logger = logging_handler.logging_handler(
logger_name=log_config["name"],
logger_level=log_config["logger_level"],
stream_level=log_config["stream_level"],
file_name=os.path.join(config.LOG_PATH,log_config["file"]),
file_level=log_config["file_level"]
)
#从yaml中读取db配置
db_config = yaml_data["db"]
#准备数据驱动
@ddt.ddt
class Test_register(unittest.TestCase):
"""
由于测试数据来源于data下的excel文件,所以我们需要提前编写接口的测试用例。
"""
#接口测试用例放在data中,所以我们需要提取到测试用例的路径,这时我们就需要调用config.py下的DATA_PATH
cases_path = os.path.join(config.DATA_PATH,case_file)
#我们需要读取cases.xlsx里面的所有数据,所以我们需要调用excel_handler来帮我们获取excel数据
logger.info("正在读取excel测试用例")
test_cases = excel_handler(cases_path).get_data("register_v0")
#将test_cases作为数据源放入data装饰器中
@ddt.data(*test_cases)
#在需要使用数据的方法中用形参来接收数据
def test_register(self,test_data):
# 如果data中的mobile_phone有占位符#phone#,则随机生成手机号替换该占位符
if "#phone#" in test_data["data"]:
new_phone = self.radom_phone()
test_data["data"] = test_data["data"].replace("#phone#", new_phone)
# 先将excel测试用例中的data单独用变量存起来,后面读取data里的手机号时需要用
excel_data = json.loads(test_data["data"])
#在调用接口传入参数前,我们需要把测试数据用变量定义出来,包括请求url、请求方法、请求参数、请求头、预期结果
url = test_data["url"]
method = test_data["method"]
json_data = excel_data
headers = json.loads(test_data["headers"])
expected = json.loads(test_data["expected"])
#调用common下的request_handler来调用接口,传入上面准备好的数据
logger.info("正在请求接口")
resp = requests_handler(
url = url,
method = method,
headers = headers,
json = json_data
)
"""
key = code,value = 1,resp["code"] = 1;
key = msg,value = "手机号为空,"resp["msg"] = "手机号为空"
"""
logger.info("正在进行断言")
try:
for key,value in expected.items():
self.assertEqual(value,resp[key])
#如果接口执行成功,那么就要去数据库中查询有没有插入新的手机号
if resp["code"] == 0:
mysql_handler2 = mysql_handler(
host=db_config["host"],
port=db_config["port"],
user=db_config["user"],
password=db_config["password"],
charset=db_config["charset"]
)
# 准备查询语句,用于查询注册成功后,excel_data里的手机号是否已经存入数据库
query_sql = "SELECT * FROM futureloan.member WHERE mobile_phone={} LIMIT 5;".format(
excel_data["mobile_phone"]
)
#调用mysql_handler的查询方法
result = mysql_handler2.query(query_sql)
#对数据库返回的结果进行断言.如果有返回结果,就是true;无返回结果就是false
self.assertTrue(result)
logger.info("测试用例通过!")
except AssertionError as e:
logger.error("测试用例不通过:{}".format(e))
raise e
#随机生成手机号方法,用于注册成功的测试用例
def radom_phone(self):
while True:
#准备手机号前2位
phone = "13"
#依次填补手机号后9位
for i in range(9):
#从0到9随机取一个数字
num = random.randint(0,9)
#把取到的随机数加到phone后。注意phone为str,而随机数为int,所以得转换成str
phone += str(num)
print(phone)
#查询生成的随机手机号是否在member表中存在
query_sql = "SELECT * FROM futureloan.member WHERE mobile_phone={} LIMIT 5;".format(phone)
#初始化mysql_handler
mysql_handler1 = mysql_handler(
host=db_config["host"],
port=db_config["port"],
user=db_config["user"],
password=db_config["password"],
charset=db_config["charset"]
)
result = mysql_handler1.query(query_sql)
#如果手机号在表中存在,则继续生成手机号(回到while);如果不存在,则返回该手机号
if not result:
return phone
mysql_handler.close()
上述这个方案明细是特别low的,不可能这么玩,所以下面问题9提供第二种解决方案。
问题9:二次封装
我们可以看到,目前为止的代码中,在test_register方法前,我们的准备工作(初始化各个公共模块的对象,包括excel_handler、logging_handler、sql_handler、yaml_handler)太过臃肿了,而这只是一个注册的测试用例,如果我再来其他的登录、充值等等的测试用例,每一个用例在测试用例方法前都需要粘贴过来这么一大段的准备工作,这很明显是不行的,我们要向下能不能把这些公共模块的对象初始化工作放到一个单独的模块中,让每一个测试用例模块都能复用这个模块。
那么问题又来了,我们应该把这个模块放到哪?
第一个想到的可能是common包下。但common包下的模块都是跟业务毫无关系,放到任何一个项目中都能用的,且彼此之间是相互独立的。而我们这个准备工作都是各个公共模块的对象初始化,excel_handler、logging_handler、sql_handler的初始化传入的参数都要依赖yaml_handler来获取yaml配置文件下的配置项,这就造成了模块与模块之间的依赖关系,所以是不宜放在common下的。
这里我们需要单独新建一个新的包,叫middleware,也就是中间件,它起到了公共模块(common下的各个模块)与业务模块(tests下的各个测试用例)搭起桥梁建立联系的作用。
下面我们开始这些准备工作写到Handler的模块当中:
- Handler.py
import os
from common import yaml_handler
from common import excel_handler
from common import logging_handler
from common.sql_handler import mysql_handler
from config import config
from pymysql.cursors import DictCursor
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
这里有一个很重要的思想,叫做万物皆对象。在python中,模块、函数和类也可以被当成对象,然后赋值给某个变量。如上面的__conf = config就是把config模块赋值给conf私有变量; mysql_class = mysqlMiddleHandler就是把mysqlMiddleHandler类赋值给mysql_class变量。
这里为什么是把类赋给变量而不是把实例赋给变量呢?如果是把实例赋给mysql_class时,所有的测试类都需要用到这个实例,也就是用到mysql_handler中的同一个游标对象。这会造成什么后果呢?比如注册测试类用了一次游标对象,这时光标是放到表中的某一条数据上的;当登录类再使用这个游标对象查询当前表或新的表时,由于光标还是在某一条数据中,此时是无法查询新的数据的,所以这是不合理的。所以考虑到每个测试类都需要重新初始化一个游标对象,所以是把类赋给变量,这样每个测试类都可以初始化一个新的mysql_handler对象。
另外还有一个概念叫私有变量。python中如果仅仅只想在当前模块使用这个变量,可以在变量名前加上两个下划线,这样别的模块就无法引用该变量了。
写好了middleware后,我们在register中就可以直接引用Handler了。
- register.py
import unittest
import ddt
import os
import json
from middleware.Handler import Handler
from common.request_handler import requests_handler
import random
#初始化yaml_handler
yaml = Handler.yaml
#初始化excel_handler
excel = Handler.excel
#初始化logging_handler
logger = Handler.logger
#准备数据驱动
@ddt.ddt
class Test_register(unittest.TestCase):
"""
由于测试数据来源于data下的excel文件,所以我们需要提前编写接口的测试用例。
"""
#我们需要读取cases.xlsx里面的所有数据,所以我们需要调用excel_handler来帮我们获取excel数据
logger.info("正在读取excel测试用例")
test_cases = excel.get_data("register_v0")
#将test_cases作为数据源放入data装饰器中
@ddt.data(*test_cases)
#在需要使用数据的方法中用形参来接收数据
def test_register(self,test_data):
# 如果data中的mobile_phone有占位符#phone#,则随机生成手机号替换该占位符
if "#phone#" in test_data["data"]:
new_phone = self.radom_phone()
test_data["data"] = test_data["data"].replace("#phone#", new_phone)
# 先将excel测试用例中的data单独用变量存起来,后面读取data里的手机号时需要用
excel_data = json.loads(test_data["data"])
#在调用接口传入参数前,我们需要把测试数据用变量定义出来,包括请求url、请求方法、请求参数、请求头、预期结果
url = test_data["url"]
method = test_data["method"]
json_data = excel_data
headers = json.loads(test_data["headers"])
expected = json.loads(test_data["expected"])
#调用common下的request_handler来调用接口,传入上面准备好的数据
logger.info("正在请求接口")
resp = requests_handler(
url = url,
method = method,
headers = headers,
json = json_data
)
"""
key = code,value = 1,resp["code"] = 1;
key = msg,value = "手机号为空,"resp["msg"] = "手机号为空"
"""
logger.info("正在进行断言")
try:
for key,value in expected.items():
self.assertEqual(value,resp[key])
#如果接口执行成功,那么就要去数据库中查询有没有插入新的手机号
if resp["code"] == 0:
# 准备查询语句,用于查询注册成功后,excel_data里的手机号是否已经存入数据库
query_sql = "SELECT * FROM futureloan.member WHERE mobile_phone={} LIMIT 5;".format(
excel_data["mobile_phone"]
)
#调用mysql_handler的查询方法
mysql = Handler.mysql_class()
result = mysql.query(query_sql)
#对数据库返回的结果进行断言.如果有返回结果,就是true;无返回结果就是false
self.assertTrue(result)
logger.info("测试用例通过!")
except AssertionError as e:
logger.error("测试用例不通过:{}".format(e))
raise e
#随机生成手机号方法,用于注册成功的测试用例
def radom_phone(self):
while True:
#准备手机号前2位
phone = "13"
#依次填补手机号后9位
for i in range(9):
#从0到9随机取一个数字
num = random.randint(0,9)
#把取到的随机数加到phone后。注意phone为str,而随机数为int,所以得转换成str
phone += str(num)
print(phone)
#查询生成的随机手机号是否在member表中存在
query_sql = "SELECT * FROM futureloan.member WHERE mobile_phone={} LIMIT 5;".format(phone)
mysql = Handler.mysql_class()
result = mysql.query(query_sql)
#如果手机号在表中存在,则继续生成手机号(回到while);如果不存在,则返回该手机号
if not result:
return phone
mysql.close()
在这里,我们每次需要进行sql操作数据库时,直接调用Handler的my_class变量,而这个变量指向mysqlMiddleHandler类,所以通过my_class()即可初始化一个mysqlMiddleHandler类的对象,而初始化对象的参数都不需要再传入,因为这些参数已经在mysqlMiddleHandler类中传进去了,所以不需要传入任何的参数即可初始化对象,这样就能使用不同的数据库连接对象生成不同的游标对象,上述问题8中使用同一个数据库连接对象的问题也就解决了。
问题10:测试用例的前置与后置
在test_register、radom_phone这两个方法中,我们初始化了2次db对象,并且最后进行关闭:
这部分代码其实是可以放到函数级别的前置、后置方法中的。每次调用这两个方法时初始化一次db对象,最终进行关闭:
#初始化db对象
def setUp(self) -> None:
self.mysql = Handler.mysql_class()
#关闭db对象
def tearDown(self) -> None:
self.mysql.close()