Skip to content

【周末】接口自动化测试

接口自动化测试

预习准备

  • 提前先预习完以下相关的知识,再开始本章节的学习。
  • 提前注册企业微信账号。
专题课 阶段 章节 Python 班级
接口自动化测试 L1 全部录播 录播地址
接口自动化测试 L2 多层嵌套响应断言
接口自动化测试 L2 【实战】宠物商店接口自动化测试实战
接口自动化测试 L3 整体结构响应断言
接口自动化测试 L3 数据库操作与断言

课程目标

  • 掌握接口自动化测试用例设计方法。
  • 掌握接口自动化测试中的各种格式的请求构造与响应断言技巧。
  • 掌握接口自动化测试中复杂断言方法。
  • L1
  • L2
  • L3
  • L4
  • L5

接口自动化测试场景

功能测试和接口测试对比

uml diagram

UI 自动化测试和接口自动化测试对比

uml diagram

接口自动化与 UI 自动化测试对比

UI 自动化测试 接口自动化测试
成本 ⭐️⭐️⭐️ ⭐️
测试效率 ⭐️⭐️ ⭐️⭐️⭐️⭐️⭐️
用例编写效率 ⭐️⭐️ ⭐️⭐️⭐️⭐️⭐️
稳定性 ⭐️⭐️⭐️ ⭐️⭐️⭐️⭐️⭐️
自动化回归测试效率 ⭐️⭐️⭐️ ⭐️⭐️⭐️⭐️⭐️
测试覆盖度 ⭐️⭐️ ⭐️⭐️⭐️⭐️⭐️

知识点总览

点击查看:接口自动化测试知识点梳理.xmind

需求说明

  • 企业微信
    • 企业微信是腾讯微信团队打造的企业通讯与办公工具。
    • 具有与微信一致的沟通体验,丰富的 OA 应用,和连接微信生态的能力。
    • 可帮助企业连接内部、连接生态伙伴、连接消费者。专业协作、安全管理、人即服务。
  • 完成企业微信部门管理接口自动化测试。
  • 环境准备
    1. 企业微信注册。
    2. 企业微信白名单配置步骤:https://ceshiren.com/t/topic/22768

实战思路

uml diagram

需求分析

请求方式:GET/POST(HTTPS)
请求地址:https://qyapi.weixin.qq.com/cgi-bin/gettoken?corpid=ID&corpsecret=SECRET
请求包体:
...
参数说明:
...
权限说明:
...
返回结果:
...
参数说明:
...

接口测试用例设计

编写接口自动化测试脚本

  • 单接口自动化测试
  • 接口业务流程自动化测试
单接口自动化测试
  • 单接口测试:保障单个接口的正确性和健壮性。
  • 场景:对外提供的接口、与第三方联调的接口。
接口鉴权

接口鉴权基本上可以分为两步。

首先需要把认证接口需要的数据放到请求体中,发送给认证接口。成功了之后,认证接口会在响应中返回 token 信息。

然后其他业务接口后面在请求后端服务的时候,都需要带上这个认证信息。

uml diagram

企业微信使用的也是这种方式。

uml diagram

获取企业微信通讯录管理接口 access_token
  1. 获取接口调用凭证-企业ID - Corpid(企业ID) - 每个企业都拥有唯一的 corpid - 查看:管理后台【我的企业】--【企业信息】下查看【企业ID】

  2. 获取接口调用凭证-Secret - Secret(应用的凭证密钥) - 通讯录管理 secret - 查看:【管理工具】--通讯录同步

  3. 代码实现

class TestWeworkToken:

    def test_get_token(self):
        '''
        获取 access_token
        :return:
        '''
        # 定义凭证
        corpid = "xxx"
        corpsecret = "xxx"
        url = f"https://qyapi.weixin.qq.com/cgi-bin/gettoken?corpid={corpid}&corpsecret={corpsecret}"
        # 发送 GET 请求
        r = requests.get(url)
        # 打印响应体
        print(r.text)

    def test_get_token2(self):
        '''
        获取 access_token 方式二
        :return:
        '''
        # 定义凭证
        corpid = "xxx"
        corpsecret = "xxx"
        url = "https://qyapi.weixin.qq.com/cgi-bin/gettoken"
        # 定义请求参数
        params = {
            "corpid": corpid,
            "corpsecret": corpsecret
        }
        # 发送 GET 请求
        r = requests.request("GET", url, params=params)
        # 打印响应体
        print(r.text)
