pytestDemo 接口自动化测试项目学习笔记
pytestDemo 接口自动化测试项目学习笔记
项目地址:https://github.com/wintests/pytestDemo
感谢:wintests博主,如果你也觉得该项目有所帮助,请给博主点个小star。一起学习,共同进步!
项目介绍:
该项目实现接口自动化的技术选型:**Python+Requests+Pytest+YAML+Allure** ,主要是针对本人的一个接口项目来开展的,通过 Python+Requests 来发送和处理HTTP协议的请求接口,使用 Pytest 作为测试执行器,使用 YAML 来管理测试数据,使用 Allure 来生成测试报告。
该项目在实现过程中,把整个项目拆分成请求方法封装、HTTP接口封装、关键字封装、测试用例等模块。
首先利用Python把HTTP接口封装成Python接口,接着把这些Python接口组装成一个个的关键字,再把关键字组装成测试用例,而测试数据则通过YAML文件进行统一管理,然后再通过Pytest测试执行器来运行这些脚本,并结合Allure输出测试报告。
学习体会:该项目通过多层封装实现测试数据和用例的分离,用例和基础请求方法的分离,利用详细的日志和allure框架提供的丰富的测试报告,是学习python接口自动化测试的良心宝库。
本人通过该项目熟悉了python的基础语法和Requests库的应用,了解了pytest框架和allure工具的应用。为了方便以后复习借鉴,特将其精华代码摘录如下。
封装Http接口
class RestClient():
def __init__(self, api_root_url):
self.api_root_url = api_root_url
self.session = requests.session()
def get(self, url, **kwargs):
return self.request(url, "GET", **kwargs)
def post(self, url, data=None, json=None, **kwargs):
return self.request(url, "POST", data, json, **kwargs)
def put(self, url, data=None, **kwargs):
return self.request(url, "PUT", data, **kwargs)
def delete(self, url, **kwargs):
return self.request(url, "DELETE", **kwargs)
def patch(self, url, data=None, **kwargs):
return self.request(url, "PATCH", data, **kwargs)
/**
**kwargs : 类型,数量可变参数列表,
**/
def request(self, url, method, data=None, json=None, **kwargs):
url = self.api_root_url + url
#读取headers等参数
headers = dict(**kwargs).get("headers")
params = dict(**kwargs).get("params")
files = dict(**kwargs).get("params")
cookies = dict(**kwargs).get("params")
self.request_log(url, method, data, json, params, headers, files, cookies)
#调用Request方法
if method == "GET":
return self.session.get(url, **kwargs)
if method == "POST":
return requests.post(url, data, json, **kwargs)
if method == "PUT":
if json:
# PUT 和 PATCH 中没有提供直接使用json参数的方法,因此需要用data来传入
data = complexjson.dumps(json)
return self.session.put(url, data, **kwargs)
if method == "DELETE":
return self.session.delete(url, **kwargs)
if method == "PATCH":
if json:
data = complexjson.dumps(json)
return self.session.patch(url, data, **kwargs)
封装Python接口
#获取项目根目录
BASE_PATH = os.path.dirname(os.path.dirname(os.path.realpath(__file__)))
#获取setting.ini配置文件
data_file_path = os.path.join(BASE_PATH, "config", "setting.ini")
#读取根地址
api_root_url = data.load_ini(data_file_path)["host"]["api_root_url"]
#User类继承RestClient类,可调用请求方法
class User(RestClient):
def __init__(self, api_root_url, **kwargs):
super(User, self).__init__(api_root_url, **kwargs)
def list_all_users(self, **kwargs):
return self.get("/users", **kwargs)
def list_one_user(self, username, **kwargs):
return self.get("/users/{}".format(username), **kwargs)
def register(self, **kwargs):
return self.post("/register", **kwargs)
def login(self, **kwargs):
return self.post("/login", **kwargs)
def update(self, user_id, **kwargs):
return self.put("/update/user/{}".format(user_id), **kwargs)
def delete(self, name, **kwargs):
return self.post("/delete/user/{}".format(name), **kwargs)
封装关键字
对python接口进一步封装,获取请求响应结果
def get_all_user_info():
"""
获取全部用户信息
:return: 自定义的关键字返回结果 result
"""
result = ResultBase()
res = user.list_all_users()
result.success = False
if res.json()["code"] == 0:
result.success = True
else:
result.error = "接口返回码是 【 {} 】, 返回信息:{} ".format(res.json()["code"], res.json()["msg"])
result.msg = res.json()["msg"]
result.response = res
return result
def login_user(username, password):
"""
登录用户
:param username: 用户名
:param password: 密码
:return: 自定义的关键字返回结果 result
"""
result = ResultBase()
payload = {
"username": username,
"password": password
}
header = {
"Content-Type": "application/x-www-form-urlencoded"
}
res = user.login(data=payload, headers=header)
result.success = False
if res.json()["code"] == 0:
result.success = True
result.token = res.json()["login_info"]["token"]
else:
result.error = "接口返回码是 【 {} 】, 返回信息:{} ".format(res.json()["code"], res.json()["msg"])
result.msg = res.json()["msg"]
result.response = res
logger.info("登录用户 ==>> 返回结果 ==>> {}".format(result.response.text))
return result
封装用例
- 调用关键字方法,对响应信息进行断言
- 利用allure工具注释接口测试步骤,输出完善报告
- 利用pytest搭建测试用例结构,进行测试数据参数化
api_test:
@allure.step("步骤1 ==>> 获取所有用户信息")
def step_1():
logger.info("步骤1 ==>> 获取所有用户信息")
@allure.step("步骤1 ==>> 获取某个用户信息")
def step_2(username):
logger.info("步骤1 ==>> 获取某个用户信息:{}".format(username))
@allure.severity(allure.severity_level.TRIVIAL)
@allure.epic("针对单个接口的测试")
@allure.feature("获取用户信息模块")
class TestGetUserInfo():
"""获取用户信息模块"""
@allure.story("用例--获取全部用户信息")
@allure.description("该用例是针对获取所有用户信息接口的测试")
@allure.issue("https://www.cnblogs.com/wintest", name="点击,跳转到对应BUG的链接地址")
@allure.testcase("https://www.cnblogs.com/wintest", name="点击,跳转到对应用例的链接地址")
@pytest.mark.single
@pytest.mark.parametrize("except_result, except_code, except_msg",
api_data["test_get_all_user_info"])
def test_get_all_user_info(self, except_result, except_code, except_msg):
logger.info("*************** 开始执行用例 ***************")
step_1()
result = get_all_user_info()
# print(result.__dict__)
assert result.response.status_code == 200
assert result.success == except_result, result.error
logger.info("code ==>> 期望结果:{}, 实际结果:{}".format(except_code, result.response.json().get("code")))
assert result.response.json().get("code") == except_code
assert except_msg in result.msg
logger.info("*************** 结束执行用例 ***************")
@allure.story("用例--获取某个用户信息")
@allure.description("该用例是针对获取单个用户信息接口的测试")
@allure.issue("https://www.cnblogs.com/wintest", name="点击,跳转到对应BUG的链接地址")
@allure.testcase("https://www.cnblogs.com/wintest", name="点击,跳转到对应用例的链接地址")
@allure.title("测试数据:【 {username},{except_result},{except_code},{except_msg} 】")
@pytest.mark.single
#测试数据参数化配置
@pytest.mark.parametrize("username, except_result, except_code, except_msg",
api_data["test_get_get_one_user_info"])
def test_get_get_one_user_info(self, username, except_result, except_code, except_msg):
logger.info("*************** 开始执行用例 ***************")
step_2(username)
result = get_one_user_info(username)
# print(result.__dict__)
assert result.response.status_code == 200
assert result.success == except_result, result.error
logger.info("code ==>> 期望结果:{}, 实际结果:【 {} 】".format(except_code, result.response.json().get("code")))
assert result.response.json().get("code") == except_code
assert except_msg in result.msg
logger.info("*************** 结束执行用例 ***************")
if __name__ == '__main__':
pytest.main(["-q", "-s", "test_01_get_user_info.py"])
scenario_test:
@allure.step("步骤1 ==>> 注册用户")
def step_1(username, password, telephone, sex, address):
logger.info("步骤1 ==>> 注册用户 ==>> {}, {}, {}, {}, {}".format(username, password, telephone, sex, address))
@allure.step("步骤2 ==>> 登录用户")
def step_2(username):
logger.info("步骤2 ==>> 登录用户:{}".format(username))
@allure.step("步骤3 ==>> 获取某个用户信息")
def step_3(username):
logger.info("步骤3 ==>> 获取某个用户信息:{}".format(username))
@allure.severity(allure.severity_level.CRITICAL)
@allure.epic("针对业务场景的测试")
@allure.feature("场景:用户注册-用户登录-查看用户")
class TestRegLogList():
@allure.story("用例--注册/登录/查看--预期成功")
@allure.description("该用例是针对 注册-登录-查看 场景的测试")
@allure.issue("https://www.cnblogs.com/wintest", name="点击,跳转到对应BUG的链接地址")
@allure.testcase("https://www.cnblogs.com/wintest", name="点击,跳转到对应用例的链接地址")
@allure.title("用户注册登录查看-预期成功")
@pytest.mark.multiple
@pytest.mark.usefixtures("delete_register_user")
def test_user_register_login_list(self, testcase_data):
username = testcase_data["username"]
password = testcase_data["password"]
telephone = testcase_data["telephone"]
sex = testcase_data["sex"]
address = testcase_data["address"]
except_result = testcase_data["except_result"]
except_code = testcase_data["except_code"]
except_msg = testcase_data["except_msg"]
logger.info("*************** 开始执行用例 ***************")
#调用多个python接口执行scenario
result = register_user(username, password, telephone, sex, address)
step_1(username, password, telephone, sex, address)
assert result.success is True, result.error
result = login_user(username, password)
step_2(username)
assert result.success is True, result.error
result = get_one_user_info(username)
step_3(username)
assert result.success == except_result, result.error
logger.info("code ==>> 期望结果:{}, 实际结果:【 {} 】".format(except_code, result.response.json().get("code")))
assert result.response.json().get("code") == except_code
assert except_msg in result.msg
logger.info("*************** 结束执行用例 ***************")
if __name__ == '__main__':
pytest.main(["-q", "-s", "test_01_register_login_list.py"])
读取测试数据
测试数据示例:
base_data.yml
init_admin_user:
username: "wintest"
password: "123456"
init_sql:
insert_delete_user:
- "INSERT INTO user(username, password, role, sex, telephone, address) VALUES('测试test', '123456', '1', '1', '13488888888', '北京市海淀区')"
- "DELETE FROM user WHERE username = '测试test'"
delete_register_user: "DELETE FROM user WHERE username = '测试test'"
update_user_telephone: "UPDATE user SET telephone = '13500010004' WHERE id = 4"
读取代码
conftest.py
BASE_PATH = os.path.dirname(os.path.dirname(os.path.realpath(__file__)))
def get_data(yaml_file_name):
try:
data_file_path = os.path.join(BASE_PATH, "data", yaml_file_name)
#读取yaml方法
yaml_data = data.load_yaml(data_file_path)
except Exception as ex:
pytest.skip(str(ex))
else:
return yaml_data
base_data = get_data("base_data.yml")
api_data = get_data("api_test_data.yml")
scenario_data = get_data("scenario_test_data.yml")
读取yaml方法:
read_data.py
def load_yaml(self, file_path):
logger.info("加载 {} 文件......".format(file_path))
with open(file_path, encoding='utf-8') as f:
data = yaml.safe_load(f)
logger.info("读到数据 ==>> {} ".format(data))
return data
def load_json(self, file_path):
logger.info("加载 {} 文件......".format(file_path))
with open(file_path, encoding='utf-8') as f:
data = json.load(f)
logger.info("读到数据 ==>> {} ".format(data))
return data
def load_ini(self, file_path):
logger.info("加载 {} 文件......".format(file_path))
config = MyConfigParser()
config.read(file_path, encoding="UTF-8")
data = dict(config._sections)
# print("读到数据 ==>> {} ".format(data))
return data
日志器
import logging, time, os
BASE_PATH = os.path.dirname(os.path.dirname(os.path.realpath(__file__)))
# 定义日志文件路径
LOG_PATH = os.path.join(BASE_PATH, "log")
if not os.path.exists(LOG_PATH):
os.mkdir(LOG_PATH)
class Logger():
def __init__(self):
self.logname = os.path.join(LOG_PATH, "{}.log".format(time.strftime("%Y%m%d")))
self.logger = logging.getLogger("log")
self.logger.setLevel(logging.DEBUG)
#日志格式:如,2016-10-09 19:12:08,289 - __main__ - DEBUG - Do something
self.formater = logging.Formatter(
'[%(asctime)s][%(filename)s %(lineno)d][%(levelname)s]: %(message)s')
#同时输出到控制台和文件
self.filelogger = logging.FileHandler(self.logname, mode='a', encoding="UTF-8")
self.console = logging.StreamHandler()
self.console.setLevel(logging.DEBUG)
self.filelogger.setLevel(logging.DEBUG)
self.filelogger.setFormatter(self.formater)
self.console.setFormatter(self.formater)
self.logger.addHandler(self.filelogger)
self.logger.addHandler(self.console)
logger = Logger().logger
if __name__ == '__main__':
logger.info("---测试开始---")
logger.debug("---测试结束---")