1. 项目概述:从脚本到框架的质变

如果你已经开始用Python写一些接口测试的脚本,比如用 requests 发几个请求,然后用 assert 判断一下返回码或者关键字段,那么恭喜你,你已经迈出了自动化测试的第一步。但很快,你就会遇到几个挠头的问题:脚本越来越多,复制粘贴的代码也越来越多;改一个公共参数,比如域名或者鉴权头,要翻几十个文件;一个用例失败了,想单独重跑或者调试,发现它和别的用例搅在一起,拆都拆不开。这时候,你就会意识到,把测试代码简单地堆砌在 .py 文件里,已经远远不够了。

“测试函数、测试类/测试方法的封装”这个主题,解决的正是这个痛点。它不是一个高深莫测的理论,而是每个从“脚本小子”进阶为“测试工程师”的必经之路。核心目标就一个: 让测试代码变得可维护、可复用、可管理 。听起来很抽象?其实很简单。想象一下你工具箱里的螺丝刀,如果每次用都临时组装一下刀头和手柄,效率得多低。封装,就是为你常用的测试操作(比如发送请求、断言响应、数据准备)预先打造好一套趁手的“工具”,并且给它们安排好“收纳盒”(测试类)和“使用说明书”(测试方法),让你能像搭积木一样快速、清晰地构建复杂的测试场景。

这个过程,本质上是在构建一个微型测试框架的雏形。它适合所有已经会用Python基础语法和 requests 库发送接口请求的测试同学。无论你是想提升个人脚本的规范性,还是为团队引入更高效的协作模式,掌握这套封装思想,都能让你事半功倍。接下来,我们就抛开那些花哨的概念,直接上手,看看怎么把这些散落的脚本,整理成一套清晰、健壮的自动化资产。

2. 测试代码封装的核心设计思路

在动手写代码之前,我们先得想清楚为什么要这么封装,以及朝哪个方向封装。很多新手会急于模仿网上某个框架的写法,却忽略了背后的设计逻辑,结果就是“形似而神不散”,用起来别别扭扭。

2.1 为何要封装:告别“面条式”代码

最初的测试脚本通常长这样:一个文件里,从上到下依次是导包、定义变量、发送请求A、打印结果、断言、再发送请求B、再断言……这种代码被称为“面条式代码”(Spaghetti Code),所有逻辑纠缠在一起。它的致命问题有三个:

  1. 维护灾难 :接口域名变了?你得在所有脚本里全局搜索替换。请求头格式调整?又是一个大工程。
  2. 复用性为零 :登录操作在十个脚本里写了十遍,稍有改动就要改十处。
  3. 可读性差 :除了写代码的你,别人(包括三个月后的你)根本看不懂这一长串在测什么。

封装的目的,就是通过“分而治之”的思想,将这些混乱的逻辑归类、抽象,形成独立的模块。

2.2 封装的三层境界:函数 -> 类 -> 模块/包

我们的封装路径是递进的:

  1. 函数封装 :将最重复、最独立的操作提炼出来。比如,将“发送POST请求并返回响应”这个动作封装成一个叫 send_post_request 的函数。这是最初级的复用。
  2. 类与方法封装 :当相关的函数越来越多,比如不仅有发送请求的函数,还有处理响应的函数、生成签名的函数,它们共同服务于“接口测试”这个主题。这时,用一个 ApiClient 类把它们组织起来。类内部的函数我们称之为“方法”。类提供了更好的组织结构和状态管理(比如可以维护一个 session 对象)。
  3. 模块与包封装 :当 ApiClient 这样的类不止一个,还有负责读取配置的 ConfigHandler 、负责断言比对的 AssertUtil 、负责生成测试数据的 DataFaker 时,我们就需要将它们分别放到不同的 .py 文件(模块)中,并进一步组织成包(包含 __init__.py 的文件夹)。这是构建框架的形态。

本次我们聚焦在前两层: 测试函数 测试类/方法 的封装。这是构建稳固自动化体系的基石。