创建部门单接口测试
import requests


class TestWeworkDepartments:

    def setup_class(self):
        # 获取 access_token
        # 定义凭证
        corpid = "xxx"
        corpsecret = "xxx"
        url = "https://qyapi.weixin.qq.com/cgi-bin/gettoken"
        # 定义请求参数
        params = {
            "corpid": corpid,
            "corpsecret": corpsecret
        }
        # 发送 GET 请求
        r = requests.request("GET", url, params=params)
        # 打印响应体
        print(r.text)
        # 获取 access_token
        self.token = r.json().get("access_token")
        print(f"获取到的 token 为 {self.token}")

    def test_create_department(self):
        '''
        创建部门冒烟用例
        '''
        url = f"https://qyapi.weixin.qq.com/cgi-bin/department/create?access_token={self.token}"
        data = {
           "name": "技术部",
           "name_en": "JISHU1",
           "parentid": 1,
           "order": 1,
           "id": 2
        }
        # 发出 POST 请求
        r = requests.request("POST", url, json=data)
        print(r.text)
        # 断言接口状态
        assert r.status_code == 200
        # 断言接口响应
        assert r.json().get("errcode") == 0
参数化实现单接口测试
class TestWeworkDepartments:

    def setup_class(self):
        self.base_url = "https://qyapi.weixin.qq.com"
        # 获取 access_token
        # 定义凭证
        corpid = "xxx"
        corpsecret = "xxx"
        url = f"{self.base_url}/cgi-bin/gettoken"
        # 定义请求参数
        params = {
            "corpid": corpid,
            "corpsecret": corpsecret
        }
        # 发送 GET 请求
        r = requests.request("GET", url, params=params)
        # 打印响应体
        print(r.text)
        # 获取 access_token
        self.token = r.json().get("access_token")
        print(f"获取到的 token 为 {self.token}")
        # 创建部门 url
        self.create_url = f"{self.base_url}/cgi-bin/department/create?access_token={self.token}"

    def test_create_department(self):
        '''
        创建部门冒烟用例
        '''
        data = {
           "name": "技术部",
           "name_en": "JISHU1",
           "parentid": 1,
           "order": 1,
           "id": 2
        }
        # 发出 POST 请求
        r = requests.request("POST", self.create_url, json=data)
        print(r.text)
        # 断言接口状态
        assert r.status_code == 200
        # 断言接口响应
        assert r.json().get("errcode") == 0


    @pytest.mark.parametrize(
        "name, name_en, parentid, order, depart_id, expect",
        [
            ("", "JISHU2", 1, 2, 3, 40058),
            ("j", "JISHU2", 1, 3, 4, 0),
            ("技术部23", "JISHU23", 1, 30, 1, 60123)
        ]
    )
    def test_create_department_by_params(self, name, name_en, parentid, order, depart_id, expect):
        '''
        参数化创建部门
        '''
        data = {
            "name": name,
            "name_en": name_en,
            "parentid": parentid,
            "order": order,
            "id": depart_id
        }
        # 发出 POST 请求
        r = requests.request("POST", self.create_url, json=data)
        print(r.text)
        # 断言接口状态
        assert r.status_code == 200
        # 断言接口响应
        assert r.json().get("errcode") == expect
复杂断言

使用 jsonpath 完成响应断言。

import pytest
import requests
from jsonpath import jsonpath


