一个简单的接口自动化测试框架:Python+Requests+Pytest+Allure

项目结构

project:api_test

------api_keyword

------------api_key.py:接口关键字驱动类

------case

------------test_cases.py:测试套件和测试用例

------report_allure(无需创建 ):allure报告

------result(无需创建 ):测试用例运行结果

------VAR

------------VAR.py:常量类

conftest.py:项目级别fixture

main.py:主函数

1. api_key.py

getattr和eval在接口测试中的应用场景,封装一个接口关键字驱动类ApiKey,作为一个基类,是整个框架的核心,用于提供自动化接口测试的关键字方法。

  1. 各种模拟请求方法:post/get/put/delete/header/...
  2. 集成Allure时,可添加@allure.step,这样在自动化执行的时候 allure报告可以直接捕捉相关的执行信息,让测试报告信息更详细
  3. 进行断言封装

代码实现

python 复制代码
import json
import allure
import jsonpath
import requests
import pymysql
import hashlib
import time
from Crypto.Cipher import AES
import base64
import rsa


class ApiKey:
    # get请求的封装:因为params可能存在无值的情况,存放默认None
    @allure.step("发送get请求")
    def get(self, url, params=None, **kwargs):
        return requests.get(url=url, params=params, **kwargs)

    @allure.step("发送post请求")
    # post请求的封装:data也可能存在无值得情况,存放默认None
    def post(self, url, data=None, **kwargs):
        return requests.post(url=url, data=data, **kwargs)

    @allure.step("获取返回结果字典值")
    # 基于jsonpath获取数据的关键字:用于提取所需要的内容
    def get_text(self, data, key):
        # jsonpath获取数据的表达式:成功则返回list,失败则返回false
        # loads是将json格式的内容转换为字典的格式
        # jsonpath接收的是dict类型的数据
        dict_data = json.loads(data)
        value = jsonpath.jsonpath(dict_data, key)
        if isinstance(value, list):
            return value[0]
        else:
            return value

    @allure.step("断言实际结果等于预期结果")
    def my_assert(self, acutal, expect):
        try:
            assert acutal == expect
        except:
            return "断言失败"
        else:
            return "断言成功"

    # 数据库检查
    @allure.step("数据库检查参数")
    def sqlCheck(self, sql, n):
        conn = pymysql.connect(
            host='shop-xo.hctestedu.com',
            port=3306,
            user='api_test',
            passwd='Aa9999!',
            database='shopxo_hctested',
            charset='utf8')
        # 创建游标
        cmd = conn.cursor()
        # 准备并执行sql语句
        cmd.execute(query=sql)
        # 获取n条查询结果
        results = cmd.fetchmany(n)[0][0]
        conn.close()
        return results

    @allure.step("Md5加密")
    def enMd5(self, text):
        # 获取变量的内存地址,获取加密后的密文值
        return hashlib.md5(text.encode('utf-8')).hexdigest()

    # AES加密填充使用
    def pad(self, text):
        """
        #填充函数,使被加密数据的字节码长度是block_size的整数倍
        """
        length = AES.block_size  # 初始化数据块大小
        count = len(text.encode('utf-8'))
        add = length - (count % length)
        entext = text + (chr(add) * add)
        return entext

    @allure.step("AES加密")
    def enAES(self, key, text):
        global aes
        key = key.encode("utf-8")  # 初始化密钥
        aes = AES.new(key, AES.MODE_ECB)  # 初始化AES,ECB模式的实例,可以选择其他模式
        res = aes.encrypt(self.pad(text).encode("utf8"))
        # Base64是网络上最常见的用于传输8Bit字节码的编码方式之一
        msg = str(base64.b64encode(res), encoding="utf8")
        return msg

    @allure.step("AES解密")
    def deAES(self, text):
        # 截断函数,去除填充的字符
        unpad = lambda date: date[0:-ord(date[-1])]
        res = base64.decodebytes(text.encode("utf8"))
        msg = aes.decrypt(res).decode("utf8")
        return unpad(msg)

    # 秘钥的位数, 可以自定义指定, 例如: 128、256、512、1024、2048等
    @allure.step("生成RSA公钥和私钥")
    def keyRSA(self, num):
        (pubkey, privkey) = rsa.newkeys(num)
        # 生成公钥
        pub = pubkey.save_pkcs1()
        with open('public.pem', 'wb') as f:
            f.write(pub)

        # 生成私钥
        pri = privkey.save_pkcs1()
        with open('private.pem', 'wb') as f:
            f.write(pri)

    @allure.step("RSA加密")
    def enRSA(self, text):
        # 以 utf-8 的编码格式打开指定文件
        f = open("public.pem", encoding="utf-8")
        # 输出读取到的数据
        pub_str = f.read()
        # 关闭文件
        f.close()
        pub_key = rsa.PublicKey.load_pkcs1(pub_str)
        # rsa加密 最后把加密字符串转为base64
        text = text.encode("utf-8")
        cryto_info = rsa.encrypt(text, pub_key)
        cipher_base64 = base64.b64encode(cryto_info)
        cipher_base64 = cipher_base64.decode()
        return cipher_base64

    @allure.step("RSA解密")
    def deRSA(self, text):
        # 以 utf-8 的编码格式打开指定文件
        f = open("private.pem", encoding="utf-8")
        # 输出读取到的数据
        priv_str = f.read()
        # 关闭文件
        f.close()
        priv_key = rsa.PrivateKey.load_pkcs1(priv_str)
        # rsa解密 返回解密结果
        cryto_info = base64.b64decode(text)
        talk_real = rsa.decrypt(cryto_info, priv_key)
        res = talk_real.decode("utf-8")
        return res

    @allure.step("获取签名")
    def getsign(self):  # 获取老签名
        dealkey = [0x07, 0xB6, 0x79, 0x56, 0x7A, 0x5C, 0x4A, 0xBE, 0x1D, 0xF1, 0xB2, 0x10, 0x3C, 0x5E, 0xDC, 0xA6,
                   0x56, 0xE7, 0x88, 0x25, 0x87, 0x95, 0xD5, 0x85, 0x76, 0x7D, 0xEA, 0x66, 0xF5, 0x0A, 0xC3, 0xA8,
                   0x55, 0x28, 0x67, 0x14, 0x06, 0xE7, 0xCB, 0x68, 0xAC, 0x2E, 0x00, 0x36, 0x57, 0x2F, 0xD2, 0xE2,
                   0x54, 0xE9, 0xC6, 0xA3, 0x03, 0xC6, 0x07, 0x33, 0xBD, 0xF1, 0x6D, 0x46, 0x62, 0xFD, 0x82, 0xCF,
                   0xA3, 0x50, 0x15, 0xB2, 0x53, 0xA4, 0x9C, 0x93, 0x98, 0x55, 0x8E, 0xF8, 0xC1, 0x0C, 0x15, 0x71,
                   0x42, 0x6A, 0xA4, 0xF1, 0x5D, 0x72, 0xB1, 0xC4, 0xF6, 0xF0, 0x56, 0xAE, 0xCA, 0x77, 0x44, 0x45,
                   0x21, 0x1B, 0x93, 0x40, 0x49, 0x89, 0x52, 0x76, 0x2C, 0x64, 0xB8, 0x3B, 0xF9, 0x8D, 0x51, 0xA5,
                   0x80, 0x2C, 0x92, 0x39, 0xF7, 0xAD, 0xAF, 0x59, 0x1F, 0x06, 0xDE, 0x5A, 0x1D, 0x91, 0x1C, 0xDB,
                   0x6F, 0xAD, 0xC1, 0xE8, 0xE5, 0xD4, 0xB4, 0x7C, 0x3E, 0x61, 0x73, 0x2D, 0xCE, 0xCD, 0x01, 0xDF,
                   0x5E, 0xCE, 0x60, 0xB7, 0x83, 0xD1, 0x39, 0xA9, 0xF3, 0x35, 0x05, 0xBA, 0x88, 0x78, 0x97, 0xFC,
                   0x3D, 0x2F, 0xF9, 0x36, 0x2A, 0x38, 0xB0, 0x25, 0x16, 0xA7, 0x08, 0x8C, 0xF6, 0x21, 0xC8, 0x22,
                   0xBC, 0x90, 0x48, 0x35, 0x9A, 0x0D, 0x1A, 0xD9, 0xFA, 0xCC, 0x70, 0xAA, 0x42, 0x3F, 0xB6, 0xE1,
                   0xBB, 0x41, 0x17, 0x74, 0xC2, 0x48, 0x7E, 0x80, 0xD6, 0x09, 0xC5, 0x24, 0x60, 0x30, 0x0E, 0xE3,
                   0xFA, 0x92, 0x66, 0x43, 0xE1, 0x8A, 0x4D, 0xD7, 0x1B, 0x6B, 0x23, 0x65, 0xA0, 0x12, 0x9D, 0x9B,
                   0xE0, 0x93, 0xE5, 0xD2, 0xE3, 0xF4, 0xDC, 0x41, 0xA4, 0x3A, 0x10, 0x2B, 0x96, 0xED, 0x1B, 0x1E,
                   0xA9, 0xB4, 0x34, 0x11, 0x94, 0xA6, 0x75, 0x34, 0xD8, 0x89, 0xFC, 0x4F, 0x3B, 0x22, 0xB1, 0xA7]
        # 生成13位整数时间戳
        timestamp = int(time.time() * 1000)
        str1 = str(timestamp) + str('_') + str(dealkey[timestamp % len(dealkey)])
        sign = hashlib.md5(str1.encode('utf-8')).hexdigest()
        return sign, timestamp