2.3 设计原则:高内聚与低耦合

在封装时,心里要默念两个原则:

  • 高内聚 :一个函数或者一个类,应该只做好一件事。比如,一个 login 函数,它的职责就是完成登录并返回 token 。它不应该还去管数据库校验或者日志记录(这些可以交给其他专门的函数)。
  • 低耦合 :模块之间的依赖应该尽可能少。 ApiClient 类不应该直接操作数据库,它应该通过一个清晰的接口(比如 UserDB 类的某个方法)来获取数据。这样,当数据库从MySQL换成PostgreSQL时,你只需要改 UserDB 类的内部实现,而 ApiClient 的代码一行都不用动。

遵循这两个原则封装出来的代码,才会真正具备良好的可维护性和可扩展性。

3. 测试函数封装:打造你的基础工具库

让我们从最具体的操作开始。假设我们有一个用户登录的接口需要测试。原始脚本可能是这样的:

import requests
import json

url = "http://api.example.com/login"
headers = {"Content-Type": "application/json"}
data = {"username": "testuser", "password": "123456"}

response = requests.post(url=url, json=data, headers=headers)
print(response.status_code)
print(response.json())
assert response.status_code == 200
assert response.json()["code"] == 0
assert "token" in response.json()["data"]

这段代码的问题显而易见:发送请求的代码( requests.post )和断言逻辑、测试数据混在一起。我们第一步,就是把“发送HTTP请求”这个通用动作封装起来。

3.1 封装通用请求函数

我们可以创建一个名为 api_utils.py 的文件,里面存放各种工具函数。

# api_utils.py
import requests
import json
from typing import Any, Dict, Optional

def send_request(method: str, url: str, **kwargs) -> requests.Response:
    """
    发送HTTP请求的通用函数
    :param method: 请求方法,'GET', 'POST', 'PUT', 'DELETE'
    :param url: 请求URL
    :param kwargs: 其他requests库支持的参数,如json, data, headers, params, auth等
    :return: requests.Response 对象
    """
    # 可以在这里添加统一的请求头,如User-Agent
    default_headers = {"User-Agent": "MyAutoTest/1.0"}
    headers = kwargs.pop('headers', {})
    headers.update(default_headers)
    kwargs['headers'] = headers

    # 可以在这里添加统一的超时时间
    if 'timeout' not in kwargs:
        kwargs['timeout'] = (5, 30)  # 连接超时5秒,读取超时30秒

    try:
        response = requests.request(method=method.upper(), url=url, **kwargs)
        # 可以在这里添加统一的响应日志记录
        print(f"[Request] {method} {url} - Status: {response.status_code}")
        return response
    except requests.exceptions.Timeout:
        print(f"[Error] Request to {url} timed out.")
        raise
    except requests.exceptions.ConnectionError:
        print(f"[Error] Failed to connect to {url}.")
        raise

def post_json(url: str, json_data: Dict[str, Any], **kwargs) -> requests.Response:
    """发送JSON格式的POST请求(快捷函数)"""
    kwargs['json'] = json_data
    return send_request('POST', url, **kwargs)

def get_request(url: str, params: Optional[Dict] = None, **kwargs) -> requests.Response:
    """发送GET请求(快捷函数)"""
    kwargs['params'] = params
    return send_request('GET', url, **kwargs)

为什么这么封装?

  1. 统一入口 :所有请求都通过 send_request 发出,便于集中管理公共行为(如添加默认头、设置超时、记录日志)。
  2. 异常处理 :在函数内捕获网络超时、连接错误等通用异常,避免在每个测试用例里重复写 try...except
  3. 快捷函数 post_json get_request 让常用操作更简洁,符合“写时便利”的原则。
  4. 类型提示 :使用 typing 模块提供类型提示,虽然不是强制,但能极大提升代码可读性和IDE的智能提示能力。