class TestWeworkDepartments:


    @pytest.mark.parametrize(
        "name, name_en, parentid, order, depart_id, expect",
        [
            ("", "JISHU2", 1, 2, 3, 40058),
            ("j", "JISHU2", 1, 3, 4, 0),
            ("技术部23", "JISHU23", 1, 30, 1, 60123)
        ]
    )
    def test_create_department_by_params(self, name, name_en, parentid, order, depart_id, expect):
        '''
        参数化创建部门
        '''
        data = {
            "name": name,
            "name_en": name_en,
            "parentid": parentid,
            "order": order,
            "id": depart_id
        }
        # 发出 POST 请求
        r = requests.request("POST", self.create_url, json=data)
        print(r.text)
        # 断言接口状态
        assert r.status_code == 200
        # 断言接口响应
        # assert r.json().get("errcode") == expect
        # 通过 jsonpath 断言
        code_list = jsonpath(r.json(), "$.errcode")
        print(code_list, type(code_list))
        assert code_list[0] == expect
课堂练习
  • 获取企业微信接口请求凭证 access token。
  • 完成新增部门单接口用例测试:
    • 冒烟用例
    • 参数化实现单接口测试
  • 完成通讯录部门管理-获取子部门 ID 列表单接口冒烟用例编写。
    • 使用 jsonpath 断言第一个部门的 id 值符合预期。

接口文档:https://developer.work.weixin.qq.com/document/path/91039

接口业务流程测试
  • 接口业务场景测试:
    • 保障通过进行多个接口的串联操作来完成原有需求中提出的业务逻辑。
  • 通讯录新增部门业务场景:
    • 创建部门前,部门已存在,先删除,然后创建。
class TestDepartmentsFlow:
    """
    接口业务场景验证
    """
    def setup_class(self):
        # 定义凭证
        corpid = "xxx"
        corpsecret = "xxx"
        url = "https://qyapi.weixin.qq.com/cgi-bin/gettoken"
        # 定义param
        params = {
            "corpid": corpid,
            "corpsecret": corpsecret
        }
        # 发get请求
        r = requests.request(method="GET", url=url, params=params)
        self.token = r.json()["access_token"]
        assert r.json()["errcode"] == 0

    def test_create_department_exist(self):
        # 1. 准备已存在部门
        # 2. 有存在部门的情况下,创建相同信息的部门失败
        # 3. 删除部门
        # 4. 查询部门删除成功
        # 5. 再次创建部门
        # 6. 查询部门创建成功
课堂练习
  • 完成通讯录部门管理增删改查接口的业务流程测试用例。
    • 创建部门前,部门已存在,先删除,然后创建。

接口文档:https://developer.work.weixin.qq.com/document/path/90205

接口测试框架封装

项目结构

├── apis
│   ├── __init__.py
│   ├── base_api.py
│   ├── department.py
│   └── wework.py
├── config
│   ├── __init__.py
│   └── config.yaml
├── tests
│   ├── __init__.py
│   ├── test_departments.py
│   └── test_department_flow.py
└── utils
    ├── __init__.py
    ├── log_utils.py
    └── utils.py
接口测试框架封装设计
  • ApiObject 设计模式:
    • 封装。
    • 分层。
    • 把实现和测试用例以及断言进行拆分。

uml diagram

框架搭建
token 获取

企业微信特有逻辑,完成 access_token 的获取

import requests


class Wework:

    def __init__(self):
        self.base_url = "https://qyapi.weixin.qq.com"
        self.token = self.get_access_token()

    def get_access_token(self):
        '''
        获取 access_token
        :return:
        '''
        # 定义凭证
        corpid = "xxx"
        corpsecret = "xxx"
        url = f"{self.base_url}/cgi-bin/gettoken"
        # 定义请求参数
        params = {
            "corpid": corpid,
            "corpsecret": corpsecret
        }
        # 发送 GET 请求
        r = requests.request("GET", url, params=params)
        # 打印响应体
        print(r.text)
        # 获取 access_token
        token = r.json().get("access_token")
        return token
业务接口描述

接口信息描述:只关注业务,不需要做断言。每个方法返回接口响应体。

import requests


