Fork me on GitHub

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

封装用例

  1. 调用关键字方法,对响应信息进行断言
  2. 利用allure工具注释接口测试步骤,输出完善报告
  3. 利用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("---测试结束---")

posted @ 2022-01-22 20:24  浩然哉  阅读(599)  评论(0)    收藏  举报
/* 看板娘 */
浏览器标题切换
浏览器标题切换end