注意 :日志打印( print )在这里仅作示例。在实际项目中,你应该使用Python的 logging 模块,可以灵活控制日志级别和输出目的地。

3.2 封装断言函数

断言是测试的核心。原始的 assert 语句功能单一,出错信息也不友好。我们可以封装更强大的断言函数。

# assert_utils.py
from typing import Any, Dict

class AssertionErrorWithMessage(AssertionError):
    """自定义断言错误,携带更丰富的上下文信息"""
    pass

def assert_status_code(response, expected_code: int):
    """断言响应状态码"""
    actual = response.status_code
    if actual != expected_code:
        raise AssertionErrorWithMessage(
            f"状态码断言失败!预期: {expected_code}, 实际: {actual}. "
            f"URL: {response.request.url}, 响应体: {response.text[:500]}"
        )

def assert_json_key_exists(response, key_path: str):
    """断言JSON响应体中存在某个键(支持点号路径,如 'data.user.id')"""
    json_data = response.json()
    keys = key_path.split('.')
    current = json_data
    for key in keys:
        if isinstance(current, dict) and key in current:
            current = current[key]
        else:
            raise AssertionErrorWithMessage(
                f"JSON路径 '{key_path}' 不存在。当前访问的键 '{key}' 在对象 {current} 中未找到。"
            )

def assert_json_value_equal(response, key_path: str, expected_value: Any):
    """断言JSON响应体中某个路径的值等于预期值"""
    json_data = response.json()
    keys = key_path.split('.')
    current = json_data
    for key in keys:
        if isinstance(current, dict) and key in current:
            current = current[key]
        else:
            raise AssertionErrorWithMessage(f"路径 '{key_path}' 访问失败于键 '{key}'。")
    if current != expected_value:
        raise AssertionErrorWithMessage(
            f"值断言失败!路径 '{key_path}'。预期: {expected_value}, 实际: {current}"
        )

封装断言的好处:

  1. 错误信息清晰 :当断言失败时,能直接看到是哪个接口、哪个字段出了问题,预期和实际值各是什么,极大缩短了调试时间。
  2. 功能增强 :支持对嵌套JSON的断言(通过点号路径),这是原生 assert 很难优雅实现的。
  3. 统一风格 :所有测试用例使用同一套断言函数,风格一致,易于维护。

3.3 测试数据准备与清理函数

测试往往需要准备特定的测试数据(如创建一个测试用户),并在测试后清理(删除该用户)。这些操作也应该被封装。

# data_utils.py
import random
import string
from typing import Dict

def generate_random_string(length: int = 8) -> str:
    """生成指定长度的随机字符串"""
    letters = string.ascii_letters + string.digits
    return ''.join(random.choice(letters) for _ in range(length))

def create_test_user(username_prefix="autotest_") -> Dict[str, str]:
    """
    创建一个测试用户(示例函数,实际需调用具体业务接口)
    返回创建的用户信息,如用户名、ID等。
    """
    username = f"{username_prefix}{generate_random_string(6)}"
    password = "Test@123456"
    # 这里应该是调用用户注册接口的代码
    # user_id = call_register_api(username, password, ...)
    print(f"[Data Prep] 创建测试用户: {username}") # 模拟操作
    return {"username": username, "password": password, "id": "mock_user_id"}

def delete_test_user(user_info: Dict):
    """
    清理测试用户(示例函数)
    """
    # 这里应该是调用用户注销或删除接口的代码
    # call_delete_user_api(user_info['id'])
    print(f"[Data Cleanup] 清理测试用户: {user_info['username']}") # 模拟操作

实操心得 :数据准备和清理是自动化测试稳定性的关键。一个黄金法则是 “谁创建,谁清理” 。最好将创建和清理逻辑配对封装,并在测试用例的 setup teardown 阶段(后面会讲到)调用,确保即使测试失败,垃圾数据也能被清理,避免污染后续测试。

4. 测试类与测试方法的封装:组织你的测试用例