# 部门需要继承 Wework,这样就可以直接获取 Wework 中获取到的 access_token
class Department(Wework):

    def create(self, data):
        '''
        创建部门
        :return:
        '''
        create_url = f"{self.base_url}/cgi-bin/department/create?access_token={self.token}"
        r = requests.request(method="POST", url=create_url, json=data)
        return r

    def update(self, data):
        '''
        更新部门信息
        :return:
        '''
        update_url = f"{self.base_url}/cgi-bin/department/update?access_token={self.token}"
        r = requests.request(method="POST", url=update_url, json=data)
        return r

    def delete(self, depart_id):
        '''
        删除部门
        :return:
        '''
        delete_url = f"{self.base_url}/cgi-bin/department/delete?access_token={self.token}&id={depart_id}"
        r = requests.request(method="GET", url=delete_url)
        return r

    def get(self):
        '''
        获取子部门 id
        :return:
        '''
        list_url = f"{self.base_url}/cgi-bin/department/simplelist?access_token={self.token}"
        r = requests.request(method="GET", url=list_url)
        return r
创建测试用例
from jsonpath import jsonpath


class TestDepartments:

    def setup_class(self):
        # 实例化部门类
        self.department = Department()
        # 准备测试数据
        self.depart_id = 210
        self.create_data = {
            "name": "广州研发中心",
            "name_en": "RDGZ",
            "parentid": 1,
            "order": 1,
            "id": self.depart_id
        }
        self.update_name = "广州研发中心-update"
        self.update_data = {
            "id": self.depart_id,
            "name": self.update_name
        }

    def test_department_flow(self):
        '''
        部门增删改查场景测试
        '''
        # 创建部门
        r = self.department.create(self.create_data)
        assert r.status_code == 200
        assert r.json().get("errcode") == 0
        # 查询是否创建成功
        r = self.department.get()
        depart_ids = jsonpath(r.json(), "$..id")
        assert self.depart_id in depart_ids
        # 更新部门信息
        r = self.department.update(self.update_data)
        assert r.status_code == 200
        assert r.json().get("errcode") == 0
        # 删除部门
        r = self.department.delete(self.depart_id)
        assert r.status_code == 200
        assert r.json().get("errcode") == 0
        # 查询是否删除成功
        r = self.department.get()
        depart_ids = jsonpath(r.json(), "$..id")
        assert self.depart_id not in depart_ids
底层技术栈优化

把 requests 封装到 BaseApi 中,供框架中其他方法调用。

import requests


class BaseApi:

    def send_api(self, req):
        '''
        对 requests 进行二次封装
        :return: 接口响应
        '''
        r = requests.request(**req)
        return r

接下来把接口信息封装到字典中,这样接口描述中不需要直接调用 requests。

class Wework(BaseApi):

    def __init__(self):
        self.base_url = "https://qyapi.weixin.qq.com"
        self.token = self.get_access_token()

    def get_access_token(self):
        '''
        获取 access_token
        :return:
        '''
        # 定义凭证
        corpid = "xxx"
        corpsecret = "xxx"
        url = f"{self.base_url}/cgi-bin/gettoken"
        req = {
            "method": "GET",
            "url": url,
            "params": {
                "corpid": corpid,
                "corpsecret": corpsecret
            }
        }
        r = self.send_api(req)
        # 打印响应体
        print(r.text)
        # 获取 access_token
        token = r.json().get("access_token")
        return token

部门管理接口修改请求发送方式。

class Department(Wework):

    def create(self, data):
        '''
        创建部门
        :return:
        '''
        create_url = f"{self.base_url}/cgi-bin/department/create?access_token={self.token}"
        req = {
            "method": "POST",
            "url": create_url,
            "json": data
        }
        r = self.send_api(req)
        return r

    def update(self, data):
        '''
        更新部门信息
        :return:
        '''
        update_url = f"{self.base_url}/cgi-bin/department/update?access_token={self.token}"
        # r = requests.request(method="POST", url=update_url, json=data)
        req = {
            "method": "POST",
            "url": update_url,
            "json": data
        }
        r = self.send_api(req)
        return r

    def delete(self, depart_id):
        '''
        删除部门
        :return:
        '''
        delete_url = f"{self.base_url}/cgi-bin/department/delete?access_token={self.token}&id={depart_id}"
        # r = requests.request(method="GET", url=delete_url)
        req = {
            "method": "GET",
            "url": delete_url
        }
        r = self.send_api(req)
        return r

    def get(self):
        '''
        获取子部门 id
        :return:
        '''
        list_url = f"{self.base_url}/cgi-bin/department/simplelist?access_token={self.token}"
        # r = requests.request(method="GET", url=list_url)
        req = {
            "method": "GET",
            "url": list_url
        }
        r = self.send_api(req)
        return r
