第5讲我们实现了代码审查------MiniCopilot 能自动发现代码中的问题。但发现问题只是第一步,更重要的是防止问题再次发生。这就需要有完善的单元测试。
然而,写测试是开发者最不喜欢做的事情之一。这一讲,我们要让 MiniCopilot 自动为代码生成单元测试。
一、测试生成的核心挑战
| 挑战 | 说明 |
|---|---|
| 理解被测代码 | 必须准确理解函数的输入、输出和行为 |
| 覆盖边界情况 | 不仅要测正常路径,还要测异常、边界值 |
| Mock 外部依赖 | 数据库、网络请求等需要模拟 |
| 测试风格一致 | 生成的测试要符合项目的测试风格 |
| 可维护性 | 测试代码要清晰、可读、易于维护 |
测试生成流程
目标函数代码
↓
1. 函数分析(AST 解析)
├── 函数签名(名称、参数、返回值)
├── 控制流(条件分支、循环)
├── 外部依赖(数据库、API、文件)
└── 边界条件(空值、极值、异常)
↓
2. 测试策略生成
├── 正常路径测试
├── 边界值测试
├── 异常路径测试
└── Mock 策略
↓
3. 测试代码生成
├── 导入必要的测试框架
├── 生成测试类和测试方法
├── 生成 Mock 对象
└── 生成断言
↓
4. 测试验证
├── 语法检查
├── 运行测试(可选)
└── 审查测试质量
二、函数分析器
2.1 函数特征提取
# engine/test_gen/function_analyzer.py
from dataclasses import dataclass, field
from typing import Optional
from engine.parser.python_parser import PythonParser
@dataclass
class ParameterInfo:
"""参数信息"""
name: str
type_hint: Optional[str] = None
default_value: Optional[str] = None
is_optional: bool = False
@dataclass
class FunctionInfo:
"""函数信息"""
name: str
parameters: list[ParameterInfo] = field(default_factory=list)
return_type: Optional[str] = None
has_docstring: bool = False
raises_exceptions: list[str] = field(default_factory=list)
calls_external: list[str] = field(default_factory=list)
has_conditional_branches: bool = False
has_loops: bool = False
complexity: int = 1 # 圈复杂度
class FunctionAnalyzer:
"""函数分析器"""
def __init__(self):
self.parser = PythonParser()
def analyze(self, code: str) -> FunctionInfo:
"""分析函数代码,提取特征"""
ast = self.parser.parse(code)
func_node = self._find_function(ast)
if not func_node:
raise ValueError("未找到函数定义")
info = FunctionInfo(name=self._get_func_name(func_node))
# 提取参数
info.parameters = self._extract_parameters(func_node)
# 提取返回类型
info.return_type = self._extract_return_type(func_node)
# 检查是否有 docstring
info.has_docstring = self._has_docstring(func_node)
# 提取抛出的异常
info.raises_exceptions = self._extract_exceptions(func_node)
# 提取外部调用
info.calls_external = self._extract_external_calls(func_node)
# 分析控制流
info.has_conditional_branches = self._has_conditional_branches(func_node)
info.has_loops = self._has_loops(func_node)
# 计算圈复杂度
info.complexity = self._calculate_complexity(func_node)
return info
def _find_function(self, node):
"""在 AST 中查找第一个函数定义"""
if node.type == "function_definition":
return node
for child in node.children:
result = self._find_function(child)
if result:
return result
return None
def _get_func_name(self, func_node) -> str:
for child in func_node.children:
if child.type == "identifier":
return child.text
return ""
def _extract_parameters(self, func_node) -> list[ParameterInfo]:
"""提取参数信息"""
params = []
for child in func_node.children:
if child.type == "parameters":
for param in child.children:
if param.type == "identifier":
params.append(ParameterInfo(name=param.text))
elif param.type == "typed_parameter":
# 带类型注解的参数
name = ""
type_hint = ""
for p in param.children:
if p.type == "identifier":
name = p.text
elif p.type == "type":
type_hint = p.text
params.append(ParameterInfo(
name=name, type_hint=type_hint
))
elif param.type == "default_parameter":
# 带默认值的参数
name = ""
default = ""
for p in param.children:
if p.type == "identifier":
name = p.text
elif p.type in ("integer", "string", "true", "false", "none"):
default = p.text
params.append(ParameterInfo(
name=name, default_value=default, is_optional=True
))
return params
def _extract_return_type(self, func_node) -> Optional[str]:
"""提取返回类型"""
for child in func_node.children:
if child.type == "return_type":
for type_node in child.children:
if type_node.type == "type":
return type_node.text
return None
def _has_docstring(self, func_node) -> bool:
"""检查是否有 docstring"""
for child in func_node.children:
if child.type == "block":
for stmt in child.children:
if stmt.type == "expression_statement":
text = stmt.text.strip()
if text.startswith('"""') or text.startswith("'''"):
return True
return False
def _extract_exceptions(self, func_node) -> list[str]:
"""提取抛出的异常"""
exceptions = []
func_text = func_node.text
import re
# 匹配 raise 语句
for match in re.finditer(r'raise\s+(\w+(?:Error|Exception|Warning))', func_text):
exceptions.append(match.group(1))
return exceptions
def _extract_external_calls(self, func_node) -> list[str]:
"""提取外部调用(数据库、API、文件)"""
calls = []
func_text = func_node.text
# 数据库调用
if 'execute' in func_text or 'query' in func_text or 'cursor' in func_text:
calls.append('database')
# API 调用
if 'requests.' in func_text or 'urllib' in func_text or 'httpx' in func_text:
calls.append('api')
# 文件操作
if 'open(' in func_text or 'read(' in func_text or 'write(' in func_text:
calls.append('file')
return calls
def _has_conditional_branches(self, func_node) -> bool:
"""检查是否有条件分支"""
func_text = func_node.text
return 'if ' in func_text or 'elif ' in func_text or 'else:' in func_text
def _has_loops(self, func_node) -> bool:
"""检查是否有循环"""
func_text = func_node.text
return 'for ' in func_text or 'while ' in func_text
def _calculate_complexity(self, func_node) -> int:
"""计算圈复杂度"""
complexity = 1
func_text = func_node.text
# 每个 if/elif/while/for/except 增加复杂度
for keyword in ['if ', 'elif ', 'while ', 'for ', 'except ', 'and ', 'or ']:
complexity += func_text.count(keyword)
return complexity
三、测试策略生成器
3.1 测试用例规划
# engine/test_gen/strategy.py
from dataclasses import dataclass, field
from typing import Optional
@dataclass
class TestCase:
"""测试用例"""
name: str
description: str
inputs: dict
expected_output: str
mock_behavior: dict = field(default_factory=dict)
should_raise: Optional[str] = None
class TestStrategyGenerator:
"""测试策略生成器"""
def generate(self, func_info) -> list[TestCase]:
"""
根据函数信息生成测试用例
策略:
1. 正常路径:典型的输入输出
2. 边界值:参数的边界情况
3. 异常路径:预期抛出异常的场景
4. 特殊值:None、空值、极大/极小值
"""
test_cases = []
# 1. 正常路径测试
normal_case = self._generate_normal_case(func_info)
if normal_case:
test_cases.append(normal_case)
# 2. 边界值测试
boundary_cases = self._generate_boundary_cases(func_info)
test_cases.extend(boundary_cases)
# 3. 异常路径测试
exception_cases = self._generate_exception_cases(func_info)
test_cases.extend(exception_cases)
# 4. 特殊值测试
special_cases = self._generate_special_cases(func_info)
test_cases.extend(special_cases)
return test_cases
def _generate_normal_case(self, func_info) -> Optional[TestCase]:
"""生成正常路径测试"""
if not func_info.parameters:
return TestCase(
name=f"test_{func_info.name}_basic",
description=f"测试 {func_info.name} 的基本功能",
inputs={},
expected_output=""
)
# 为每个参数生成典型值
inputs = {}
for param in func_info.parameters:
inputs[param.name] = self._get_typical_value(param)
return TestCase(
name=f"test_{func_info.name}_normal",
description=f"使用典型参数测试 {func_info.name}",
inputs=inputs,
expected_output=""
)
def _generate_boundary_cases(self, func_info) -> list[TestCase]:
"""生成边界值测试"""
cases = []
for param in func_info.parameters:
if param.type_hint in ("int", "float"):
# 整数/浮点数边界
cases.append(TestCase(
name=f"test_{func_info.name}_boundary_{param.name}_zero",
description=f"测试 {param.name}=0 的边界情况",
inputs={param.name: 0},
expected_output=""
))
cases.append(TestCase(
name=f"test_{func_info.name}_boundary_{param.name}_negative",
description=f"测试 {param.name} 为负数的边界情况",
inputs={param.name: -1},
expected_output=""
))
cases.append(TestCase(
name=f"test_{func_info.name}_boundary_{param.name}_large",
description=f"测试 {param.name} 为大数的边界情况",
inputs={param.name: 999999},
expected_output=""
))
elif param.type_hint in ("str", "string"):
# 字符串边界
cases.append(TestCase(
name=f"test_{func_info.name}_boundary_{param.name}_empty",
description=f"测试 {param.name} 为空字符串",
inputs={param.name: ""},
expected_output=""
))
cases.append(TestCase(
name=f"test_{func_info.name}_boundary_{param.name}_very_long",
description=f"测试 {param.name} 为超长字符串",
inputs={param.name: "a" * 1000},
expected_output=""
))
elif param.type_hint in ("list", "List"):
cases.append(TestCase(
name=f"test_{func_info.name}_boundary_{param.name}_empty",
description=f"测试 {param.name} 为空列表",
inputs={param.name: []},
expected_output=""
))
cases.append(TestCase(
name=f"test_{func_info.name}_boundary_{param.name}_single",
description=f"测试 {param.name} 为单元素列表",
inputs={param.name: [1]},
expected_output=""
))
return cases
def _generate_exception_cases(self, func_info) -> list[TestCase]:
"""生成异常路径测试"""
cases = []
# 如果函数会抛出异常,为每个异常生成测试
for exc in func_info.raises_exceptions:
cases.append(TestCase(
name=f"test_{func_info.name}_raises_{exc}",
description=f"测试 {func_info.name} 抛出 {exc}",
inputs={},
expected_output="",
should_raise=exc
))
# 如果没有显式声明异常,但参数有类型约束,生成类型错误测试
for param in func_info.parameters:
if param.type_hint:
wrong_type = self._get_wrong_type(param.type_hint)
if wrong_type:
cases.append(TestCase(
name=f"test_{func_info.name}_invalid_{param.name}_type",
description=f"测试传入错误类型到 {param.name}",
inputs={param.name: wrong_type},
expected_output="",
should_raise="TypeError"
))
return cases
def _generate_special_cases(self, func_info) -> list[TestCase]:
"""生成特殊值测试"""
cases = []
for param in func_info.parameters:
if param.is_optional:
# 可选参数不传
cases.append(TestCase(
name=f"test_{func_info.name}_without_{param.name}",
description=f"测试不传可选参数 {param.name}",
inputs={},
expected_output=""
))
# None 值测试
if param.type_hint and param.type_hint not in ("int", "float", "bool"):
cases.append(TestCase(
name=f"test_{func_info.name}_{param.name}_is_none",
description=f"测试 {param.name} 为 None",
inputs={param.name: None},
expected_output=""
))
return cases
def _get_typical_value(self, param) -> str:
"""获取参数的典型值"""
type_defaults = {
"int": "10",
"float": "3.14",
"str": '"hello"',
"string": '"hello"',
"bool": "True",
"list": "[1, 2, 3]",
"List": "[1, 2, 3]",
"dict": '{"key": "value"}',
"Dict": '{"key": "value"}',
"Optional": "None",
}
if param.default_value:
return param.default_value
return type_defaults.get(param.type_hint, '"test"')
def _get_wrong_type(self, type_hint: str):
"""获取错误类型的值"""
wrong_types = {
"int": '"not_a_number"',
"float": '"not_a_float"',
"str": "123",
"string": "123",
"list": '"not_a_list"',
"dict": '"not_a_dict"',
}
return wrong_types.get(type_hint)
四、测试代码生成器
4.1 pytest 测试生成
# engine/test_gen/pytest_generator.py
from .function_analyzer import FunctionAnalyzer
from .strategy import TestStrategyGenerator
class PytestGenerator:
"""pytest 测试生成器"""
def __init__(self):
self.analyzer = FunctionAnalyzer()
self.strategy_generator = TestStrategyGenerator()
def generate(self, source_code: str, module_name: str = "my_module") -> str:
"""
为给定的函数生成 pytest 测试代码
参数:
source_code: 函数源代码
module_name: 模块名(用于 import)
返回:生成的测试代码字符串
"""
# 1. 分析函数
func_info = self.analyzer.analyze(source_code)
# 2. 生成测试策略
test_cases = self.strategy_generator.generate(func_info)
# 3. 生成测试代码
test_code = self._generate_test_code(
func_info, test_cases, module_name
)
return test_code
def _generate_test_code(self, func_info, test_cases, module_name: str) -> str:
"""生成测试代码"""
lines = []
# 文件头
lines.append('"""')
lines.append(f"单元测试 - {func_info.name}")
lines.append(f"自动生成于 MiniCopilot")
lines.append('"""')
lines.append("")
lines.append("import pytest")
lines.append(f"from {module_name} import {func_info.name}")
lines.append("")
# 如果有外部依赖,生成 fixture
if func_info.calls_external:
lines.extend(self._generate_fixtures(func_info))
lines.append("")
# 测试类
class_name = f"Test{func_info.name.capitalize()}"
lines.append(f"class {class_name}:")
lines.append(f' """{func_info.name} 的测试类"""')
lines.append("")
# 生成每个测试用例
for tc in test_cases:
lines.extend(self._generate_test_method(func_info, tc))
lines.append("")
return '\n'.join(lines)
def _generate_fixtures(self, func_info) -> list[str]:
"""生成 fixture"""
lines = []
if 'database' in func_info.calls_external:
lines.append("@pytest.fixture")
lines.append("def mock_db(mocker):")
lines.append(' """模拟数据库连接"""')
lines.append(" mock_conn = mocker.MagicMock()")
lines.append(" mock_cursor = mocker.MagicMock()")
lines.append(" mock_conn.cursor.return_value = mock_cursor")
lines.append(" return mock_conn")
lines.append("")
if 'api' in func_info.calls_external:
lines.append("@pytest.fixture")
lines.append("def mock_api(mocker):")
lines.append(' """模拟 API 请求"""')
lines.append(" mock_response = mocker.MagicMock()")
lines.append(" mock_response.status_code = 200")
lines.append(" mock_response.json.return_value = {'status': 'ok'}")
lines.append(" mocker.patch('requests.get', return_value=mock_response)")
lines.append(" return mock_response")
lines.append("")
return lines
def _generate_test_method(self, func_info, tc: TestCase) -> list[str]:
"""生成单个测试方法"""
lines = []
# 装饰器
if tc.should_raise:
lines.append(f" @pytest.mark.xfail(raises={tc.should_raise})")
# 方法签名
lines.append(f" def {tc.name}(self):")
lines.append(f' """{tc.description}"""')
lines.append("")
# 准备输入
if tc.inputs:
for name, value in tc.inputs.items():
lines.append(f" {name} = {value}")
lines.append("")
# Mock 行为
if tc.mock_behavior:
for mock_name, mock_value in tc.mock_behavior.items():
lines.append(f" {mock_name} = {mock_value}")
lines.append("")
# 调用被测函数
params_str = ", ".join(
f"{name}={name}" if name in tc.inputs else name
for name in [p.name for p in func_info.parameters]
if name in tc.inputs
)
if tc.should_raise:
lines.append(f" with pytest.raises({tc.should_raise}):")
lines.append(f" {func_info.name}({params_str})")
else:
lines.append(f" result = {func_info.name}({params_str})")
lines.append("")
lines.append(" # 验证结果")
lines.append(" assert result is not None")
return lines
4.2 unittest 测试生成
# engine/test_gen/unittest_generator.py
class UnittestGenerator:
"""unittest 测试生成器"""
def __init__(self):
self.analyzer = FunctionAnalyzer()
self.strategy_generator = TestStrategyGenerator()
def generate(self, source_code: str, module_name: str = "my_module") -> str:
"""生成 unittest 风格的测试代码"""
func_info = self.analyzer.analyze(source_code)
test_cases = self.strategy_generator.generate(func_info)
lines = []
# 文件头
lines.append('"""')
lines.append(f"单元测试 - {func_info.name}")
lines.append('"""')
lines.append("")
lines.append("import unittest")
lines.append("from unittest.mock import patch, MagicMock")
lines.append(f"from {module_name} import {func_info.name}")
lines.append("")
# 测试类
class_name = f"Test{func_info.name.capitalize()}"
lines.append(f"class {class_name}(unittest.TestCase):")
lines.append(' """测试类"""')
lines.append("")
# setUp 方法
if func_info.calls_external:
lines.append(" def setUp(self):")
lines.append(" """测试前置准备"""")
for dep in func_info.calls_external:
lines.append(f" self.mock_{dep} = MagicMock()")
lines.append("")
# 生成测试方法
for tc in test_cases:
lines.extend(self._generate_test_method_unittest(func_info, tc))
lines.append("")
# 入口
lines.append("")
lines.append('if __name__ == "__main__":')
lines.append(" unittest.main()")
return '\n'.join(lines)
def _generate_test_method_unittest(self, func_info, tc) -> list[str]:
"""生成 unittest 测试方法"""
lines = []
lines.append(f" def {tc.name}(self):")
lines.append(f' """{tc.description}"""')
lines.append("")
# 准备输入
if tc.inputs:
for name, value in tc.inputs.items():
lines.append(f" {name} = {value}")
lines.append("")
# 调用
params_str = ", ".join(
f"{name}={name}" if name in tc.inputs else name
for name in [p.name for p in func_info.parameters]
if name in tc.inputs
)
if tc.should_raise:
lines.append(f" with self.assertRaises({tc.should_raise}):")
lines.append(f" {func_info.name}({params_str})")
else:
lines.append(f" result = {func_info.name}({params_str})")
lines.append("")
lines.append(" # 验证结果")
lines.append(" self.assertIsNotNone(result)")
return lines
五、测试验证器
5.1 测试质量检查
# engine/test_gen/validator.py
import ast
import sys
from io import StringIO
class TestValidator:
"""测试验证器"""
def validate(self, test_code: str) -> dict:
"""
验证生成的测试代码
返回:
{
"valid": True/False,
"syntax_ok": True/False,
"import_ok": True/False,
"coverage_estimate": 0.8,
"issues": [...]
}
"""
result = {
"valid": True,
"syntax_ok": True,
"import_ok": True,
"coverage_estimate": 0.0,
"issues": []
}
# 1. 语法检查
try:
ast.parse(test_code)
except SyntaxError as e:
result["syntax_ok"] = False
result["valid"] = False
result["issues"].append(f"语法错误: {e}")
# 2. 导入检查
if result["syntax_ok"]:
try:
# 尝试编译
compile(test_code, '<test>', 'exec')
except ImportError as e:
result["import_ok"] = False
result["issues"].append(f"导入错误: {e}")
# 3. 覆盖率估算
result["coverage_estimate"] = self._estimate_coverage(test_code)
# 4. 测试完整性检查
completeness_issues = self._check_completeness(test_code)
result["issues"].extend(completeness_issues)
return result
def _estimate_coverage(self, test_code: str) -> float:
"""估算测试覆盖率"""
# 简单的启发式:根据测试方法的数量和质量估算
test_methods = test_code.count('def test_')
if test_methods == 0:
return 0.0
elif test_methods <= 2:
return 0.3
elif test_methods <= 4:
return 0.6
elif test_methods <= 6:
return 0.8
else:
return 0.9
def _check_completeness(self, test_code: str) -> list[str]:
"""检查测试完整性"""
issues = []
# 检查是否有断言
assertions = ['assert ', 'self.assert', 'pytest.raises']
has_assertion = any(a in test_code for a in assertions)
if not has_assertion:
issues.append("测试中没有找到断言语句")
# 检查是否有边界测试
if 'boundary' not in test_code and 'edge' not in test_code:
issues.append("建议添加边界值测试")
# 检查是否有异常测试
if 'raises' not in test_code and 'assertRaises' not in test_code:
issues.append("建议添加异常路径测试")
return issues
六、完整测试生成管道
# engine/test_gen/pipeline.py
from .function_analyzer import FunctionAnalyzer
from .strategy import TestStrategyGenerator
from .pytest_generator import PytestGenerator
from .unittest_generator import UnittestGenerator
from .validator import TestValidator
class TestGenerationPipeline:
"""测试生成完整管道"""
def __init__(self):
self.analyzer = FunctionAnalyzer()
self.strategy = TestStrategyGenerator()
self.pytest_gen = PytestGenerator()
self.unittest_gen = UnittestGenerator()
self.validator = TestValidator()
def generate(self, source_code: str, module_name: str = "my_module",
framework: str = "pytest") -> dict:
"""
完整测试生成流程
返回:
{
"function_info": {...},
"test_cases": [...],
"test_code": "...",
"validation": {...},
"alternative_framework": "..."
}
"""
result = {}
# 1. 分析函数
func_info = self.analyzer.analyze(source_code)
result["function_info"] = {
"name": func_info.name,
"parameters": [p.name for p in func_info.parameters],
"return_type": func_info.return_type,
"complexity": func_info.complexity,
"external_deps": func_info.calls_external
}
# 2. 生成测试策略
test_cases = self.strategy.generate(func_info)
result["test_cases"] = [
{
"name": tc.name,
"description": tc.description,
"inputs": tc.inputs,
"should_raise": tc.should_raise
}
for tc in test_cases
]
# 3. 生成测试代码
if framework == "pytest":
test_code = self.pytest_gen.generate(source_code, module_name)
alt_code = self.unittest_gen.generate(source_code, module_name)
else:
test_code = self.unittest_gen.generate(source_code, module_name)
alt_code = self.pytest_gen.generate(source_code, module_name)
result["test_code"] = test_code
result["alternative_framework"] = alt_code
# 4. 验证测试
validation = self.validator.validate(test_code)
result["validation"] = validation
return result
七、完整演示
# test_testgen.py
from engine.test_gen.pipeline import TestGenerationPipeline
import json
# 待测函数
source_code = """
def calculate_discount(price: float, rate: float = 0.1) -> float:
\"\"\"
计算折扣后的价格
Args:
price: 原价
rate: 折扣率,默认为0.1(10%)
Returns:
折扣后的价格
Raises:
ValueError: 如果价格或折扣率为负数
\"\"\"
if price < 0:
raise ValueError("价格不能为负数")
if rate < 0 or rate > 1:
raise ValueError("折扣率必须在0到1之间")
discount = price * rate
return price - discount
"""
print("=" * 60)
print("🧪 单元测试自动生成")
print("=" * 60)
# 初始化管道
pipeline = TestGenerationPipeline()
# 生成测试
result = pipeline.generate(source_code, module_name="shop")
# 1. 函数分析结果
print("\n📋 函数分析:")
func_info = result["function_info"]
print(f" 函数名: {func_info['name']}")
print(f" 参数: {', '.join(func_info['parameters'])}")
print(f" 返回类型: {func_info['return_type']}")
print(f" 圈复杂度: {func_info['complexity']}")
print(f" 外部依赖: {func_info['external_deps'] or '无'}")
# 2. 测试策略
print(f"\n📝 测试策略 ({len(result['test_cases'])} 个用例):")
for tc in result["test_cases"]:
icon = "❌" if tc["should_raise"] else "✅"
print(f" {icon} {tc['name']}")
print(f" {tc['description']}")
if tc["inputs"]:
print(f" 输入: {tc['inputs']}")
if tc["should_raise"]:
print(f" 预期异常: {tc['should_raise']}")
# 3. 生成的测试代码
print(f"\n📄 生成的测试代码 (pytest):")
print("-" * 40)
print(result["test_code"])
print("-" * 40)
# 4. 验证结果
print(f"\n🔍 测试验证:")
validation = result["validation"]
print(f" 语法正确: {'✅' if validation['syntax_ok'] else '❌'}")
print(f" 导入正确: {'✅' if validation['import_ok'] else '❌'}")
print(f" 预估覆盖率: {validation['coverage_estimate']:.0%}")
if validation["issues"]:
print(f" 注意事项:")
for issue in validation["issues"]:
print(f" • {issue}")
# 5. 备选框架
print(f"\n🔄 备选框架 (unittest):")
print(result["alternative_framework"][:500] + "...")
八、实际运行测试
# run_generated_tests.py
import subprocess
import tempfile
import os
def run_generated_test(test_code: str, source_code: str):
"""
运行生成的测试
1. 创建临时目录
2. 写入源代码和测试代码
3. 运行 pytest
4. 返回结果
"""
with tempfile.TemporaryDirectory() as tmpdir:
# 写入源代码
src_file = os.path.join(tmpdir, "my_module.py")
with open(src_file, "w") as f:
f.write(source_code)
# 写入测试代码
test_file = os.path.join(tmpdir, "test_my_module.py")
with open(test_file, "w") as f:
f.write(test_code)
# 运行测试
result = subprocess.run(
["python", "-m", "pytest", test_file, "-v"],
capture_output=True,
text=True,
cwd=tmpdir
)
return {
"passed": result.returncode == 0,
"stdout": result.stdout,
"stderr": result.stderr
}
# 测试
test_code = result["test_code"]
run_result = run_generated_test(test_code, source_code)
print("\n🏃 测试运行结果:")
print(run_result["stdout"])
if run_result["stderr"]:
print("错误:", run_result["stderr"])
print(f"通过: {'✅' if run_result['passed'] else '❌'}")
九、常见错误 & 排坑指南
-
生成的测试无法运行
-
原因:模块导入路径不正确
-
解决:根据项目结构自动调整 import 语句
-
-
Mock 不完整
-
原因:函数有深层依赖未被 mock
-
解决:使用
mocker.patch.object进行更精确的 mock
-
-
测试过于脆弱
-
原因:测试依赖于具体实现细节
-
解决:关注行为而非实现,使用更宽松的断言
-
-
缺乏集成测试
-
原因:只生成了单元测试
-
解决:添加集成测试生成选项
-
十、课后作业
-
实现 Mock 自动生成:分析函数的外部依赖,自动生成对应的 Mock 代码。
-
添加参数化测试 :使用
@pytest.mark.parametrize减少重复测试代码。 -
挑战题:实现"测试驱动开发(TDD)模式"------用户先写测试描述,系统生成测试代码,用户再实现功能代码。
十一、总结
这一讲我们实现了单元测试自动生成:
-
函数分析:自动提取函数签名、参数、返回类型、异常
-
测试策略:正常路径、边界值、异常路径、特殊值全覆盖
-
代码生成:支持 pytest 和 unittest 两种框架
-
测试验证:语法检查、导入检查、覆盖率估算
-
实际运行:自动运行生成的测试并反馈结果
现在,MiniCopilot 不仅能写代码、审代码,还能自动为代码生成完整的单元测试套件。
下一讲,我们将实现代码重构与优化建议------让 MiniCopilot 帮助开发者改进现有代码。
🧰 开发之余,处理 Base64、JWT 解析、JSON 格式化、Crontab 计算、PDF 合并压缩这些碎片需求,我常用一个纯前端本地工具箱:zz365.top (子页 PDF 大师:PDF 大师 - zz365工具箱)。所有计算在浏览器完成,文件不上传服务器,关页即清。免费、无登录、无广告,适合开发者当常驻标签页。