可直接落地的pytest+request+allure接口自动化框架

一、框架结构简介

框架采用python3+pytest+request+allure搭建,需要有一定代码基础方可读懂,代码内每个方法都加有注释方便理解,该框架相对比较完善,可直接落地公司项目进行使用,也可根据公司项目情况继续完善开发。

框架目录结构:

  • common 用于存放公共方法的包,比较核心
  • config 用于存放配置文件
  • img 用于存放图片文件
  • report 用于存放测试报告
  • testcases 用于存放测试用例
  • token_dir 用于存放接口所使用的token,避免重复调用登录接口

image.png

二、config_yaml配置文件

为了框架使用起来更加方便,更加便捷,更加好维护,这里我们使用yaml来把一些需要经常修改的内容放到里面
我这里目前存放了接口的账号密码、测试环境域名、数据库,有其他需要可以继续进行添加补充
文件内容结构如下:

# key可以照着这个来,value都是假的(*^▽^*),根据自己项目来
# 内容暂时这么多,格式需要固定这个,方便后面读取,具体怎么用下面会讲到

user:
  york:
    username: 166xxxxxxxx
    password: xxxxxxxxxxxxxxxx
  laoyan:
    username: 188xxxxxxxx
    password: xxxxxxxxxxxxxxxx

test_url: https://www.baidu.com

mysql:
  db: test_system
  host: 666.888.999.555
  password: 123456
  port: 3306
  user: root

三、common包公共方法

common包方法简介:

  • commom_requests.py 用于接口请求, 核心方法
  • deal_with_response.py 处理allure测试报告,为报告增加附件信息
  • login.py 于处理登录接口,储存token
  • mysql_operate.py 处理数据库,操作数据库增改查
  • tools.py 处理文件路径,拼接文件路径获取项目路径
  • yaml_config.py 读取yaml配置文件

3.1 tools.py 代码讲解

该文件为获取当前项目路径,以及封装拼接接口路径等方法(实现方式不唯一,可以适当调整)
因该文件内方法最为基础,后面都要用到,所以第一步优先编写
代码如下:

# 导入os包
import os

def get_project_path():
    """
    获取项目目录
    :return:
    """
    # api_auto_test为项目名称,可以自行调整
    project_name = "api_auto_test"
    # 获取当前项目路径
    file_path = os.path.dirname(__file__)
    # 因为file_path返回的是当前文件所在位置的目录,而我们需要项目的跟目录
    # 所以这里使用切片,把返回的路径切片到刚好为根目录的地方(方法不唯一)
    a = file_path[:file_path.find(project_name) + len(project_name)]
    return a


def sep(path, add_sep_before=False, add_sep_after=False):
    """
    拼接文件路径,添加系统分隔符
    :param path: 路径列表,类型为数组  ["config","environment.yaml"]
    :param add_sep_before: 是否需要在拼接的路径前加一个分隔符
    :param add_sep_after: 是否需要再拼接的路径后加一个分隔符
    :return:
    """
    # 拼接传入的数组
    all_path = os.sep.join(path)
    # 如果before为TRUE,那就在路径前面加“/”
    if add_sep_before:
        all_path = os.sep + all_path
    # 如果after为TRUE,那就在路径后面加“/”
    if add_sep_after:
        all_path = all_path + os.sep
    return all_path

if __name__ == '__main__':
    # 测试一下
    print(get_project_path())
    print(sep(["config","environment.yaml"], add_sep_before=True))

测试代码结果如下:
很成功,可以看到控制台成功打印出了项目路径,以及使用拼接方法成功拼接了输入的路径,并带有分隔符
image.png

3.2 yaml_config.py 代码讲解

处理完获取路径的方法后,那么紧接着就是要获取配置文件了,配置文件内容在上方第二段已经给出;
那么下面我们根据yaml内容,来编写读取的方法。
代码如下:

# 导出处理yaml文件的包
import yaml
# 导入3.1编写好的tools里的方法
from common.tools import get_project_path, sep