secret 改造

创建 test_env.yaml,文件中配置和业务强相关的数据。

# 接口自动化测试的基础服务地址抽离
base_url: https://qyapi.weixin.qq.com/cgi-bin/
# 和账户相关的信息
corpid:
  hogwarts: ww876064acebf0fa3c
corp_secret:
  contacts: A7LgEhs_Ty_dYXO9BcgY00eULrOghx4UyjqQoQNLc3o

创建工具类。

import os
import yaml


class Utils:

    @classmethod
    def get_yaml_data(cls, file_path):
        '''
        封装yaml读取
        :param file_path: 文件路径
        :return: 返回yaml数据体
        '''
        with open(file_path, encoding="utf-8") as f:
            datas = yaml.safe_load(f)
        return datas

    @classmethod
    def get_root_path(cls):
        '''
        获取测试框架项目的绝对路径
        :return:
        '''
        path = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
        print(f"项目绝对路径为 {path}")
        return path

修改 wework 中配置数据获取方式。

class Wework(BaseApi):

    def __init__(self):
        self.base_url = "https://qyapi.weixin.qq.com"
        self.token = self.get_access_token()

    def get_access_token(self):
        '''
        获取 access_token
        :return:
        '''
        # 获取配置数据
        self.get_config()
        url = f"{self.base_url}/cgi-bin/gettoken"
        # 1. 把接口信息封装到字典中
        # 2. 接口描述中不需要直接调用 requests
        req = {
            "method": "GET",
            "url": url,
            "params": {
                "corpid": self.corpid,
                "corpsecret": self.corpsecret
            }
        }
        r = self.send_api(req)
        # 打印响应体
        print(r.text)
        # 获取 access_token
        token = r.json().get("access_token")
        return token

    def get_config(self):
        '''
        获取配置数据
        :return:
        '''
        path = f"{Utils.get_root_path()}/config/test_env.yaml"
        yaml_data = Utils.get_yaml_data(path)
        # 获取需要的值
        self.base_url = yaml_data.get("base_url")
        self.corpid = yaml_data.get("corpid").get("hogwarts")
        self.corpsecret = yaml_data.get("corp_secret").get("contacts")
课堂练习
  • 使用 ApiObject 模式完成接口自动化测试框架搭建。
数据库断言
  • 数据库断言:pymysql:
    • 使用场景:验证数据操作的实际结果是否正确。

在工具类中定义数据库查询方法。

import pymysql


class Utils:


    @classmethod
    def query_db(cls, sql, database_info):
        '''
        查询数据库
        :param sql: 要执行的 SQL 语句
        :param database_info: 数据库信息
        :return: 查询结果
        '''
        # 连接数据库
        conn = pymysql.Connect(**database_info)
        # 创建游标
        cursor = conn.cursor()
        # 执行 SQL 语句
        cursor.execute(sql)
        # 获取查询结果
        datas = cursor.fetchall()
        print("查询到的数据为:", datas)  # 获取多条数据
        # 关闭连接
        cursor.close()
        conn.close()
        return datas

在测试用例中使用数据库断言。