if __name__ == '__main__':
    ak = ApiKey()
    # MD5
    print("MD5加密:", ak.enMd5("测试同学"))

    # AES加密和解密
    print("AES加密:", ak.enAES("1234567812345678", "测试同学"))
    print("AES解密:", ak.deAES("CFbJUXkduezgDZ7ZbO+SOw=="))

    # RSA加密
    ak.keyRSA(512)
    print("RSA加密:", ak.enRSA("测试同学"))
    print("RSA解密:", ak.deRSA(ak.enRSA("测试同学")))

    timestamp = int(time.time() * 1000)
    print(timestamp)

2. VAR.py

常量统一管理文件,为了方便代码中识别,目录、文件、常量名全部大写。根据项目中的需要,添加常量到此py文件中。

当前示例用到的几个常量如下:

python 复制代码
# 项目链接
PROJECT_URL = "http://shop-xo.hctestedu.com/index.php?s="
# 公共参数
PARAMS = {
    "application": "app",
    "application_client_type": "weixin"
}
# 用户名
USERNAME = "zz"
# 密码
PASSWD = "123456"

3. test_cases.py

此框架执行的测试用例,全部在py文件中编写。可以根据自己项目的需求,采取在excel编写用例的这种方式,借助openyxl对excel测试用例进行读取和处理