class GetConfig:
    # 使用构造函数,初始化yaml文件,把yaml文件读取出来
    def __init__(self):
        # 用tools里的get_project_path()获取项目路径
        project_path = get_project_path()
        # 使用with——open方法读取yaml文件内容
        # open里的project_path + sep(["config", "environment.yaml")用于把yaml文件路径拼出来
        with open(project_path + sep(["config", "environment.yaml"], add_sep_before=True), "r",
                  encoding="utf-8") as env_file:
            # 使用yaml.load方法把读取出的文件转化为列表或字典,方便后续取值
            # Loader=yaml.FullLoader意思为加载完整的YAML语言,避免任意代码执行
            self.env = yaml.load(env_file, Loader=yaml.FullLoader)
    
    def get_username_password(self, user):
        """
        读取配置文件里的账号密码
        :param user: 需要取哪一个账号的就输入对应的名称,比如我想去york的账密,user就传“york”
        :return: 
        """
        # 直接return出来对应的账号密码
        return self.env["user"][f"{user}"]["username"], self.env["user"][f"{user}"]["password"]

    def get_url(self):
        """
        测试地址
        :return:
        """
        # 直接return出来对应的测试域名
        return self.env["url"]

    def get_mysql_config(self):
        """
        获取数据库配置
        :return:
        """
        # 直接return出来对应yaml里的数据库参数,输出字典
        return self.env["mysql"]


# 测试一下
if __name__ == "__main__":
    getConfig = GetConfig()
    print(getConfig.get_username_password("york"))
    print(getConfig.get_url())
    print(getConfig.get_mysql_config())

输出结果如下:
可以看到想要拿到的东西都读取到了,后面用起来直接调用对应方法即可,方便又快捷,性价比极高。
image.png

3.3 deal_with_response.py 代码讲解

因下面在封装request的时候,需要用到对测试报告的处理,所以这里优先讲一下这里;
该文件主要是用来把接口请求的一些必要的信息,显示到allure报告里,查看报告的时候更容易查看接口的具体情况,可以看下加上这段代码的报告下过,如下图所示:

可以看到报告内圈住地方有七行信息,分别对应下面我们要写的七个方法;
代码如下:

# 导入allure
import allure


def deal_with_res(data, res):
    # 主要用到了allure.attach,在接口请求时可以把必要的信息存放到报告里查看
    # 一一把需要显示的内容获取到,然后使用attach存放到报告
    # 方法里的res就是后面接口请求的内容,data就算是入参报文
    
    # 请求的url
    request_url = str(res.request.url)
    allure.attach(request_url, "请求的url")

    # 请求的方法
    request_method = str(res.request.method)
    allure.attach(request_method, "请求的方法")

    # 请求的headers
    request_headers = str(res.request.headers)
    allure.attach(request_headers, "请求的headers")

    # 入参报文
    request_data = str(data)
    allure.attach(request_data, "入参报文")

    # 响应时间
    response_time = str(res.elapsed.total_seconds() * 1000)
    allure.attach(response_time, "响应时间")

    # 状态码
    status_code = str(res.status_code)
    allure.attach(status_code, "状态码")

    # 响应报文
    response_text = str(res.text)
    allure.attach(response_text, "响应报文")

3.4 commom_requests.py 代码讲解

下面到了整个框架最为核心的代码了,这个文件主要用于封装requests接口请求的方法;
目前我只封装了post和get方法,像put、delete等用的比较少,暂时没写,可自行添加;
框架主要用到了requests和adapters,前者不用多说,后者是用来处理当接口请求失败了,可自动重试,具体的可以看下源码或者网上的讲解,不具体讲了。
代码如下:

# 导入requests
import requests
# 导入adapters,处理接口重试
from requests.adapters, import HTTPAdapter
# 导入前面写的两个方法
from common.yaml_config import GetConfig
from common.deal_with_response import deal_with_res