class TestDepartments:

    ...

    def test_department_flow(self):
        '''
        部门增删改查场景测试
        '''
        # 创建部门
        ...
        # 更新部门信息
        r = self.department.update(self.update_data)
        assert r.status_code == 200
        assert r.json().get("errcode") == 0
        # 使用数据库查询
        database_info = {
            "host": "127.0.0.1",
            "port": 3306,
            "database": "contacts",
            "user": "root",
            "password": "hogwarts",
            "charset": "utf8"
        }
        datas = Utils.query_db("select name from departments where id=210", database_info)
        print(f"数据库查询结果 {datas[0][0]}")
        assert datas[0][0] == self.update_name
        # 删除部门
        ...
数据清理

现在的用例还存在问题,例如如果前面已经有创建好的部门的话,可能就会发生数据冲突,那么执行用例的时候就会报错。最好的方法就是要先把测试环境的数据清理好。在 pytest 中数据清理逻辑可以放在测试装置中。

在部门管理接口描述中添加清理数据方法。

class Department(Wework):

    ...

    def clear(self):
        '''
        清理已经存在的部门信息
        '''
        # 查询目前存在的部门
        r = self.get()
        # 提取部门 id 的列表
        id_list = jsonpath(r.json(), "$..id")
        # id 为 1 的部门是最基础的父部门,不可以删除
        for i in id_list:
            if i != 1:
                # 调用删除部门的接口清除所有部门数据
                self.delete(i)

在测试用例的 setup 中添加清理数据步骤。

class TestDepartment:

    def setup_class(self):

        # 清除部门数据
        self.department.clear()
日志添加
  • 应用场景:报告中添加详细的日志信息,有助于分析定位问题。
  • 解决方案:
    • 使用 python 自带的 logging 模块生成日志,日志会自动添加到测试报告中。
# 创建一个日志模块: log_util.py
import logging
import os

from logging.handlers import RotatingFileHandler

# 绑定绑定句柄到logger对象
logger = logging.getLogger(__name__)
# 获取当前工具文件所在的路径
root_path = os.path.dirname(os.path.abspath(__file__))
# 拼接当前要输出日志的路径
log_dir_path = os.sep.join([root_path, f'/logs'])
if not os.path.isdir(log_dir_path):
    os.mkdir(log_dir_path)
# 创建日志记录器,指明日志保存路径,每个日志的大小,保存日志的上限
file_log_handler = RotatingFileHandler(os.sep.join([log_dir_path, 'log.log']), maxBytes=1024 * 1024, backupCount=10 , encoding="utf-8")
# 设置日志的格式
date_string = '%Y-%m-%d %H:%M:%S'
formatter = logging.Formatter(
    '[%(asctime)s] [%(levelname)s] [%(filename)s]/[line: %(lineno)d]/[%(funcName)s] %(message)s ', date_string)
# 日志输出到控制台的句柄
stream_handler = logging.StreamHandler()
# 将日志记录器指定日志的格式
file_log_handler.setFormatter(formatter)
stream_handler.setFormatter(formatter)
# 为全局的日志工具对象添加日志记录器
# 绑定绑定句柄到logger对象
logger.addHandler(stream_handler)
logger.addHandler(file_log_handler)
# 设置日志输出级别
logger.setLevel(level=logging.INFO)

在框架中需要添加日志信息的地方,加上日志。

class BaseApi:


    def send_api(self, req):
        '''
        对 requests 进行二次封装
        :return: 接口响应结果
        '''
        logger.debug(f"请求数据为==========>{req}")
        r = requests.request(**req)
        logger.debug(f"响应数据为==========>{r.text}")
        return r
生成测试报告

添加 Allure 报告描述。

