# api_unittest_ddt_autotesting **Repository Path**: situbu/api_unittest_ddt_autotesting ## Basic Information - **Project Name**: api_unittest_ddt_autotesting - **Description**: 基于unittest和ddt的数据驱动接口测试框架,报告展示使用XTestRunner - **Primary Language**: Python - **License**: MIT - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 3 - **Forks**: 1 - **Created**: 2021-09-26 - **Last Updated**: 2026-03-18 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # api_unittest_framework #### 介绍 在构建接口自动化测试框架时,使用 unittest + ddt 配合 YAML 文件管理测试数据和预置/清理步骤是一种非常灵活且易于维护的方案。下面给出一个完整的最佳实践方案,包含框架设计、目录结构、核心代码示例及关键注意事项。 #### 框架设计思路 分层设计:将测试数据与代码分离,使用 YAML 文件管理所有接口的请求数据(URL、方法、参数、预期结果等)以及预置(setUp)和清理(tearDown)步骤。 数据驱动:利用 ddt 库将 YAML 中的多条测试数据动态生成多个测试用例。 可复用的预置与清理:在测试类的 setUpClass / tearDownClass 或 setUp / tearDown 中读取 YAML 中定义的接口序列,按顺序执行并处理依赖数据。 统一接口调用:封装 HTTP 请求客户端,支持常见的 GET/POST/PUT/DELETE 等,并处理响应解析和异常。 数据传递:将预置接口返回的关键数据(如 token、ID)保存在测试类的属性中,供测试用例或后续接口使用。 #### 目录结构 ```txt project/ ├── config/ # 配置文件(可选) ├── data/ # YAML 测试数据 │ ├── user_scenario.yml # 某个功能模块的测试数据 │ └── ... ├── core/ # 核心封装 │ ├── http_client.py # HTTP 请求封装 │ ├── yaml_loader.py # YAML 加载器 │ └── base_test.py # 测试基类,封装 setUp/tearDown ├── tests/ # 测试用例 │ ├── test_user.py # 用户模块测试 │ └── ... ├── reports/ # 测试报告(可选) ├── requirements.txt └── run.py # 测试运行入口 ``` #### YAML 文件设计 YAML 文件既可以管理测试用例的数据,也可以管理预置和清理步骤的接口数据。推荐采用一个 YAML 对应一个测试类,其中包含 setup、testcases、teardown 三大块。 示例:data/user_scenario.yml ```yaml # 预置操作(可多个接口) setup: - name: login_admin # 步骤名称,用于日志 request: method: POST url: /api/auth/login headers: Content-Type: application/json json: username: admin password: 123456 extract: # 提取响应中的值供后续使用 - token: $.data.token # JSONPath 表达式 # 清理操作(可多个接口) teardown: - name: delete_user request: method: DELETE url: /api/users/{user_id} # 使用占位符,运行时替换 depends_on: testcase # 表示依赖测试用例中创建的 user_id # 测试用例数据(每条对应一个测试方法的数据驱动) testcases: - name: test_create_user_success description: 创建用户成功场景 data: method: POST url: /api/users headers: Authorization: "Bearer {token}" # 使用 setup 中提取的 token Content-Type: application/json json: name: "John Doe" email: "john@example.com" expected: status_code: 201 json_paths: $.code: 0 $.data.id: int # 类型校验 save_as: user_id # 保存返回的 user_id 供 teardown 使用 - name: test_create_user_duplicate_email description: 重复邮箱创建用户失败 data: method: POST url: /api/users headers: Authorization: "Bearer {token}" json: name: "Jane Doe" email: "jane@example.com" expected: status_code: 400 json_paths: $.code: 10001 $.message: "Email already exists" ``` #### 核心模块实现 1. YAML 加载器 core/yaml_loader.py ```py import yaml import os class YamlLoader: @staticmethod def load(file_path): with open(file_path, 'r', encoding='utf-8') as f: return yaml.safe_load(f) ``` 2. HTTP 客户端封装 core/http_client.py ```py import requests import jsonpath from requests.exceptions import RequestException class HttpClient: def __init__(self, base_url): self.base_url = base_url.rstrip('/') self.session = requests.Session() def request(self, method, url, **kwargs): full_url = self.base_url + url try: response = self.session.request(method, full_url, **kwargs) return response except RequestException as e: raise Exception(f"HTTP请求失败: {e}") def extract_value(self, response, expr): """根据 JSONPath 提取响应值,返回第一个匹配项""" if expr.startswith('$.'): result = jsonpath.jsonpath(response.json(), expr) return result[0] if result else None # 可扩展支持正则提取 header 等 return None ``` 3. 测试基类 core/base_test.py ```py import unittest from core.yaml_loader import YamlLoader from core.http_client import HttpClient import re class BaseTestCase(unittest.TestCase): """所有测试类的基类,封装 setUp / tearDown 逻辑""" @classmethod def setUpClass(cls): """读取 YAML,执行预置接口,提取变量保存到 cls.context 中""" cls.yaml_data = YamlLoader.load(cls.yaml_path) cls.http_client = HttpClient(cls.base_url) cls.context = {} # 用于存储提取的变量(如 token) # 执行预置接口 if 'setup' in cls.yaml_data: for step in cls.yaml_data['setup']: cls._execute_api_step(step, from_setup=True) @classmethod def tearDownClass(cls): """执行清理接口""" if 'teardown' in cls.yaml_data: for step in cls.yaml_data['teardown']: # 清理可能依赖测试中生成的变量,需先渲染 cls._execute_api_step(step, from_teardown=True) @classmethod def _execute_api_step(cls, step, from_setup=False, from_teardown=False, test_context=None): """ 执行单个接口步骤 :param step: 步骤定义(字典) :param from_setup: 是否为预置步骤 :param from_teardown: 是否为清理步骤 :param test_context: 测试用例生成的上下文(如 user_id),用于清理 """ request_info = step['request'] # 占位符替换:优先使用测试上下文,其次使用类上下文 placeholders = {**cls.context, **(test_context or {})} request_info = cls._render_placeholders(request_info, placeholders) # 发送请求 method = request_info.pop('method') url = request_info.pop('url') response = cls.http_client.request(method, url, **request_info) # 如果定义了 extract,则提取变量存入 context if 'extract' in step: for item in step['extract']: for var_name, expr in item.items(): value = cls.http_client.extract_value(response, expr) cls.context[var_name] = value print(f"提取变量: {var_name} = {value}") # 如果是预置步骤,不需要校验;测试用例的校验在具体测试方法中执行 return response @staticmethod def _render_placeholders(data, context): """递归替换字典/列表中的 {key} 占位符""" if isinstance(data, dict): return {k: BaseTestCase._render_placeholders(v, context) for k, v in data.items()} elif isinstance(data, list): return [BaseTestCase._render_placeholders(item, context) for item in data] elif isinstance(data, str): # 使用正则替换 {key} pattern = re.compile(r'\{(\w+)\}') def repl(match): key = match.group(1) return str(context.get(key, match.group(0))) return pattern.sub(repl, data) else: return data ``` #### 测试用例实现 使用 @ddt 和 @data 从 YAML 的 testcases 列表中加载数据。 示例:tests/test_user.py ```py import unittest from ddt import ddt, data, unpack from core.base_test import BaseTestCase @ddt class TestUser(BaseTestCase): # 类属性:指定 YAML 文件路径和 base_url(可从配置文件读取) yaml_path = 'data/user_scenario.yml' base_url = 'http://localhost:8080' @data(*BaseTestCase.yaml_data['testcases']) # 动态展开测试用例列表 def test_user_scenario(self, case): """执行单个测试用例""" case_name = case['name'] description = case.get('description', '') step_data = case['data'] expected = case['expected'] with self.subTest(case=case_name): # 准备测试上下文(如果测试用例中需要保存变量供后续或清理使用) test_context = {} # 渲染请求数据(使用类上下文 context) rendered_data = self._render_placeholders(step_data, self.context) # 发送请求 method = rendered_data.pop('method') url = rendered_data.pop('url') response = self.http_client.request(method, url, **rendered_data) # 状态码断言 self.assertEqual(response.status_code, expected['status_code']) # JSONPath 断言 if 'json_paths' in expected: for path, expected_value in expected['json_paths'].items(): actual = self.http_client.extract_value(response, path) if expected_value == 'int': # 特殊标记,只校验类型 self.assertIsInstance(actual, int) else: self.assertEqual(actual, expected_value) # 如果测试用例定义了 save_as,则保存到 test_context 供清理使用 if 'save_as' in case: var_name = case['save_as'] # 假设保存的是响应中的某个字段,这里简单保存整个 data 对象,实际可按需提取 # 这里为了演示,将整个响应 JSON 的 data 字段保存 data_value = response.json().get('data') if data_value: # 将变量存入类上下文,供 teardown 使用(通过 test_context 传递) # 因为 tearDownClass 在类级别执行,无法感知每个用例生成的变量 # 解决方案:在用例执行后将变量存入类的一个字典,key 为用例索引或唯一标识 # 但 tearDownClass 无法区分是哪个用例产生的,因此清理可能依赖全局变量 # 实际可改用 setUp/tearDown 方法级预置清理,或者用测试套件隔离 # 这里简化:将变量存入类上下文中,但多个用例会覆盖 # 更好的做法:在 tearDown 方法中清理当前用例的数据 pass ``` 注意:上述示例中,如果多个测试用例都需要独立的清理,建议将预置和清理放在 setUp / tearDown 方法中(而不是类级别),并在每个用例执行时单独执行预置步骤。但这样会降低效率。一个折衷方案是:将测试数据分组,每个 YAML 对应一组独立场景,确保清理不影响其他用例。 #### 运行测试 编写 run.py 或使用 unittest 命令行运行。 ```py import unittest import sys if __name__ == '__main__': suite = unittest.defaultTestLoader.discover('tests', pattern='test_*.py') runner = unittest.TextTestRunner(verbosity=2) result = runner.run(suite) sys.exit(not result.wasSuccessful()) ``` #### 最佳实践要点 1. 数据与代码分离:所有接口参数、预期结果都放在 YAML 中,便于维护和修改。 2. 动态占位符替换:利用 {variable} 语法在 YAML 中引用之前提取的值,如 Bearer {token},实现数据依赖。 3. 灵活的提取机制:支持 JSONPath 提取响应值,并可扩展支持正则提取 Header、Cookie 等。 4. 预置与清理的复用:将预置步骤放在 setUpClass,但注意清理步骤可能依赖具体用例生成的数据。如果清理与用例数据强相关,建议改用 setUp / tearDown 并在每个用例内执行预置和清理。 5. DDT 动态生成用例:使用 @data(*list) 将 YAML 中的多条用例自动展开,每个用例独立报告。 6. 错误处理与日志:在请求封装中添加日志记录,便于排查问题。 7. 参数化与分层:对于通用配置(如 base_url、超时时间),可单独放在配置文件中,避免硬编码。 8. 断言增强:支持多类型断言(状态码、JSON 字段、类型校验等)。 9. 测试隔离:如果多个用例共享预置数据(如 token),可以保留类级别预置;但如果用例之间会相互影响,建议每个用例独立执行预置清理,或在用例中不依赖共享状态。 #### 改进方向(可选) 引入 pytest 替代 unittest,结合 pytest-dependency 处理用例依赖。 使用 allure 生成更美观的测试报告。 支持多环境切换(通过命令行参数指定环境)。 封装更强大的断言库(如 jsonpath + assertpy)。 以上方案提供了一个清晰、可扩展的基于 unittest + ddt + YAML 的接口测试框架最佳实践。 (优化后) 通过命令行参数 --env 切换不同环境(dev/test/prod),配置从 config/config.yml 读取。 在 BaseTestCase 中封装了基于 assertpy 和 jsonpath 的增强断言方法(如 assert_json_path、assert_status_code)。 保留了原有的预置/清理步骤执行、变量提取和占位符替换逻辑。 测试用例使用 ddt 从 YAML 文件动态生成,且 YAML 在类定义时加载,确保数据驱动正常工作。 #### 运行用例 将run.py文件放置于项目根目录,即可通过 python run.py --env test 等方式运行测试,并生成带环境标识的测试报告。 也可以通过命令行指定测试人员: ```py python run.py --env test --tester "李四" ```