class Requests:
    # 构造函数,初始化session,封装requests
    def __init__(self, headers=None, timeout=None):
        """
        封装requests方法
        :param headers:接口的header
        :param timeout:如果需要设置设置超时时间就传,默认None
        """
        self.s = requests.Session()
        # 在session实例上挂载adapter实例,目的就是请求异常时,自动重试
        self.s.mount("http://", HTTPAdapter(max_retries=3))
        self.s.mount("https://", HTTPAdapter(max_retries=3))

        # 公共请求头设置,把对应的值设置好
        self.s.headers = headers
        self.timeout = timeout
        # 调用获取yaml里的url,把测试域名拿出来,下面做拼接接口用
        self.url = GetConfig().get_url()

    def get_request(self, url, params=None):
        """
        GET方法封装
        :param url: 接口地址
        :param params: 一般GET的参数都是放在URL里面
        :return:
        """
        # 可以看到用yaml里的self.url加上接口路径,就是完整的接口了
        # 后面要测试uat或者生产环境直接的话直接改yaml里面的域名就好了
        res = self.s.get(self.url + url, params=params, timeout=self.timeout)
        # 调用处理报文的方法,把接口信息加入到测试报告
        deal_with_res(params, res)
        return res

    def post_request(self, url, data=None, json=None):
        """
        POST方法封装
        :param url: 接口地址
        :param data: 参数放在表单中
        :param json: 参数放在请求体中,一般是json
        :param headers:
        :return:
        """
        # 如果传入的是表单,那接口就传data,适用一些接口是form-data格式的
        if data:
            res = self.s.post(self.url + url, data=data, timeout=self.timeout)
            # 调用处理报文的方法,把接口信息加入到测试报告
            deal_with_res(data, res)
            return res
        # 如果传入的json,就传入json,适用大部分接口
        if json:
            res = self.s.post(self.url + url, json=json, timeout=self.timeout)
            # 调用处理报文的方法,把接口信息加入到测试报告
            deal_with_res(json, res)
            return res
        # 有些post接口是什么也不传的,兼容这种情况
        res = self.s.post(self.url + url, timeout=self.timeout)
        # 调用处理报文的方法,把接口信息加入到测试报告
        deal_with_res(json, res)
        return res

    # 魔法函数
    def __del__(self):
        """
        当实例被销毁时,释放掉session所持有的连接
        :return:
        """
        if self.s:
            self.s.close()
# 测试一下下
if __name__ == '__main__':
    # 这里域名设置的是http://httpbin.org,懂得都懂
    get_res = Requests().get_request("/get")
    post_res = Requests().post_request("/post")
    print(get_res.text, "\n", post_res.text, "\n")

执行结果:
这里测试用的是http://httpbin.org,这个网站提供了各种各样的测试接口,用来练习很不错
可以看到,都通了,没毛病
image.png

3.5 mysql_operate.py 代码讲解

在日常工作中,有很多场景,需要接口请求后与数据库里的数据做对比。
因此我们这里直接封装一套兼并数据查、改、增的方法,为什么没有删呢,因为删库容易出事ヽ(ー_ー)ノ,而且很多公司都不会给测试放删库的权限,dddd(懂得都懂),因此就不加删的方法了,需要的话可以自行添加。
下面上代码:

"""
封装数据库的增改查方法
"""
# 导入处理数据库的包
import pymysql
# 老样子,导入获取yaml的方法来读取数据库配置信息
from common.yaml_config import GetConfig