有了好用的工具函数,我们就可以更优雅地组织测试用例了。这时, unittest pytest 这类测试框架就该登场了。它们提供了“测试类”和“测试方法”的骨架。这里我们以Python标准库 unittest 为例,因为它无需安装,概念清晰。 pytest 更强大灵活,但原理相通。

4.1 理解测试框架的结构

unittest 中:

  • 测试类 :继承自 unittest.TestCase 。一个测试类通常对应一个功能模块或一组相关接口的测试。
  • 测试方法 :类中任何一个以 test_ 开头的方法,都会被自动识别为一个测试用例。
  • 脚手架方法
    • setUp() :在每个测试方法 执行前 自动运行。用于准备测试数据、初始化客户端等。
    • tearDown() :在每个测试方法 执行后 自动运行。用于清理测试数据、关闭连接等。
    • setUpClass(cls) :在整个测试类 开始前 运行一次(需配合 @classmethod 装饰器)。用于执行耗时的全局初始化,如建立数据库连接。
    • tearDownClass(cls) :在整个测试类 结束后 运行一次。用于执行全局清理。

4.2 封装一个API客户端类

首先,我们把之前散落的请求函数,整合成一个更有状态的API客户端类。这个类可以管理会话(Session)、基础URL、通用头信息等。

# api_client.py
import requests
from requests.adapters import HTTPAdapter
from urllib3.util.retry import Retry

class ApiClient:
    """封装HTTP请求的客户端,支持会话保持和重试机制"""

    def __init__(self, base_url: str):
        self.base_url = base_url.rstrip('/')
        self.session = requests.Session()
        # 配置重试策略
        retry_strategy = Retry(
            total=3,  # 总重试次数
            backoff_factor=1,  # 退避因子,等待时间 = backoff_factor * (2^(重试次数-1)) 秒
            status_forcelist=[500, 502, 503, 504]  # 遇到这些状态码才重试
        )
        adapter = HTTPAdapter(max_retries=retry_strategy)
        self.session.mount("http://", adapter)
        self.session.mount("https://", adapter)
        # 设置默认请求头
        self.session.headers.update({
            "User-Agent": "AutoTestSuite/1.0",
            "Accept": "application/json"
        })

    def _make_url(self, endpoint: str) -> str:
        """拼接完整的请求URL"""
        return f"{self.base_url}/{endpoint.lstrip('/')}"

    def request(self, method, endpoint, **kwargs):
        """发送请求的核心方法"""
        url = self._make_url(endpoint)
        # 可以在这里添加统一的请求日志、签名计算等
        print(f"[ApiClient] {method.upper()} {endpoint}")
        response = self.session.request(method=method, url=url, **kwargs)
        response.raise_for_status()  # 如果状态码不是2xx,抛出HTTPError异常
        return response

    # 以下是便捷方法
    def get(self, endpoint, params=None, **kwargs):
        return self.request('GET', endpoint, params=params, **kwargs)

    def post(self, endpoint, json_data=None, **kwargs):
        return self.request('POST', endpoint, json=json_data, **kwargs)

    def login(self, username, password):
        """封装业务登录接口,返回token或其他认证信息"""
        endpoint = "/api/v1/login"
        data = {"username": username, "password": password}
        resp = self.post(endpoint, json_data=data)
        resp_data = resp.json()
        token = resp_data.get('data', {}).get('token')
        if token:
            # 登录成功后,将token添加到后续请求的头部
            self.session.headers.update({"Authorization": f"Bearer {token}"})
        return resp_data

这个客户端类的优势:

  1. 会话保持 :使用 requests.Session() ,可以自动处理cookies,在同一个会话中保持登录状态。
  2. 自动重试 :通过 Retry 策略,对服务器临时性错误(5xx)进行自动重试,提升测试稳定性。
  3. 统一配置 :基础URL、默认请求头都在一个地方管理。
  4. 业务方法封装 :像 login 这样的业务接口被封装成类方法,测试用例调用时语义更清晰。