用到了allure,配置及使用教程详见:
Pytest+html,Pytest+allure配置及使用

代码实现

以下给出了几个用例的示例

python 复制代码
import allure
import pytest

from VAR.VAR import PARAMS, PROJECT_URL


@pytest.mark.skip
@allure.feature("用户注册")
@allure.title("test_register_001, 用户名为不超过7位,注册成功")
def test_register_001(token_fix):
    # 从fix中获取预置的工具类和token
    # 所有返回都要获取,不然会报错
    ak, token = token_fix
    data = {
        "accounts": "zz0010",
        "pwd": 123456,
        "type": "username"
    }

    url = PROJECT_URL + "api/user/reg"
    resp = ak.post(url=url, params=PARAMS, json=data)
    # 输出结果
    print(resp.json())
    # 结果断言
    msg = ak.get_text(resp.text, "$..msg")
    assert msg == "注册成功"


@allure.feature("用户注册")
@allure.title("test_register_002, 验证当type输入不存在的类型提示错误信息")
def test_register_002(token_fix):
    # 从fix中获取预置的工具类和token
    # 所有返回都要获取,不然会报错
    ak, token = token_fix
    data = {
        "accounts": "zz0010",
        "pwd": 123456,
        "type": "phone"
    }

    url = PROJECT_URL + "api/user/reg"
    resp = ak.post(url=url, params=PARAMS, json=data)
    # 输出结果
    print(resp.json())
    # 结果断言
    msg = ak.get_text(resp.text, "$..msg")
    assert msg == "注册类型有误"


@allure.feature("登录")
@allure.title("test_login_001, 使用用户名能正确的登录用户")
def test_login_001(token_fix):
    # 从fix中获取预置的工具类和token
    # 所有返回都要获取,不然会报错
    ak, token = token_fix
    data = {
        "accounts": "zz888",
        "pwd": 123456,
        "type": "username"
    }

    url = PROJECT_URL + "api/user/login"
    resp = ak.post(url=url, params=PARAMS, json=data)
    # 输出结果
    print(resp.json())
    # 结果断言
    msg = ak.get_text(resp.text, "$..msg")
    assert msg == "登录成功"