@allure.feature("部门管理")
class TestDepartments:

    ...

    @allure.story("部门管理业务场景")
    @allure.title("部门增删改查场景测试")
    def test_department_flow(self):
        '''
        部门增删改查场景测试
        '''
        # 创建部门
        with allure.step("创建部门"):
            r = self.department.create(self.create_data)
        with allure.step("断言创建部门成功"):
            assert r.status_code == 200
            assert r.json().get("errcode") == 0
            # 查询是否创建成功
            r = self.department.get()
            depart_ids = jsonpath(r.json(), "$..id")
            assert self.depart_id in depart_ids
        # 更新部门信息
        with allure.step("更新部门"):
            r = self.department.update(self.update_data)
        with allure.step("断言更新部门成功"):
            assert r.status_code == 200
            assert r.json().get("errcode") == 0
            # 使用数据库查询
            database_info = {
                "host": "127.0.0.1",
                "port": 3306,
                "database": "contacts",
                "user": "root",
                "password": "hogwarts",
                "charset": "utf8"
            }
            datas = Utils.query_db("select name from departments where id=210", database_info)
            print(f"数据库查询结果 {datas[0][0]}")
            assert datas[0][0] == self.update_name
        # 删除部门
        with allure.step("删除部门"):
            r = self.department.delete(self.depart_id)
        with allure.step("断言更删除部门成功"):
            assert r.status_code == 200
            assert r.json().get("errcode") == 0
            # 查询是否删除成功
            r = self.department.get()
            depart_ids = jsonpath(r.json(), "$..id")
            assert self.depart_id not in depart_ids

执行命令生成 Allure 报告。

# 执行测试,搜集测试结果
pytest --alluredir=./results --clean-alluredir

# 生成在线的测试报告
allure serve ./results

# 生成静态资源
allure generate --clean report/html report -o report/html
课堂练习
  • 优化 token 获取。
  • 添加日志。
  • 完成 allure 报告信息添加。
  • 完成 allure 报告生成。

接口自动化测试框架优化

数据驱动优化

准备测试数据文件 departments_data.yaml

- name: 技术部
  name_en: JISHU1
  parentid: 1
  order: 1
  id: 2
  expect: 0
- name: ''
  name_en: JISHU2
  parentid: 1
  order: 2
  id: 3
  expect: 40058
- name: j
  name_en: JISHU2
  parentid: 1
  order: 3
  id: 4
  expect: 0
- name: 研发部1t6yujk9osjhynnj890lkmbg54321
  name_en: YANFA
  parentid: 1
  order: 4
  id: 5
  expect: 0
- name: 产品部1t6yujk9osjhynnj890lkmbg54321w
  name_en: JISHU3
  parentid: 1
  order: 5
  id: 6
  expect: 60001
- name: 教学部1
  name_en: ''
  parentid: 1
  order: 6
  id: 7
  expect: 60001
- name: 教学部2
  name_en: J
  parentid: 1
  order: 7
  id: 8
  expect: 0
- name: 教学部3
  name_en: JIAOXUE1234567890poi09iuytghjfr4q11111234567890pi09iytghjfr12345
  parentid: 1
  order: 8
  id: 9
  expect: 0
- name: 教学部4
  name_en: JIAOXUE1234567890poi09iuytghjfr4q11111234567890pi09iytghjfr12345p
  parentid: 1
  order: 9
  id: 10
  expect: 40058
- name: 上级部门
  name_en: Top
  parentid: 1
  order: 12
  id: 4294967295
  expect: 0
- name: 技术部3
  name_en: JISHU3
  parentid: 1
  order: 13
  id: 4294967296
  expect: 40058
- name: 技术部4
  name_en: JISHU4
  parentid: 4294967295
  order: 10
  id: 11
  expect: 0
- name: 技术部5
  name_en: JISHU5
  parentid: 4294967296
  order: 11
  id: 12
  expect: 40058
- name: 行政部1
  name_en: XINGZHENG1
  parentid: 1
  order: 0
  id: 4294967294
  expect: 0
- name: 行政部2
  name_en: XINGZHENG2
  parentid: 1
  order: 4294967295
  id: 4294967293
  expect: 0
- name: 行政部3
  name_en: XINGZHENG3
  parentid: 1
  order: 4294967296
  id: 4294967292
  expect: 40058
- name: '*?<>|'
  name_en: JISHU8
  parentid: 1
  order: 14
  id: 15
  expect: 60009
- name: 技术部8
  name_en: '*?<>|'
  parentid: 1
  order: 15
  id: 16
  expect: 60009
- name: 技术部9
  name_en: JISHU9
  parentid: 技术部
  order: 16
  id: 17
  expect: 40058