class MysqlOperate:
    # 初始化数据库,把数据库字段映射上
    def __init__(self):
        # 获取到yaml里的数据库配置信息
        mysql_config = GetConfig().get_mysql_config()
        # 把获取的值一一对应上
        self.host = mysql_config['host']
        self.db = mysql_config['db']
        self.port = mysql_config['port']
        self.user = mysql_config['user']
        self.password = mysql_config['password']
        self.conn = None
        self.cursor = None

    # 数据库建立连接
    def __conn_db(self):
        # 用到try,因为有时候会出现数据库连接失败的情况,
        try:
            self.conn = pymysql.connect(
                host=self.host,
                user=self.user,
                password=self.password,
                db=self.db,
                port=self.port,
                charset='utf8'
            )
        except Exception as e:
            print(e)
            return False
        self.cur = self.conn.cursor()
        return True

    # 关闭数据库连接,随手关门养成好习惯
    def __close_conn(self):
        self.cur.close()
        self.conn.close()
        return True

    # 增、改后要commit一下,提交到数据库
    def __commit(self):
        self.conn.commit()
        return True

    def query(self, sql):
        """
        查询数据库
        :param sql: sql查询语句
        :return:
        """
        # 建立连接
        self.__conn_db()
        # 操作数据库查询
        self.cur.execute(sql)
        query_data = self.cur.fetchall()
        # 如果查询的是空,就返回None
        if query_data == ():
            query_data = None
            print("没有获取到数据")
        # 不是空就继续往下走
        else:
            pass
        # 关闭数据库连接
        self.__close_conn()
        # return出查询的接口
        return query_data
    
    def insert_update_table(self, sql):
        """
        插入数据或者修改数据
        :param sql: 增、改sql语句
        :return:
        """
        # 建立连接
        self.__conn_db()
        # 执行sql
        self.cur.execute(sql)
        # commit一下
        self.__commit()
        # 关闭数据库连接
        self.__close_conn()
        # return出查询的接口
        return True
        

这里就不做测试了,大家可以用自己公司的测试库测试一下,我试过了,没毛病的(^_−)☆。

3.6 login.py 代码讲解

这个方法比较简单,用意也很简单,就是因为登录接口很多地方都会依赖,所以干脆封装起来,后面用着方便。
如果有一些其他的,调用频繁的接口,也可以放到这个文件里。
上代码:

# 导入获取yaml方法
from common.yaml_config import GetConfig
# 导入封装好的request
from common.commom_requests import Requests


def login(user):
    """
    封装登录接口
    :param user: yaml文件里账号密码的用户名称
    :return: 
    """
    # 取出账号密码
    username, password = GetConfig().get_username_password(user)
    # 赋值给登录接口的入参
    login_data = {
        "name": f"{username}",
        "pwd": f"{password}"
    }
    # 执行接口请求
    login_res = Requests().post_request("/login", data=login_data)
    # 返回出参
    return login_res

# 测试一下,道友们可以用自己公司系统测试
if __name__ == '__main__':
    print(login("york").json())

执行结果如下:
非常完美,后面再用到登录直接掉就完事了
image.png

3.7 总结

好的,到这里我们的整个common包就完成编写了,整个common是我们这套框架的发动机,下面的测试案例编写都会用到这些,具体的用法,下面我会编写一些样例测试case,可供参考;
目前这些方法是我能想到的了,后面如果大家有补充,也可以自行添加。

四、接口测试案例

我们的测试案例使用的是pytest测试框架,为什么要用pytest呢?
pytest是一个非常成熟的全功能的Python测试框架,主要有以下几个特点:

  • 简单灵活,容易上手;
  • 支持参数化;
  • 能够支持简单的单元测试和复杂的功能测试,还可以用来做selenium/appnium等自动化测试、接口自动化测试(pytest+requests);
  • pytest具有很多第三方插件,并且可以自定义扩展,比较好用的如pytest-selenium(集成selenium)、pytest-html(完美html测试报告生成)、pytest-rerunfailures(失败case重复执行)、pytest-xdist(多CPU分发)等;
  • 测试用例的skip和xfail处理;
  • 可以很好的和jenkins集成;
  • report框架----allure 也支持了pytest;

pytest的用法及其丰富,后面我会单独给出介绍,这里就不细说了,大家也可以自行翻阅网上的资料

4.1 pytest框架之conftest.py&fixture夹具