@allure.feature("登录")
@allure.title("test_login_002, 验证输入错误的用户名提示用户")
def test_login_002(token_fix):
    # 从fix中获取预置的工具类和token
    # 所有返回都要获取,不然会报错
    ak, token = token_fix
    data = {
        "accounts": "zz1231231231223",
        "pwd": 123456,
        "type": "username"
    }

    url = PROJECT_URL + "api/user/login"
    resp = ak.post(url=url, params=PARAMS, json=data)
    # 输出结果
    print(resp.json())
    # 结果断言
    msg = ak.get_text(resp.text, "$..msg")
    assert msg == "登录帐号不存在"


@allure.feature("登录")
@allure.title("test_login_003, 验证用户名为空提示用户")
def test_login_003(token_fix):
    # 从fix中获取预置的工具类和token
    # 所有返回都要获取,不然会报错
    ak, token = token_fix
    data = {
        "accounts": "",
        "pwd": 123456,
        "type": "username"
    }

    url = PROJECT_URL + "api/user/login"
    resp = ak.post(url=url, params=PARAMS, json=data)
    # 输出结果
    print(resp.json())
    # 结果断言
    msg = ak.get_text(resp.text, "$..msg")
    assert msg == "登录账号不能为空"

4. conftest.py

项目级fixture,整个项目只初始化一次。在这个示例中,所有测试用例使用的都是一个账号,因此账号信息放置VAR.py中。接口需要鉴权,因此执行每个接口之前,都需要先登录拿到token作为后续接口的参数,再进行相关测试,例如加入购物车、删除商品、查询订单等等。

python 复制代码
import pytest
from VAR.VAR import USERNAME, PASSWD, PROJECT_URL, PARAMS
from api_keyword.api_key import ApiKey


# 项目级fixture,整个项目只初始化一次
@pytest.fixture(scope='session')
def token_fix():
    # 初始化工具类
    ak = ApiKey()
    data = {
        "accounts": USERNAME,
        "pwd": PASSWD,
        "type": "username"
    }
    url = PROJECT_URL + "api/user/login"
    resp = ak.post(url=url, params=PARAMS, json=data)
    # 获取token
    token = ak.get_text(resp.text, '$..token')
    return ak, token

5. main.py

python 复制代码
import os
import pytest

if __name__ == '__main__':
    # 运行某个py文件下指定的testcase
    # pytest.main(['-v', '--alluredir', './result', '--clean-alluredir', './case/test_cases.py::test_login_001'])
    # 运行某个py文件下的所有测试用例
    pytest.main(['-v', '--alluredir', './result', '--clean-alluredir', './case/test_cases.py'])
    # 运行前清除之前旧的report
    os.system('allure generate ./result/ -o ./report_allure/ --clean')

6. 执行所有测试用例

运行main.py文件,将会运行所有测试用例,生成allure测试报告。main.py中的pytest命令,指明了testcase运行结果的目录和allure报告的路径。

所有测试用例运行完成后,会自动生成对应目录。report_allure和result。

7. 查看allure报告

allure从多个维度生成了测试报告,体验很不错。




8. 定时构建

CI/CD的方式,还可以:
使用Jenkins集成Python + Pytest + Allure

9. 框架优化

这个框架还有很多优化的空间~ 有什么好的idea,快去实践一下吧~

相关推荐
尘浮生几秒前
Java项目实战II基于微信小程序的校运会管理系统(开发文档+数据库+源码)
java·开发语言·数据库·微信小程序·小程序·maven·intellij-idea
MessiGo1 分钟前
Python 爬虫 (1)基础 | 基础操作
开发语言·python
Tech Synapse7 分钟前
Java根据前端返回的字段名进行查询数据的方法
java·开发语言·后端
乌啼霜满天24915 分钟前
JDBC编程---Java
java·开发语言·sql
肥猪猪爸25 分钟前
使用卡尔曼滤波器估计pybullet中的机器人位置
数据结构·人工智能·python·算法·机器人·卡尔曼滤波·pybullet
色空大师28 分钟前
23种设计模式
java·开发语言·设计模式
Bruce小鬼41 分钟前
QT文件基本操作
开发语言·qt
2202_754421541 小时前
生成MPSOC以及ZYNQ的启动文件BOOT.BIN的小软件
java·linux·开发语言
我只会发热1 小时前
Java SE 与 Java EE:基础与进阶的探索之旅
java·开发语言·java-ee
LZXCyrus1 小时前
【杂记】vLLM如何指定GPU单卡/多卡离线推理
人工智能·经验分享·python·深度学习·语言模型·llm·vllm