4.3 编写基于测试类的测试用例

现在,我们来创建一个测试文件 test_user_api.py ,看看如何利用封装好的工具和客户端。

# test_user_api.py
import unittest
from api_client import ApiClient
from assert_utils import assert_status_code, assert_json_key_exists, assert_json_value_equal
from data_utils import create_test_user, delete_test_user

class TestUserAPI(unittest.TestCase):
    """用户相关接口的测试类"""

    @classmethod
    def setUpClass(cls):
        """整个测试类开始前执行一次"""
        print("\n=== 开始执行用户API测试套件 ===")
        # 初始化API客户端,配置测试环境的基础URL
        cls.client = ApiClient(base_url="http://api.example.com")
        # 这里可以初始化数据库连接等全局资源
        # cls.db_conn = create_db_connection()

    @classmethod
    def tearDownClass(cls):
        """整个测试类结束后执行一次"""
        print("\n=== 用户API测试套件执行完毕 ===")
        # 关闭全局资源
        # cls.db_conn.close()

    def setUp(self):
        """每个测试方法开始前执行"""
        print(f"\n--- 开始执行测试: {self._testMethodName} ---")
        # 为当前测试用例创建专用的测试数据
        self.test_user = create_test_user()
        # 使用创建的用户登录,获取认证状态
        login_resp = self.client.login(self.test_user['username'], self.test_user['password'])
        self.assertTrue(login_resp['code'] == 0, "测试用户登录失败,无法进行后续测试")

    def tearDown(self):
        """每个测试方法结束后执行"""
        print(f"--- 结束测试: {self._testMethodName}, 开始清理 ---")
        # 清理本用例创建的测试数据
        delete_test_user(self.test_user)
        # 可选:登出,清除客户端认证状态
        self.client.session.headers.pop('Authorization', None)

    # -------------------- 具体的测试用例 --------------------
    def test_get_user_profile_success(self):
        """测试成功获取用户资料"""
        # 1. 发起请求
        endpoint = f"/api/v1/users/{self.test_user['id']}/profile"
        response = self.client.get(endpoint)

        # 2. 使用封装的断言函数进行验证
        assert_status_code(response, 200)
        resp_json = response.json()
        assert_json_key_exists(response, 'data.username')
        assert_json_value_equal(response, 'data.username', self.test_user['username'])
        # 也可以使用unittest原生的断言,但信息不如自定义的丰富
        self.assertEqual(resp_json['code'], 0, "业务状态码非0")

    def test_update_user_profile(self):
        """测试更新用户资料"""
        new_nickname = "自动化测试昵称"
        endpoint = f"/api/v1/users/{self.test_user['id']}/profile"
        update_data = {"nickname": new_nickname}

        response = self.client.post(endpoint, json_data=update_data)

        assert_status_code(response, 200)
        assert_json_value_equal(response, 'code', 0)
        assert_json_value_equal(response, 'data.nickname', new_nickname)

        # 可以再调用一次查询接口,验证数据确实已更新(这是更严谨的测试)
        get_response = self.client.get(endpoint)
        assert_json_value_equal(get_response, 'data.nickname', new_nickname)

    def test_get_user_profile_with_invalid_id(self):
        """测试使用无效用户ID获取资料,应返回错误"""
        endpoint = "/api/v1/users/invalid_id_999999/profile"
        response = self.client.get(endpoint)
        # 预期业务逻辑错误,HTTP状态码可能仍是200,但业务码非0
        assert_status_code(response, 200)
        assert_json_value_equal(response, 'code', 1001)  # 假设1001是用户不存在的错误码
        assert_json_key_exists(response, 'message')

if __name__ == '__main__':
    unittest.main(verbosity=2)  # verbosity=2 输出更详细的测试信息