这里我要单独介绍一下pytestconftest,他为后续的测试提供很大的编写,一些公用的前置或后置测试案例都可以放到这里面,pytest会自动读取引用,配合fixture简直是好用到爆
conftest的特性

  • conftest是可以跨文件使用的
  • conftest.py这个文件名是固定的,不能更改
  • 就近原则 如果同级目录有,就引用同级目录的conftest文件,如果没有就向上级查找
  • conftest不能被其他文件导入
  • conftest可以设置多个pytest内置的钩子函数

4.2 conftest代码编写

在日常做测试工作中,有很多接口是依赖登录接口返回的token的。
那如果每个案例都都调用一下登录接口的话,那代码也太冗余了。
如果测试用例数量很庞大的话,一般都会用到pytest-xdist插件,来分布式执行测试案例,那这时候对登录接口会造成一定的压力。
所以这里封装一个存储token的方法,存放到夹具里,逻辑是先判断存储token的文件是否存在,如果存在就直接取用token,如果不存在那就调用登录接口,取出token,生成文件,存放到文件里。
后面每个案例执行的时候,都会按上述逻辑走一遍,有token就直接取,没token再调登录接口,并发拿到的token存起来,其他案例就直接取。
conftest文件需要存放在testcases文件夹里,后续测试案例也需要放在这个文件夹里。
上代码:

# 导入各种包,不一一介绍了,下面代码都要有用到的
import os
import json
import pytest
# 读取数据库的方法也可加到夹具里,我没加
from common.mysql_operate import MysqlOperate
from common.login import login
from common.tools import sep, get_project_path


# pytest的精髓,夹具fixture,效果类似setup
@pytest.fixture()
def token():
    def _token(user):
        # 判断存放token文件的文件夹是否存在,不存在则自动创建
        token_json_dir = sep([get_project_path(), "token_dir"])
        if not os.path.exists(token_json_dir):
            os.mkdir(token_json_dir)

        # 生成用户user对应token的json文件
        token_json_path = sep([token_json_dir, user + "_token.json"])
        # 若文件不存在,调用登录接口,并把token写入json文件
        if not os.path.exists(token_json_path):
            print(f"{user}对应的token的json文件不存在,调用登录接口")
            # 调用登录方法,拿到token,每个系统的token字段名不一样,自行修改
            token = login(user).json()["data"]
            print(f"写入{user}对应token的json文件{token}")
            # 拿到token后,开始生成token文件,并写入token
            with open(token_json_path,"w+") as write_token:
                # 写入是时候是键值对的形式,方便拿取
                write_token.write(json.dumps({"token": token}))
            # return出token
            return token
        else:
            # 文件存在了,直接取出文件里面的token
            print(f"{user}对应的token_json文件存在,直接取文件token")
            with open(token_json_path, "r") as token_info:
                token = json.loads(token_info.read())
                # 因为token是键值对的形式,需要取一下
                return token["token"]

    return _token

这里就不做测试了,下面附上conftest的工作流程,后面写测试案例的时候会用到这一块的代码。

4.3 测试案例编写

测试案例我们放到testcases文件夹里,需要与conftest在一个文件夹。
这里我拿我们公司的新增用户的接口进行测试,为了保证公司隐私,接口部分我会写成假的,各位自行修改。

# 导入必要的包
import pytest
from common.commom_requests import Requests

# 测试案例必须要以Test开头
class TestAddUser:
    # 敲重点,test_add_user(self, token),看到没,这里的token就是conftest里面的token方法
    # 在写测试案例的时候,如果要用到conftest里面的方法,就直接把方法名带上
    # 代码下方我会付上它的执行流程图
    def test_add_user(self, token):
        # header里需要带入token,这里直接
        header = {
                "Access-Token": token("york")
            }
        # 接口入参,
        json = {
                "phone": "13288889999",
                "title": "测试一下",
                "userName": "测试一下嘿嘿嘿",
            }
        # 调用封装好的request,传入headers。
        # 调用post方法,传入接口路径,方法会自动拼接域名;传入json入参。
        res = Requests(headers=header).post_request("/user/add", json=user_data)
        # 打印一下方便查看
        print(res.json())
        # 做一个简单的断言
        assert res.status_code == 200

