Python接口测试进阶:从脚本到框架的封装实践
1. 项目概述:从脚本到框架的质变
如果你已经开始用Python写一些接口测试的脚本,比如用 requests 发几个请求,然后用 assert 判断一下返回码或者关键字段,那么恭喜你,你已经迈出了自动化测试的第一步。但很快,你就会遇到几个挠头的问题:脚本越来越多,复制粘贴的代码也越来越多;改一个公共参数,比如域名或者鉴权头,要翻几十个文件;一个用例失败了,想单独重跑或者调试,发现它和别的用例搅在一起,拆都拆不开。这时候,你就会意识到,把测试代码简单地堆砌在 .py 文件里,已经远远不够了。
“测试函数、测试类/测试方法的封装”这个主题,解决的正是这个痛点。它不是一个高深莫测的理论,而是每个从“脚本小子”进阶为“测试工程师”的必经之路。核心目标就一个: 让测试代码变得可维护、可复用、可管理 。听起来很抽象?其实很简单。想象一下你工具箱里的螺丝刀,如果每次用都临时组装一下刀头和手柄,效率得多低。封装,就是为你常用的测试操作(比如发送请求、断言响应、数据准备)预先打造好一套趁手的“工具”,并且给它们安排好“收纳盒”(测试类)和“使用说明书”(测试方法),让你能像搭积木一样快速、清晰地构建复杂的测试场景。
这个过程,本质上是在构建一个微型测试框架的雏形。它适合所有已经会用Python基础语法和 requests 库发送接口请求的测试同学。无论你是想提升个人脚本的规范性,还是为团队引入更高效的协作模式,掌握这套封装思想,都能让你事半功倍。接下来,我们就抛开那些花哨的概念,直接上手,看看怎么把这些散落的脚本,整理成一套清晰、健壮的自动化资产。
2. 测试代码封装的核心设计思路
在动手写代码之前,我们先得想清楚为什么要这么封装,以及朝哪个方向封装。很多新手会急于模仿网上某个框架的写法,却忽略了背后的设计逻辑,结果就是“形似而神不散”,用起来别别扭扭。
2.1 为何要封装:告别“面条式”代码
最初的测试脚本通常长这样:一个文件里,从上到下依次是导包、定义变量、发送请求A、打印结果、断言、再发送请求B、再断言……这种代码被称为“面条式代码”(Spaghetti Code),所有逻辑纠缠在一起。它的致命问题有三个:
- 维护灾难 :接口域名变了?你得在所有脚本里全局搜索替换。请求头格式调整?又是一个大工程。
- 复用性为零 :登录操作在十个脚本里写了十遍,稍有改动就要改十处。
- 可读性差 :除了写代码的你,别人(包括三个月后的你)根本看不懂这一长串在测什么。
封装的目的,就是通过“分而治之”的思想,将这些混乱的逻辑归类、抽象,形成独立的模块。
2.2 封装的三层境界:函数 -> 类 -> 模块/包
我们的封装路径是递进的:
- 函数封装 :将最重复、最独立的操作提炼出来。比如,将“发送POST请求并返回响应”这个动作封装成一个叫
send_post_request的函数。这是最初级的复用。 - 类与方法封装 :当相关的函数越来越多,比如不仅有发送请求的函数,还有处理响应的函数、生成签名的函数,它们共同服务于“接口测试”这个主题。这时,用一个
ApiClient类把它们组织起来。类内部的函数我们称之为“方法”。类提供了更好的组织结构和状态管理(比如可以维护一个session对象)。 - 模块与包封装 :当
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)
为什么这么封装?
- 统一入口 :所有请求都通过
send_request发出,便于集中管理公共行为(如添加默认头、设置超时、记录日志)。 - 异常处理 :在函数内捕获网络超时、连接错误等通用异常,避免在每个测试用例里重复写
try...except。 - 快捷函数 :
post_json和get_request让常用操作更简洁,符合“写时便利”的原则。 - 类型提示 :使用
typing模块提供类型提示,虽然不是强制,但能极大提升代码可读性和IDE的智能提示能力。
注意 :日志打印(
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}"
)
封装断言的好处:
- 错误信息清晰 :当断言失败时,能直接看到是哪个接口、哪个字段出了问题,预期和实际值各是什么,极大缩短了调试时间。
- 功能增强 :支持对嵌套JSON的断言(通过点号路径),这是原生
assert很难优雅实现的。 - 统一风格 :所有测试用例使用同一套断言函数,风格一致,易于维护。
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
这个客户端类的优势:
- 会话保持 :使用
requests.Session(),可以自动处理cookies,在同一个会话中保持登录状态。 - 自动重试 :通过
Retry策略,对服务器临时性错误(5xx)进行自动重试,提升测试稳定性。 - 统一配置 :基础URL、默认请求头都在一个地方管理。
- 业务方法封装 :像
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 输出更详细的测试信息
这个测试类的精妙之处:
- 清晰的生命周期管理 :
setUpClass/tearDownClass管理全局资源,setUp/tearDown管理用例级别的资源。确保了测试的独立性和环境的干净。 - 测试数据隔离 :每个测试方法都有自己的
self.test_user,互不干扰。tearDown确保即使测试失败,垃圾数据也能被清理。 - 用例即文档 :测试方法名
test_xxx清晰地描述了测试目的。用例内部“准备数据 -> 执行操作 -> 断言结果”的结构清晰。 - 复用与清晰 :
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的执行。 排查 :
- 检查
setUp和tearDown是否真的为每个测试方法都执行了。确保它们正确清理了self上的属性或外部资源。 - 检查是否使用了全局变量或类变量(
cls.xxx)来存储测试数据。除非必要,否则尽量使用实例变量(self.xxx)。 - 检查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 ,不知道具体是哪个字段不对。 解决 :
- 一定要使用我们前面封装的
assert_json_value_equal这类函数,它们携带了详细的上下文信息。 - 在断言前,可以先把关键的响应内容用
logger.debug打印出来。 - 对于复杂的JSON,可以使用
json.dumps(response.json(), indent=2, ensure_ascii=False)格式化打印,便于肉眼比对。
6.4 网络超时或环境不稳定
问题 :测试偶尔因网络波动失败。 解决 :
- 在
ApiClient中配置合理的重试机制(如前文所示)。 - 为
requests设置合理的timeout参数,避免无限等待。 - 对于非核心的断言(如查询列表的长度),可以考虑使用“软断言”或范围断言,例如
self.assertGreater(len(list_data), 0)而不是self.assertEqual(len(list_data), 5)。 - 区分环境问题与bug。可以设置一个简单的
/health或/ping接口检查,在setUpClass中调用,如果失败则跳过整个测试类,并标记为环境问题。
6.5 封装过度导致灵活性下降
问题 :为了复用,把很多逻辑都封装在底层函数或类里,导致想测试一个特殊场景时,需要层层修改封装好的代码。 解决 :遵循“开放-封闭原则”。核心的、稳定的逻辑(如HTTP请求、基础断言)可以封装得很死。但业务逻辑相关的部分,封装应该提供足够的“钩子”和“配置项”。例如, ApiClient.request 方法应该允许调用者传入自定义的 headers 来覆盖默认值,而不是写死。
更多推荐
所有评论(0)