- name: 技术部10
  name_en: JISHU10
  parentid: 1
  order: 技术部
  id: 18
  expect: 40058
- name: 技术部11
  name_en: JISHU11
  parentid: 1
  order: 18
  id: 技术部
  expect: 40058
- name: 技术部
  name_en: JISHU19
  parentid: 1
  order: 26
  id: 27
  expect: 60008
- name: 技术部20
  name_en: JISHU
  parentid: 1
  order: 27
  id: 28
  expect: 0
- name: 技术部21
  name_en: JISHU21
  parentid: 1
  order: 28
  id: 4
  expect: 60008
- name: 技术部22
  name_en: JISHU22
  parentid: 3
  order: 29
  id: 30
  expect: 60004
- name: 技术部23
  name_en: JISHU23
  parentid: 1
  order: 30
  id: 1
  expect: 60123

添加参数化单接口用例。

class TestDepartments:

    ...

    @pytest.mark.parametrize("depart_data", Utils.get_yaml_data("../config/departments.yaml"))
    def test_create_department(self, depart_data):
        data = {
            "name": depart_data["name"],
            "name_en": depart_data["name_en"],
            "parentid": depart_data["parentid"],
            "order": depart_data["order"],
            "id": depart_data["id"]
        }
        print(data)
        r = self.department.create(data)
        print(r.json())
        assert r.status_code == 200
        assert r.json()["errcode"] == depart_data["expect"]
整体结构响应断言优化
pip install genson
pip install jsonschema

在 utils 中添加方法

from genson import SchemaBuilder
from jsonschema.validators import validate


class Utils:

    ...

    @classmethod
    def generate_schema(cls, obj, file_path):
        '''
        生成 json schema 文件
        :param obj: 要生成 scheme 的 python 对象
        :param file_path: json schema 文件保存路径
        '''
        builder = SchemaBuilder()
        # 把预期响应添加到 builder 中
        builder.add_object(obj)
        # 生成 jsonschema
        schema_content = builder.to_schema()
        print(schema_content)
        # 写入 json 文件
        with open(file_path, "w", encoding="utf-8") as f:
            json.dump(schema_content, f)

    @classmethod
    def schema_validate(cls, obj, schema):
        '''
        对比 python 对象与生成的 json schema 结构是否一致
        :param obj: json 格式对象
        :param schema: 生成的 json schema 结构
        :return: 传入的 json 格式对象符合 schema 格式则返回 True,反之返回 False
        '''
        try:
            validate(instance=obj, schema=schema)
            return True
        except Exception as e:
            print(f"schema 校验异常 =========> {e}")
            return False

编写获取子部门 ID 接口用例,并使用 json schema 完成整体断言。

class TestDepartments:

    ...

    def test_get_department_schema(self):
        '''
        获取所有的部门 id 列表
        '''
        # 接口预期响应
        expected = {
            "errcode": 0,
            "errmsg": "ok",
            "department_id": [
                {
                    "id": 2,
                    "parentid": 1,
                    "order": 10
                },
                {
                    "id": 3,
                    "parentid": 2,
                    "order": 40
                }
            ]
        }
        # schema 文件路径
        schema_path = f"{Utils.get_root_path()}/config/get_department_schema.json"
        # 生成 schema 文件
        Utils.generate_schema(expected, schema_path)
        # 发出查询请求
        r = self.department.get()
        # 读取 json schema 文件内容
        schema = json.load(open(schema_path, encoding="utf-8"))
        # 完成整体断言
        assert Utils.schema_validate(r.json(), schema)

总结

  • 单接口自动化测试
  • 接口业务流程测试
  • 接口测试框架封装
  • 复杂断言
  • 生成测试报告

接口自动化测试常见问题

  1. 写接口自动化测试用例时,你使用了什么库,大概使用到了哪些功能?
  2. 接口自动化实现思路?
  3. 接口产生的垃圾数据如何清理?
  4. 接口响应值嵌套较深,获取不方便怎么办?
  5. 接口的返回字段特别多,结构特别复杂时,如何做断言?