这个测试类的精妙之处:

  1. 清晰的生命周期管理 setUpClass / tearDownClass 管理全局资源, setUp / tearDown 管理用例级别的资源。确保了测试的独立性和环境的干净。
  2. 测试数据隔离 :每个测试方法都有自己的 self.test_user ,互不干扰。 tearDown 确保即使测试失败,垃圾数据也能被清理。
  3. 用例即文档 :测试方法名 test_xxx 清晰地描述了测试目的。用例内部“准备数据 -> 执行操作 -> 断言结果”的结构清晰。
  4. 复用与清晰 ApiClient 和断言函数被复用,测试用例代码非常简洁,只关注业务逻辑和验证点。

5. 高级封装技巧与最佳实践

掌握了基础的类和函数封装后,我们可以再进一步,让测试框架更强大、更智能。

5.1 使用配置文件管理环境与参数

硬编码的 base_url 和测试数据是脆弱的。我们需要将配置外化。

# config.py (或 config.yaml/json)
import os
from typing import Dict, Any
import yaml  # 需要安装PyYAML: pip install PyYAML

class Config:
    _instance = None

    def __new__(cls):
        if cls._instance is None:
            cls._instance = super().__new__(cls)
            cls._instance._load_config()
        return cls._instance

    def _load_config(self):
        # 通过环境变量决定加载哪个环境的配置
        env = os.getenv('TEST_ENV', 'dev').lower()
        config_file = f'config_{env}.yaml'
        with open(config_file, 'r', encoding='utf-8') as f:
            self._config = yaml.safe_load(f)

    def get(self, key: str, default: Any = None) -> Any:
        """通过点号路径获取配置,如 'database.host'"""
        keys = key.split('.')
        value = self._config
        for k in keys:
            if isinstance(value, dict):
                value = value.get(k)
                if value is None:
                    return default
            else:
                return default
        return value

# 使用单例模式,全局一份配置
config = Config()

# config_dev.yaml 示例
# base_url: http://dev-api.example.com
# database:
#   host: localhost
#   port: 3306
#   user: test
#   password: test123
# test_data:
#   default_password: Test@123456

然后在 ApiClient 和测试数据函数中使用配置:

# api_client.py 修改 __init__
def __init__(self, base_url: str = None):
    from config import config
    self.base_url = base_url or config.get('base_url')
    # ... 其余初始化代码

# data_utils.py 修改 create_test_user
def create_test_user(username_prefix="autotest_"):
    from config import config
    password = config.get('test_data.default_password', 'Test@123456')
    # ...

5.2 封装数据驱动测试

当同一个测试逻辑需要多组不同数据验证时,数据驱动可以避免写多个重复的测试方法。 unittest 本身支持不完美,但我们可以结合 @parameterized.expand 装饰器(需要安装 parameterized 库)或自己实现。

# test_with_data_driven.py
import unittest
from parameterized import parameterized
from api_client import ApiClient

class TestLoginDDT(unittest.TestCase):

    def setUp(self):
        self.client = ApiClient(base_url="http://api.example.com")

    @parameterized.expand([
        ("正确用户名密码", "correct_user", "correct_pwd", 200, 0),
        ("错误密码", "correct_user", "wrong_pwd", 200, 1001),
        ("空用户名", "", "some_pwd", 400, None),  # 预期HTTP 400错误,可能无业务码
        ("用户不存在", "non_exist_user", "any_pwd", 200, 1002),
    ])
    def test_login_with_various_input(self, test_case_name, username, password, expected_http_code, expected_biz_code):
        """数据驱动测试登录接口"""
        print(f"  执行用例: {test_case_name}")
        endpoint = "/api/v1/login"
        data = {"username": username, "password": password}
        response = self.client.post(endpoint, json_data=data)
        self.assertEqual(response.status_code, expected_http_code)
        if expected_biz_code is not None:
            resp_json = response.json()
            self.assertEqual(resp_json.get('code'), expected_biz_code)

这样,一个测试方法就覆盖了多种边界情况,测试报告也会清晰地列出每一个数据组合作为独立的测试点。