执行一下,第一次执行,下面为测试结果:
注意,第一次执行是没有存放token文件的文件夹的,看到没,圈住的,案例自动走了conftest里面的方法,
因为是第一次执行,没有文件夹,就自动创建了文件夹,没有json文件就自动创建了文件。
是不是很酷!!
image.png
生成的文件夹以及文件截图:
image.png

现在已经有现成的token了,不需要再调用登录了,那么我们再执行一次案例,看看效果:
ok,非常酷,逻辑没问题,发现有token,直接拿了token,去执行案例,接口执行成功。
image.png

附上conftest的流程图:

4.4 总结

测试案例基本上千篇一律,可以仿照上面的列子自行添加测试案例。
添加测试案例又是一门很深的学问,可以用到各种pytest自带的类似夹具的方法。
这里就不一一介绍了,网上资料多得很,可以自己查一下。

五、Allure测试报告

5.1 安装allure

  1. 到github上的allure2项目下载zip包,挑一个版本下载,在版本中的Assets里找到zip包下载
    地址:https://github.com/allure-framework/allure2/releases
  2. 把下载的zip包解压缩到python目录的Lib\site-packages,比如windows上在环境变量加上类似这种C:\python3\Lib\site-packages\allure-2.10.0\bin
  3. 在环境变量的path中,把allure的bin目录添加上去
    终端执行 allure --version 检查是否出现版本号,出现则为安装成功
  4. 以上安装的是allure的服务端,还要安装一下allure提供的python包,使用pip install allure-pytest

5.2 使用allure

终端执行命令:
pytest -s testcases\test_add_user.py --alluredir=report
--alluredir=report生成测试报告的目录

根据执行结果,生成测试报告,查看allure报告:
allure generate report -o report/api_report
意思是在report文件下,用执行完用例的结果,生成测试报告,报告存放在api_report文件夹下面
执行结果:
image.png

5.3 allure注释功能

image.png

5.4使用allure生成测试报告

这里我们拿上面的新增用户的案例代码,加入allure的注释代码,来做个例子:

import pytest
import allure
from common.commom_requests import Requests

json = {
    "phone": "13288889999",
    "remark": "测试一下",
    "userName": "测试一下嘿嘿嘿",
}

@allure.description("调用企业管理添加用户接口")
@allure.epic("企业管理")
@allure.feature("用户管理")
@allure.story("添加用户")
@allure.tag("新增")
class TestAddUser:
    @pytest.mark.parametrize("user_data", user_data)
    def test_add_user(self, token, user_data):
        with allure.step("登录获取tolen"):
            header = {
                "Access-Token": token("york")
            }
        with allure.step("调用新增用户接口"):
            res = Requests(headers=header).post_request("/user/add", json=user_data)
            print(res.json())
        with allure.step("断言"):
            assert res.status_code == 200

执行后生成测试报告如下,一一对应报告,如图所示:
是不是这么一搞,报告美观很多。
image.png

六、框架总结

没有总结,你就拿去用吧,一用一个不吱声(o゚▽゚)o !
纯手打,转载注明出处,Thanks♪(・ω・)ノ!

posted @   隔壁老闫  阅读(4374)  评论(1编辑  收藏  举报
相关博文:
阅读排行:
· DeepSeek 开源周回顾「GitHub 热点速览」
· 物流快递公司核心技术能力-地址解析分单基础技术分享
· .NET 10首个预览版发布:重大改进与新特性概览!
· AI与.NET技术实操系列(二):开始使用ML.NET
· 单线程的Redis速度为什么快?
点击右上角即可分享
微信分享提示