AI 辅助单元测试生成:随机用例生成与暴力解兜底的验证链路

AI 辅助单元测试生成:随机用例生成与暴力解兜底的验证链路

AI 辅助单元测试生成:随机用例生成与暴力解兜底的验证链路

一、写测试比写代码还费劲

写完一道算法题的代码,还要写测试用例来验证。测试用例通常有三种:题目给的样例(太少)、你自己想的边界用例(凭经验)、以及随机生成的用例(可能覆盖到你没想到的情况)。

对于一个 AI 题解生成系统,测试用例不能靠人工手写。需要自动化地生成足够多的测试用例来覆盖代码的各种执行路径。这篇文章讨论一条完整的验证链路——从随机用例生成,到暴力解法兜底,再到 AI 驱动的变异测试。

二、验证链路架构

flowchart TD A[AI 生成的代码] --> B[随机用例生成器] B --> C[执行 AI 代码] B --> D[执行暴力解法] C --> E{结果一致?} D --> E E -->|一致| F[通过该用例] E -->|不一致| G[记录差异用例] F --> H{覆盖率达标?} H -->|否| I[变异现有用例] I --> B H -->|是| J[验证通过] G --> K[反馈给 AI 修正]

核心思路:用暴力解法作为答案的 Ground Truth。对于组合、排列、搜索类问题,暴力解虽然慢,但一定是对的。AI 代码可以和暴力解在小规模数据上对比,从而验证正确性。

三、实现

import random import itertools from typing import Callable, Optional from dataclasses import dataclass @dataclass class TestCase: """测试用例""" input_data: dict expected: any source: str # "brute_force" | "random" | "mutated" class RandomTestCaseGenerator: """随机测试用例生成器 根据题目约束条件,在合法范围内随机生成输入。 生成策略因题目类型不同而不同——数组题生成随机数组, 图论题生成随机图,DP 题生成满足约束的输入。 """ def __init__(self, constraints: dict): """ constraints 示例: { "type": "array", "n_min": 1, "n_max": 100, "val_min": -1000, "val_max": 1000, "sorted": False, "unique": False, } """ self.constraints = constraints def generate(self) -> TestCase: """生成一个随机测试用例""" ctype = self.constraints["type"] if ctype == "array": input_data = self._generate_array() elif ctype == "string": input_data = self._generate_string() elif ctype == "graph": input_data = self._generate_graph() else: raise ValueError(f"未支持的类型: {ctype}") return TestCase( input_data=input_data, expected=None, # 运行时由暴力解法填充 source="random", ) def _generate_array(self) -> dict: """生成随机数组输入""" c = self.constraints n = random.randint(c.get("n_min", 1), c.get("n_max", 20)) # 使用小数据:保证暴力解能在合理时间内完成 if c.get("unique"): # 生成不重复的随机数 vals = random.sample( range(c.get("val_min", -100), c.get("val_max", 101)), min(n, c.get("val_max", 100) - c.get("val_min", -100)), ) else: vals = [ random.randint(c.get("val_min", -100), c.get("val_max", 100)) for _ in range(n) ] if c.get("sorted"): vals.sort() return {"type": "array", "data": vals, "n": n} def _generate_string(self) -> dict: """生成随机字符串输入""" c = self.constraints n = random.randint(c.get("n_min", 1), c.get("n_max", 10)) charset = c.get("charset", "abcdefghijklmnopqrstuvwxyz") s = "".join(random.choice(charset) for _ in range(n)) return {"type": "string", "data": s, "n": n} def _generate_graph(self) -> dict: """生成随机图输入""" c = self.constraints n = random.randint(c.get("n_min", 2), c.get("n_max", 8)) edges = [] # 生成随机边,确保连通(简化:生成随机生成树 + 额外边) for i in range(1, n): edges.append((random.randint(0, i - 1), i)) # 添加一些额外边 extra = random.randint(0, min(n * 2, n * (n - 1) // 2 - (n - 1))) for _ in range(extra): u, v = random.randint(0, n - 1), random.randint(0, n - 1) if u != v: edges.append((u, v)) return {"type": "graph", "n": n, "edges": edges} class VerificationPipeline: """代码验证流水线 流程: 1. 随机生成 N 个测试用例 2. 用暴力解法计算期望输出(Ground Truth) 3. 用 AI 代码执行得到实际输出 4. 比较两者,输出差异报告 """ def __init__( self, ai_solution: Callable, brute_force: Callable, generator: RandomTestCaseGenerator, ): self.ai_solution = ai_solution self.brute_force = brute_force self.generator = generator def verify(self, num_cases: int = 50) -> dict: """执行验证""" results = {"passed": 0, "failed": 0, "failures": [], "timeout": 0} for _ in range(num_cases): test_case = self.generator.generate() try: # 用小数据跑暴力解(数据规模已在 generator 中控制) expected = self.brute_force(**test_case.input_data) test_case.expected = expected # 执行 AI 代码 actual = self.ai_solution(**test_case.input_data) except Exception as e: results["failed"] += 1 results["failures"].append({ "input": test_case.input_data, "error": str(e), }) continue # 对比结果 if actual == expected: results["passed"] += 1 else: results["failed"] += 1 results["failures"].append({ "input": test_case.input_data, "expected": expected, "actual": actual, }) results["pass_rate"] = ( results["passed"] / num_cases if num_cases > 0 else 0 ) return results class MutationTester: """变异测试:通过微调用例来探测代码边界 思路:把现有的通过用例做微小变化, 考验代码在处理边界情况时的鲁棒性。 """ def mutate(self, test_case: TestCase) -> list[TestCase]: """对测试用例进行变异,生成边界测试用例""" mutants = [] data = test_case.input_data if data.get("type") == "array": arr = data["data"] # 变异 1:空数组 mutants.append(TestCase( input_data={**data, "data": [], "n": 0}, expected=None, source="mutated", )) # 变异 2:全相同元素 if arr: mutants.append(TestCase( input_data={**data, "data": [arr[0]] * len(arr)}, expected=None, source="mutated", )) # 变异 3:极大/极小值 if arr: extreme = [10**9, -10**9] + arr[2:] mutants.append(TestCase( input_data={**data, "data": extreme}, expected=None, source="mutated", )) # 变异 4:单元素 if arr: mutants.append(TestCase( input_data={**data, "data": [arr[0]], "n": 1}, expected=None, source="mutated", )) return mutants

四、边界与权衡

4.1 暴力解的性能天花板

暴力解法在小数据上可用(n < 20),但生成器必须控制数据规模。如果题目在 n=10 时暴力解已经秒级,生成器的 n_max 就应该设到 8。这是一种在测试覆盖度和执行时间之间的权衡。

4.2 暴力解的"正确性"也有前提

暴力解本身也可能有 bug。如果题目类型不确定,建议使用两个独立实现的暴力解交叉验证:如果两者输出一致,confidential 程度更高。

4.3 随机用例的分布偏差

随机生成的用例可能集中在某个输入区域,缺乏边缘用例。变异测试是弥补这个不足的有效手段。

4.4 覆盖度 vs 置信度

50 个随机用例通过也不代表代码绝对正确。理论上只有形式化证明能给出 100% 的确定性。但在工程实践中,随机测试 + 边界测试能在合理时间内给出足够高置信度的正确性评估。

五、总结

AI 辅助测试生成的核心不是让 AI 写测试——那又回到了代码生成的不确定性问题。核心是用确定性方法(暴力解)来验证概率性输出(AI 代码)。这种「用慢但对的验证快但可能错的」模式,是 AI 生成代码质量保障的基础范式。