5.3 日志与报告集成

打印 print 不是长久之计。集成日志模块和生成HTML测试报告能极大提升效率。

# 在 conftest.py (pytest) 或测试类中集成 logging
import logging
import sys

def setup_logging():
    logger = logging.getLogger('autotest')
    logger.setLevel(logging.DEBUG)
    # 控制台处理器
    ch = logging.StreamHandler(sys.stdout)
    ch.setLevel(logging.INFO)
    # 文件处理器
    fh = logging.FileHandler('test_run.log', encoding='utf-8')
    fh.setLevel(logging.DEBUG)
    # 格式
    formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
    ch.setFormatter(formatter)
    fh.setFormatter(formatter)
    logger.addHandler(ch)
    logger.addHandler(fh)
    return logger

logger = setup_logging()
# 然后在代码中用 logger.info(...) 代替 print

对于报告,可以使用 pytest-html allure-pytest unittest HTMLTestRunner 来生成美观的测试报告。

6. 常见问题与排查技巧实录

在实际封装和运行过程中,你肯定会遇到各种问题。这里记录一些典型的“坑”和解决思路。

6.1 测试用例相互污染

问题 :测试用例A创建的数据,影响了测试用例B的执行。 排查

  1. 检查 setUp tearDown 是否真的为每个测试方法都执行了。确保它们正确清理了 self 上的属性或外部资源。
  2. 检查是否使用了全局变量或类变量( cls.xxx )来存储测试数据。除非必要,否则尽量使用实例变量( self.xxx )。
  3. 检查API是否有缓存机制,导致B用例读到了A用例缓存的数据。可以在 setUp 中清理客户端缓存,或为每个用例生成唯一标识(如用户名加时间戳)。

6.2 接口依赖与测试顺序

问题 test_B 需要 test_A 先执行产生的数据或状态。 解决

  • 错误做法 :依赖 unittest 默认按方法名顺序执行。这是不可靠的。
  • 正确做法 :将 test_A test_B 的公共前置条件提取出来,放在一个独立的 setUp 步骤或一个单独的 @classmethod 方法中。确保每个测试用例都是独立的。如果B确实依赖A的 结果 ,那么应该把A的操作封装成一个函数,在B的 setUp 或测试方法开头调用。

6.3 断言失败信息不清晰

问题 :测试失败时,只看到 AssertionError ,不知道具体是哪个字段不对。 解决

  1. 一定要使用我们前面封装的 assert_json_value_equal 这类函数,它们携带了详细的上下文信息。
  2. 在断言前,可以先把关键的响应内容用 logger.debug 打印出来。
  3. 对于复杂的JSON,可以使用 json.dumps(response.json(), indent=2, ensure_ascii=False) 格式化打印,便于肉眼比对。

6.4 网络超时或环境不稳定

问题 :测试偶尔因网络波动失败。 解决

  1. ApiClient 中配置合理的重试机制(如前文所示)。
  2. requests 设置合理的 timeout 参数,避免无限等待。
  3. 对于非核心的断言(如查询列表的长度),可以考虑使用“软断言”或范围断言,例如 self.assertGreater(len(list_data), 0) 而不是 self.assertEqual(len(list_data), 5)
  4. 区分环境问题与bug。可以设置一个简单的 /health /ping 接口检查,在 setUpClass 中调用,如果失败则跳过整个测试类,并标记为环境问题。

6.5 封装过度导致灵活性下降

问题 :为了复用,把很多逻辑都封装在底层函数或类里,导致想测试一个特殊场景时,需要层层修改封装好的代码。 解决 :遵循“开放-封闭原则”。核心的、稳定的逻辑(如HTTP请求、基础断言)可以封装得很死。但业务逻辑相关的部分,封装应该提供足够的“钩子”和“配置项”。例如, ApiClient.request 方法应该允许调用者传入自定义的 headers 来覆盖默认值,而不是写死。

更多推荐