第6讲:单元测试自动生成

第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 '❌'}")

九、常见错误 & 排坑指南

  1. 生成的测试无法运行

    • 原因:模块导入路径不正确

    • 解决:根据项目结构自动调整 import 语句

  2. Mock 不完整

    • 原因:函数有深层依赖未被 mock

    • 解决:使用 mocker.patch.object 进行更精确的 mock

  3. 测试过于脆弱

    • 原因:测试依赖于具体实现细节

    • 解决:关注行为而非实现,使用更宽松的断言

  4. 缺乏集成测试

    • 原因:只生成了单元测试

    • 解决:添加集成测试生成选项


十、课后作业

  1. 实现 Mock 自动生成:分析函数的外部依赖,自动生成对应的 Mock 代码。

  2. 添加参数化测试 :使用 @pytest.mark.parametrize 减少重复测试代码。

  3. 挑战题:实现"测试驱动开发(TDD)模式"------用户先写测试描述,系统生成测试代码,用户再实现功能代码。


十一、总结

这一讲我们实现了单元测试自动生成:

  • 函数分析:自动提取函数签名、参数、返回类型、异常

  • 测试策略:正常路径、边界值、异常路径、特殊值全覆盖

  • 代码生成:支持 pytest 和 unittest 两种框架

  • 测试验证:语法检查、导入检查、覆盖率估算

  • 实际运行:自动运行生成的测试并反馈结果

现在,MiniCopilot 不仅能写代码、审代码,还能自动为代码生成完整的单元测试套件。

下一讲,我们将实现代码重构与优化建议------让 MiniCopilot 帮助开发者改进现有代码。


🧰 开发之余,处理 Base64、JWT 解析、JSON 格式化、Crontab 计算、PDF 合并压缩这些碎片需求,我常用一个纯前端本地工具箱:zz365.top (子页 PDF 大师:PDF 大师 - zz365工具箱)。所有计算在浏览器完成,文件不上传服务器,关页即清。免费、无登录、无广告,适合开发者当常驻标签页。

相关推荐
fthux2 小时前
装闭 RenoPit 源码解析(08):多模态AI调用、重试与文本降级
人工智能·ai·开源·github·open source·renopit
tachibana22 小时前
文件上传分布式限流如何做?
人工智能·ai·大模型·llm·prompt
问天_观心2 小时前
python之uv库的学习
开发语言·python
vx-程序开发2 小时前
django汽车租赁系统---附源码25360
java·javascript·spring boot·python·eclipse·django·php
笨鸟先飞,勤能补拙2 小时前
AI Agent应用领域深度解析:从概念到落地的全维度审视
大数据·人工智能·python·物联网·安全·网络安全·github
智码看视界2 小时前
Day49-AI微服务化-将大模型能力封装为标准微服务
java·微服务·ai·架构·大模型·sse流式输出·ai中台
北斗落凡尘2 小时前
LangGraph 入门实战(6)
python·langchain
安逸sgr2 小时前
激活函数有什么用?Sigmoid、Tanh、ReLU 到底怎么选?
人工智能·ai·大模型·agent·智能体
大模型码小白3 小时前
AI安全前沿:AI大模型安全防护的前沿技术
java·网络·人工智能·python·深度学习·学习·安全
evans在进步3 小时前
LeetCode 34:在排序数组中查找元素的首尾位置——Java 两次二分查找详解
java·python·leetcode