Industry & PracticeResearch & Benchmarks
软件 演化 下 的 LLM 测试 生成 评 测
Virginia Tech 与 CMU 的大规模实证研究全译:8 个模型、22,374 个程序变体。基线 79% 行覆盖,代码一改就掉——超过 99% 的失败测试在原始代码上能通过。
In this piece
软件演化下的 LLM 测试生成评测(中文全译)
翻译说明:本文是 arXiv 论文 Evaluating LLM-Based Test Generation Under Software Evolution(arXiv:2603.23443,2026-03-24 提交)的中文全译,由智测团队翻译。原作者:Sabaat Haroon(Virginia Tech)、Mohammad Taha Khan(Carnegie Mellon University)、Muhammad Ali Gulzar(Virginia Tech)。原文以 CC BY 4.0 许可发布,允许翻译与再分发,须署名。译文保留原文全部章节、数据与结论;参考文献列表与文内引注编号从略,模型名、基准名、指标名保留英文。实验数据与图表版权归原作者。
摘要
大语言模型(LLM)正被越来越多地用于自动生成单元测试。但目前仍不清楚:这些测试反映的是对程序行为的真正推理,还是只是复现训练中学到的表面模式。如果后者占主导,LLM 生成的测试可能暴露出重要弱点——覆盖率下降、漏掉回归、检不出缺陷。因此,理解 LLM 如何为一个程序生成测试、这些测试又如何响应代码演化,就变得至关重要。本工作对程序演化下的 LLM 测试生成做了一项大规模实证研究。我们用一个自动化的、变异驱动的框架,分析生成的测试如何响应语义改变型变更(semantic-altering changes,SAC)和语义保持型变更(semantic-preserving changes,SPC)。评测覆盖 8 个 LLM 和从广泛使用的基准派生出的 22,374 个程序变体。
LLM 取得了很强的基线结果:在原始程序上,测试套件全部通过时达到 79% 行覆盖率和 76% 分支覆盖率。然而随着程序演化,表现下降。在 SAC 下,新生成测试的通过率降到 66%,分支覆盖率降到 60%。在执行到被修改区域的 SAC 失败测试中,超过 99% 在原始程序上能通过,说明它们残留着对原始程序行为的对齐,而不是适应更新后的语义。在 SPC 下表现同样下降,尽管功能没有改变:测试通过率降到 79%,分支覆盖率降到 69%。SPC 编辑通常比 SAC 编辑引入更大的语法变化,却保持了语义,但它们在生成的测试套件中触发了更大的不稳定。平均而言,模型多产出 1.2 倍的新测试,同时丢弃许多基线测试,说明它对词汇层面的变化敏感,而不是对真正的语义影响敏感。总体而言,我们的发现表明:当前基于 LLM 的测试生成严重依赖表面线索,并且在程序演化时难以维持回归意识。
1. 引言
大语言模型正被越来越多地整合进软件工程工作流,自动测试生成是其中一个突出的用例。早期研究和基准表明,LLM 能为广为人知的公开编程任务生成语法有效、且往往语义正确的测试用例。然而,产出完整、可靠的自动化测试套件,需要对控制流、执行路径,以及所给实现的具体功能状态做深入推理。真实世界的软件本质上是动态的:代码被频繁复用、重构、微调,以服务不同的功能目的。当开发者把这样修改过的代码交给 LLM、要求生成高覆盖率测试套件时,他们期望得到的测试能准确反映当前代码。
问题。 当前关于 LLM 测试生成的研究,大多忽视了代码演化如何影响模型的稳健性和行为。这个空白留下了一个关键问题:LLM 是真正理解了所给代码的语义,还是在对预训练中见过的程序做浅层的模式复制?
设想这样一个场景:开发者拿一个已有的开源函数,做一处小的语义改变型修改,让它适配一个新用例。即使明确要求为这段新代码生成高覆盖率测试,一个理想的测试生成器(例如随机测试生成器、模糊测试器或符号执行)也应当适应所给的逻辑。然而,如果 LLM 严重依赖记忆的结构,它可能隐含地假设被修改的代码是原始程序的一个「有 bug 的版本」。它也可能完全忽略代码变更的语义含义。结果,它可能生成在所给代码上失败、却自相矛盾地在训练时见过的原始未修改版本上通过的测试用例。反过来,如果开发者施加一处语义保持型变异——例如重命名变量或在不改变输出的情况下重构逻辑——生成的测试覆盖率和通过率应当保持稳定。如果 LLM 在这类无害的词汇变化下都无法保持韧性,它在真实测试生成场景中的效用就打了折扣。
当前技术水平。 传统测试生成系统和模糊测试器被设计来最大化代码覆盖率,而现代 LLM 代码生成基准(如 HumanEval、LiveCodeBench)几乎只关注代码生成任务中的功能正确性。两种方法都没有刻画 LLM 的测试生成行为如何随软件演化而变化。尽管变异测试是评估测试套件充分性的标准技术,它还没有被系统地适配来评估程序变更下的 LLM 测试生成。目前尚不清楚 LLM 能否在适应功能变更的同时,对不影响行为的变更保持韧性。
贡献。 评估 LLM 测试生成的一个挑战,是区分真正的语义推理与对训练中观察到的模式的对齐。仅在静态基准上的高覆盖率无法回答这个问题。一个模型可能看起来有效,仅仅因为被测程序与训练中遇到的实现相似,使它无需针对具体代码实例推理就能复现熟悉的测试结构。为解决这一局限,我们在受控的程序变更下评估 LLM 行为。
我们引入一个变异驱动的评测框架,施加两类代码变更。语义改变型变更(SAC) 产生行为被修改的程序变体,要求测试适应新语义。语义保持型变更(SPC) 修改代码表面而保持功能。由于程序行为不变,SPC 下的退化揭示的是对结构或词汇变化的敏感,而不是语义推理。通过分析 LLM 在两类变更下的行为,我们得以区分表现差异究竟来自语义误解,还是对表面代码模式的依赖。这个双重视角让我们能刻画 LLM 测试生成的三个性质:对语义变化的敏感性、对非功能性结构变化的韧性,以及生成测试套件在程序演化中的稳定性。
我们设计了一个自动化的端到端评测框架:生成基线测试、测量覆盖率、注入 SAC 和 SPC,并跨程序变体评估测试质量。除覆盖率指标外,我们还分析失败测试,判断它们的断言是对齐被变异程序的行为,还是对齐原始程序的语义。这一分析让我们能直接评估 LLM 生成的测试反映的是对程序行为的真正推理,还是浅层模式复制——这是静态公开基准无法提供的洞察。
实验结果。 我们用 22,374 个程序变体评估了 8 个最先进的 LLM,这些变体来自 Project CodeNet 数据集,聚焦 Java 和 Python 实现。基线评测显示,LLM 在原始未修改程序上表现良好:测试套件全部通过时,平均达到 79.2% 行覆盖率和 76.1% 分支覆盖率,平均每个程序 13.1 个测试。然而在代码变更下,这一表现急剧退化。施加 SAC 时,新生成测试的通过率骤降到 66.5%,分支覆盖率降到 60.6%。关键是,对失败测试用例的进一步分析发现:在 SAC 下生成的 119,163 个测试中,在被变异代码上失败的 23,977 个测试里,超过 99% 在执行到被修改区域时能在原始程序上通过。这些结果支持我们的假设:LLM 缺乏对语义代码变更进行精确推理的能力,转而严重依赖训练中观察到的模式。因此,它们常常无法捕捉演化程序引入的行为变化,也很少生成反映更新后语义的测试。
此外,施加 SPC 时,测试通过率从 100% 降到 79%,分支覆盖率降到 69%。尽管功能相同,LLM 把 SPC 修改后的代码解读为截然不同的程序,触发的新测试生成量是 SAC 下的 1.2 倍。我们把这一行为归因于 SPC 引入的略大的语法变化——通常跨 1 到 2 行代码,而 SAC 被限制为单行变更。结果是,语法影响更大但无语义效果的变更导致生成测试套件的更大差异,而有真实语义影响但语法变化极小的变更产生的偏移更小。然而,一个具备有意义代码推理能力的模型应当对这类 SPC 保持稳健。相反,我们的发现表明:更大的编辑距离——即便语义无关——会扰乱 LLM 的模式匹配并触发对测试的探索性再生成,导致测试套件不稳定。总体而言,这些发现说明基于 LLM 的测试生成严重依赖表面模式和记忆结构,揭示出理解和适应演化代码的能力有限。
数据可用性。 本研究使用的制品和数据集公开可得:https://doi.org/10.5281/zenodo.18898624 。
2. 引例
我们给出一个基于 CodeNet Python 数据集实例(ID:p02701_s025427336)的引例。该程序读入一个整数 n,随后读入 n 个字符串,按长度(1 到 10 个字符)分组,并打印所有长度下不同字符串的总数。
我们首先提示 GPT-5.2 为原始未修改程序生成测试套件。模型成功生成了 7 个测试用例,达到 100% 行覆盖率和 81.0% 分支覆盖率,测试通过率 100%。这建立了一个强基线,说明问题是可解的,且 LLM 能通过为字符串分桶逻辑生成可执行、通过的测试用例来满足高代码覆盖率目标。
接着,我们通过注入一处改变语义的代码变更来模拟一次功能扩展。具体来说,我们修改输入读取逻辑,使其只处理前 n-1 个条目,实际上把最后一个输入当作终止符或元数据,而不是数据点。这个变更建立了一个新的有效功能需求:程序有意忽略序列中的最后一个字符串。注意我们不追求功能对齐,因为没有向 LLM 提供代码的功能需求。相反,我们要求 LLM 以代码覆盖率为主要目标,这与自动化测试生成文献中的做法一致。
然后我们把这个修改版本提供给一个独立的 LLM 实例,指示它从零生成高覆盖率测试套件。通过使用全新会话,我们确保模型对之前的基线代码没有记忆,迫使它完全依赖所给代码进行推理。
原文图 1 给出了注入 SAC 和 SPC 的 CodeNet 程序片段:SAC 把 for _ in range(n) 改为 for _ in range(n - 1)(循环边界从包含改为排除),SPC 在第一个长度检查后注入一个冗余的 else: pass。
评估为这个变异体生成的测试时,我们观察到行覆盖率中等程度地降到 68%,分支覆盖率降到 50%。然而测试通过率降到 82%,意味着若干测试失败。仔细检查后,图 3 所示的具体失败测试用例 test_all_lengths_unique 尤其有启发:这个测试生成的输入恰好喂入 10 个不同字符串,走一遍分桶逻辑。然而,尽管提示词里没有外部规格,模型明确断言程序会输出「10」。这揭示出:LLM 没有从所给的被变异代码推导期望行为,而是强行把断言匹配到它在预训练中观察到的原始算法规格。
在原始程序上,这个测试通过。施加 SAC 后,循环在 9 次迭代后终止,第 10 个字符串(「jjjjjjjjjj」)从未被读取,输出是「9」,导致断言失败。LLM 未能把期望输出调整为「9」,表明缺乏语义根基。模型没有从所给代码推导断言,而是表现出残留对齐:它从训练数据中幻觉出标准算法的需求,忽略当前实例显式的功能逻辑。
为进一步评估稳健性,我们回到原始基线代码,施加一处语义保持型代码变更(SPC)。我们在第一个长度检查之后立即插入一个纯冗余的 else: pass 块(见图 1)。因为这个变更保持了完全相同的控制流和程序行为,一个稳健的 LLM 应当生成与基线相当的测试套件。相反,SPC 导致行覆盖率降到 79%,分支覆盖率降到 64%。更令人担忧的是,测试通过率降到 80%。
这一退化揭示出对语法可见性的过度依赖,代价是语义推理。因为 SPC 常常引入高度可见的结构修改,例如插入一整块新代码,LLM 容易察觉表面的扰动,却难以识别它的语义影响,从而不必要地丢弃有效测试并生成新的、更差的套件。反过来,SAC 通常有极小的语法足迹(例如修改单个运算符),却承载更大的语义含义。因为这些行为偏移在视觉上很微妙,LLM 经常忽略它们。
3. 研究问题
原文表 1 列出了施加到种子程序上的语义改变型和语义保持型代码变更类型:
语义改变型代码变更(SAC) 包括:边界偏移(修改循环或数组索引的包含/排除范围,如 range(n) → range(n-1))、布尔逻辑改变(切换布尔运算符,如 a && b → a || b)、算术改变(切换算术运算符,如 a + b → a - b)、参数交换(重排函数调用中同类型的参数,如 f(sum, price) → f(price, sum))、变量角色重绑定(重新分配变量的逻辑角色)。
语义保持型代码变更(SPC) 包括:空循环注入(插入执行但无操作的哑循环,如 for i in range(1): pass)、空条件(插入总是执行但什么都不做的哑条件)、冗余 else(给缺少 else 的语句加空 else 块)、等价比较(把比较运算符重写为逻辑等价形式,如 x < 10 → !(x >= 10))、未使用参数(给方法签名加一个未使用参数并更新调用)、误导性变量(把变量重命名为通用或误导性名字,如 count → sum)、误导性注释(加上错误描述代码行为的注释)、误导性中文注释(加上翻译成中文的误导性注释,如 // 返回总和)、移除注释(删除所有单行和多行注释)。
为指导这项关于程序变更下 LLM 测试的研究,我们调查以下研究问题:
- RQ1:LLM 能在多大程度上为处于初始状态的软件基准生成结构有效、高覆盖率的测试套件?
- RQ2:LLM 对语义改变型代码变更有多敏感——这类有意义的变更本应导致测试适应?
- RQ3:LLM 对语义保持型代码变更有多大韧性——这类变更下程序行为功能上完全相同?
- RQ4:如何解释 LLM 生成的测试在语义改变型代码变更下的失败?
- RQ5:LLM 生成的测试套件在多大程度上表现出回归意识?
4. 方法
我们设计了一个自动化流水线来对 LLM 生成测试套件的适应能力做基准测试。如图 2 所示,我们的框架让模型同时接受 SAC 和 SPC,以量化它与当前代码状态保持对齐的能力。第一阶段,我们建立基线来评估模型的初始生成能力。我们提示 LLM 为一组种子程序生成测试套件,并严格只保留那些所有生成测试都能完美编译并通过的程序。这确保后续阶段观察到的任何退化都直接归因于注入的代码变更,而不是对原始代码库的整体理解无能。第二阶段评估模型对代码变更的适应性。我们施加 SAC(详见表 1)来表示功能变更,例如改变循环边界或算术运算符;再施加 SPC(同样列于表 1)来表示非功能性代码重构,例如插入冗余条件或重命名变量。虽然真实世界的提交和拉取请求反映了自然的开发者行为,但使用它们会引入一个关键的数据污染问题——LLM 在预训练中已经接触过这些开源提交及其对应的回归测试。此外,大规模手工打造新颖、真实的拉取请求并不可行。因此,系统施加的变异是代码变更的可扩展、稳健的代理。
为便于统一评估,我们采用标准化的 two-shot 提示策略,其中第二 shot 在生成的测试因错误无法编译时为 LLM 执行一次修复循环,这一策略在流水线所有阶段一致应用。提示词指示模型扮演专家开发者,完全基于所给代码片段生成高覆盖率、成功通过的测试套件。我们没有采用自定义提示设计,而是使用先前 LLM 单元测试生成研究中常用的提示策略,即指示模型直接从所给实现生成测试。如果 two-shot 尝试后测试仍有错误,则该特定程序版本的测试生成过程被标记为失败,程序被排除。
4.1 种子程序获取
我们从 Project CodeNet 数据集获取种子程序,这是一个大规模、广受认可的基准。我们的实证研究聚焦 Java 和 Python——两种广泛使用的编程语言,在真实软件系统和 LLM 预训练语料中都有大量代表。通过用来自 CodeNet 的 5,723 个候选程序(3,231 个 Java 和 2,492 个 Python)初始化流水线,我们以显著超过标准测试生成基准(如 HumanEval 的 164 题或 MBPP 的 974 题)的规模评估模型。这种大规模选择确保我们的发现稳健,并建立在一个已被众多先前实证软件工程研究成功使用的、经过验证的数据集之上。我们遵循以下标准:
- 数据集标准:我们强制一个严格的质量过滤,要求所有选中程序都能原生编译、执行,且严格自包含。这确保模型收到完整的逻辑状态,无需依赖外部依赖。
- 规模:我们用 5,723 个候选程序(3,231 个 Java 和 2,492 个 Python)初始化流水线。
- 分层抽样:我们按代码行数(LOC)百分位把数据集划分为四个系统层:0-25%、25-50%、50-75%、75-100%。通过用这些四分位数定义边界,每个区间包含数量相同的候选项目,而不是依赖任意的行数上限。从这些等频区间均匀抽样通过的项目,保证了跨所有代码复杂度的均衡评估,防止结果偏向小文件或大文件。
4.2 基线测试生成与程序过滤
原文表 2 列出了被评估的 LLM:GPT-OSS(OpenAI,20B,开源)、Nemotron-3-Nano(NVIDIA,30B,开源)、GPT-5(OpenAI,未公开,闭源)、GPT-5.2(OpenAI,未公开,闭源)、Claude 4.5 Haiku(Anthropic,未公开,闭源)、Claude 4.6 Sonnet(Anthropic,未公开,闭源)、Gemini 2.5-Flash(Google,未公开,闭源)、Gemini 3.1-Pro(Google,未公开,闭源)。
对每个种子程序,我们指示一组 8 个 LLM(列于表 2)生成针对原始代码逻辑的高覆盖率测试套件。选择这些模型是因为它们在当前文献中的突出地位,既包括广泛使用的开源模型,也包括专有替代品。开源模型在配备 NVIDIA L40S GPU 的本地服务器上运行,闭源模型通过各自的 API 访问。生成后,测试套件在原始程序上执行,以建立正确性和代码覆盖率的基线。
我们系统地把最终基线集限制为每个 LLM 恰好 100 个完全通过的项目。使用获取阶段定义的四个行数区间,我们持续生成并评估基线测试套件,直到从四个规模区间中每个都成功识别出 25 个通过的项目。这种分层抽样保证我们的评估在不同代码复杂度间彻底均衡,不被数量失衡的琐碎片段或过大文件扭曲。关键的是,我们只选 100 个程序,因为每个基线程序都要独立接受 14 种不同的代码变更(涵盖 SAC 和 SPC 的所有类别),初始的 100 个基线程序因此扩展到每个被评估 LLM 近 1,400 个独立测试套件。这种巨大的乘数效应确保了对代码演化的统计严格、大规模评估,同时在 8 个不同模型间保持计算可行。
我们强制一个严格的过滤条件:只有当 LLM 成功生成在原始代码上通过率 100% 的测试套件时,程序才被选入最终的 100 项目基线。任何模型产出语法错误、编译失败或至少一个失败测试的程序都被丢弃。如果一个模型从根本上无法理解原始程序以生成有效测试,那么评估它对后续代码变更的响应只会产生混淆的结果。
这个严格的过滤步骤创建了一个无可挑剔的对照组。它保证 LLM 对每个保留程序都展示了绝对的基线胜任力。因此,我们可以确定地把后续阶段观察到的测试通过率或覆盖率的任何退化直接归因于注入的变异,完全隔离代码演化的影响,排除固有任务歧义或基线生成缺陷。
4.3 语义改变型代码变更
我们对过滤后的种子程序施加语义改变型代码变更。这些变更代表与原始代码的功能性偏离,模拟程序逻辑被更新以满足新操作需求或边界条件的常见场景。在这一阶段,我们把修改后的代码提供给一个独立的 LLM 实例,指示它生成高覆盖率测试套件。这个生成过程确保模型把代码当作主要参考,让我们能在不受先前版本影响的情况下评估它的语义对齐。这使我们能测量语义变化如何影响测试生成,包括测试套件规模、结构多样性和代码覆盖率的变化。
表 1 列出了所用的 SAC。举两个具体例子。第一,边界偏移变更把循环条件从 range(n) 改为 range(n-1)。期望行为是 LLM 生成反映期望输出中少一次迭代的测试。第二,算术改变变异把加法运算符(a + b)换成减法运算符(a - b),要求 LLM 生成检查完全不同数学结果的断言。
4.4 语义保持型代码变更
为评估 LLM 测试生成的稳健性,我们对过滤后的基线程序施加语义保持型代码变更(SPC)。真实世界的软件维护频繁涉及不改变底层执行逻辑的代码重构、变量重命名和文档更新。一个可靠、可用于生产的测试 agent 必须在这些功能上完全相同的变体间保持一致表现。
我们把重构后的代码提供给一个独立的 LLM 实例,指示它生成高覆盖率测试。因为 SPC 严格保持程序语义,期望行为是 LLM 识别出未变的执行逻辑,并生成覆盖率和通过率与基线场景相当的测试套件。覆盖率或通过率的任何退化都是实证证据,表明模型过度依赖表面的语法或词汇线索,而不是主动的代码理解。
表 1 列出了 SPC。在我们的整个变异流水线中,SAC 平均每个程序修改 1.0 行,而 SPC 平均修改 2.4 行(有些影响到多达 7 行);我们随后分析这个更大的语法编辑规模如何影响测试套件稳定性(5.5 节)。举两个 SPC 的具体例子。第一,等价比较变异把条件语句从 x < 10 重写为逻辑等价的 !(x >= 10)。期望行为是 LLM 识别出相同的边界条件并生成完全相同的测试断言。第二,误导性变量变异可能把一个简单循环计数器从 count 重命名为 sum。一个稳健的模型应当从施加于变量的实际操作推导理解,而不是被欺骗性标识符诱骗去生成关于加法的无关断言。我们把 SPC 模拟为三个不同组,完整细节见表 1:
- 结构重构:我们引入严格保持行为的控制流变换(例如注入空循环或冗余 else),测试 LLM 是否会被无害的、无操作的执行路径的存在搞混。
- 标识符重构:我们系统地把变量重命名为通用或上下文上具欺骗性的标识符,评估模型是否严重依赖语义命名约定而非程序逻辑。
- 注释重构:我们修改、添加或移除注释来操纵模型的文本上下文,包括注入跨语言的误导性中文注释,以评估逻辑推理是否会被与实际代码行为矛盾的文本产物带偏。
4.5 失败归因分析
针对第 1 节的 RQ4,我们扩展评测流水线,加入一项失败归因分析,应用于 LLM 在带 SAC 的程序上生成的测试。对每个 SAC,我们收集所有在变更程序上执行时失败的 LLM 生成测试。对每个这样的失败测试,我们执行两项额外检查:
原始程序执行。 失败测试在程序的原始未修改版本上执行。如果测试在原始程序上也失败,则失败归因于糟糕或畸形的测试生成,而不是与程序逻辑的失配。
代码变更执行覆盖。 我们验证测试是否执行了被改动的语句或控制流区域,确保观察到的失败与代码变更有因果关系,而不是无关的执行路径。一个失败测试被归类为与原始程序语义残留对齐,如果它同时满足两个条件:(1)在原始程序上执行时通过,且(2)在被变异程序上失败的同时执行了被变异的代码区域。这一分析验证了我们的假设:LLM 倾向于把代码匹配到类似训练中见过的程序的规格。因此,它们经常忽略有重大语义影响的代码变更,生成的测试仍与原始程序行为对齐,即使被修改的程序表现出偏离的语义。
5. 实验结果
本节呈现我们在代码变更下对 LLM 测试生成的大规模评估的实证结果。在整个评测流水线中,我们在被评估模型上运行了 22,374 个测试生成任务,消耗约 3.46 亿 token(包括输入提示和生成输出)。
5.1 RQ1:未修改程序上的基线表现
我们首先建立对照。对每个种子程序,我们提示 LLM 为原始未修改代码生成测试套件。为确保我们评估的是变异的影响,而不是模型为该程序生成测试的固有能力,我们严格只保留生成测试达到 100% 通过率的那些程序。
虽然这个过滤步骤保证了后续阶段的对照组,但追踪每个模型为拿到这 100 个通过程序所需的尝试次数,揭示了基线测试生成能力的显著差异。如图 5 所示,LLM 的效率在不同模型和编程语言间差异巨大。像 GPT-5、Gemini 3.1 Pro 和 Claude 4.6 Sonnet 这样能力强的模型只需相对较少的尝试就达到目标阈值(例如 Claude 4.6 Sonnet 尝试 180 个程序就拿到 100 个通过的 Java 套件)。相反,我们观察到若干模型在 Python 测试生成上有严重的性能瓶颈。例如 Gemini 2.5 Flash 在 Python 上需要 2,155 次尝试,而 Java 上只需 323 次。这表明在 Python 中产出完全自包含、语法正确、逻辑合理的测试套件,对当前 LLM 构成了明显更高的基线挑战。
我们把这一差异主要归因于两种语言类型系统的根本不同。Java 的静态强类型强制所有变量、方法参数和返回类型都有显式类型声明。这种丰富的语法结构为 LLM 提供了确定性的上下文,大幅减少了有效测试输入和期望行为的搜索空间。相反,Python 的动态类型要求模型从周围控制流隐式推断期望的数据类型和对象结构。这种对隐式类型推断的依赖迫使 LLM 频繁做假设,显著增加了生成类型不匹配输入、调用不兼容方法,或在测试生成中幻觉出错误对象结构的概率。除类型系统外,这一性能差距也可能源于模型训练语料中正式 Python 测试的稀缺。Python 大量用于计算笔记本等探索性环境,开发者在那里很少写结构化单元测试。
分析基线测试失败。 分析被丢弃的程序揭示了 LLM 在引入任何代码变更之前就在哪里挣扎。阻止测试套件达到 100% 通过率的失败大致分两类:
- 语法与环境错误:很大一部分失败(即 39% 的失败,尤其在 Python 中)源于结构性幻觉。模型频繁生成试图导入不存在的测试工具、或幻觉出违反我们自包含数据集标准的外部文件依赖的测试。在 Java 中,失败常涉及不正确的类实例化或不匹配的访问修饰符(例如试图在不使用反射的情况下测试私有辅助方法)。
- 逻辑断言失败:在 61% 的被丢弃案例中,测试成功编译但在执行时失败,因为 LLM 从根本上误解了程序的边界情况。这些逻辑失配证明了我们过滤阶段的必要性——如果一个模型无法正确断言原始算法的行为,它在变更版本上的表现就无法产生可靠洞察。
关键结论:LLM 难以生成语法有效的测试,因为该任务需要深入的代码理解,而不只是代码生成。因此,模型在 Java 上表现明显好于 Python,因为强类型减少了歧义。
5.2 RQ2:语义改变型代码变更的影响
对每个带 SAC 的程序,我们在一个隔离实例中提示 LLM 为更新后的代码生成测试套件,并直接在变更程序上评估其有效性。在所有被评估程序中,一旦程序行为改变,测试生成有效性就明显恶化。平均测试通过率从基线的 100% 降到 66.5%,下降 33.4 个百分点。覆盖率也大幅下降。行覆盖率从 79.3% 降到 67.4%(减少 11.9 个百分点,或下降 15.0%),分支覆盖率从 76.1% 降到 60.6%(减少 15.5 个百分点,或下降 20.4%)。这些下降表明,一旦程序行为偏离原始版本,LLM 生成的测试经常无法捕捉更新后的逻辑,执行的相关程序路径也更少。
除整体性能下降外,还出现几个趋势。第一,分支覆盖率比行覆盖率退化更多。这说明 LLM 在推理功能变更引入的改变后的控制流决策时尤其挣扎,尤其当语义改变型变更直接发生在分支谓词内部时。虽然生成的测试可能仍执行程序的部分,它们常常无法构造探索新引入分支或修改后条件逻辑的输入。
第二,我们观察到一个称之为散弹式测试(Scattershot Testing)的现象:尽管覆盖率严重下降,生成测试的总数实际上略有增加(从平均 13.2 到 13.9)。这表明当 LLM 遇到偏离已识别程序模式的改变逻辑时,它们失去了构思最优测试策略的能力。模型不是针对新执行路径,而是试图通过生成更多浅层测试来暴力覆盖。这种散弹式方法人为抬高了测试数量,却未能触及改变后的控制流,完美解释了测试量上升与分支覆盖率骤降的同时发生。
图 4 进一步展示了这些模式在不同语义改变型代码变更类别间的分布。边界偏移保持了明显更高的行覆盖率(73.4%),相比其他变异,说明生成的测试仍能执行受影响循环或边界的部分。然而存在一个持续的差距:在每一个 SAC 类别中,分支覆盖率都明显低于行覆盖率。这强化了如下观察:虽然 LLM 成功生成输入来触发沿主执行路径的改变后代码块,它们却难以提供探索新逻辑所有替代分支和条件结果所需的多样化测试用例。
关键结论:即使小的功能变更也会损害测试性能。模型在新控制流路径上挣扎,倾向于基于旧程序结构生成测试。
5.3 RQ3:语义保持型代码变更的影响
与 SAC 一样,我们在施加 SPC 后重新生成测试套件,并在重构后的代码上评估它们。尽管底层语义与基线完全相同,我们仍观察到测试生成性能的可测量下降。相比基线,平均测试通过率从 100% 降到 78.9%,下降 21.0 个百分点。覆盖率指标表现出相应退化。平均行覆盖率从 79.3% 降到 73.7%(减少 5.6 个百分点),分支覆盖率从 76.1% 降到 69.2%(减少 6.9 个百分点)。虽然这些下降小于语义改变型变更下观察到的,但鉴于执行行为完全没有改变,它们仍令人警惕。
这些结果出现几个模式。第一,分支覆盖率再次比行覆盖率下降更多,说明即使纯结构重构也会扰乱模型推理控制流探索的能力。虽然生成的测试仍执行程序中的许多语句,它们经常无法构造演练替代决策路径的输入。第二,在我们先前对语义改变型变更的分析中,模型通过生成更多测试来过度补偿。相反,在语义保持型变更下,生成测试的平均数量从 13.2 降到 12.1。这种截断在未使用参数变更下最为极端,模型平均只生成 8.1 个测试,导致行覆盖率骤降到 65.7%。这表明一种「分心效应」:当面对噪声或多余的结构元素时,模型耗费推理能力去解读死逻辑,导致测试套件突然被截断且不完整。
这些观察表明,LLM 的测试生成主要受修改的语法「表面积」影响,而不是它的语义效果。语法变化更大但无语义影响的变更(例如 SPC)导致生成测试的实质性偏离,而低语法、高语义的变更(例如 SAC)常常未被察觉。如图 6 所示,SPC 持续地比行覆盖率更多地降低分支覆盖率,凸显模型对结构噪声而非真实程序语义的敏感。
关键结论:基于 LLM 的测试生成对代码变更的语法和词汇足迹敏感,而不是对它们真正的语义影响敏感。
#### 5.3.1 讨论:不同 LLM 间的代码变更敏感性
图 7 提供了一个整合的、按模型细分的视图,展示不同类型的代码变更如何影响 LLM 生成的测试。首先,汇总数据确认了一个普遍的退化层级:行为变更(SAC)可预测地在所有模型上造成最灾难性的失败,而纯结构重构(SPC)在代码功能相同的情况下持续诱发退化。没有一个被评估的模型能完全免疫于 SPC 引入的结构噪声。即使像 Claude 4.6 Sonnet 和 GPT-5 Mini 这样最先进的模型,也仅仅因为结构重构就经历了大约 10 个百分点的通过率下降。这强化了如下结论:当前 LLM 测试生成严重依赖表面模式匹配,而不是稳健的语义理解。
其次,细分揭示了韧性上的深刻差异。我们观察到三种不同的代码变更敏感性画像:
高韧性(例如 GPT-5 Mini、Claude 4.6 Sonnet):这些模型展示了最强的适应性。特别是 GPT-5 Mini,在 SAC 下保持 82.9% 的通过率,在 SPC 下保持 90.9% 的通过率,是所有被评估模型中最高的保持率。它们在被给予改变后的代码时,表现出更强的更新内部上下文的能力。
高基线但更高敏感性(例如 GPT-5.2):令人惊讶的是,虽然 GPT-5.2 取得了最高的基线行覆盖率(85.6%),它对语义演化却很脆弱。面对 SAC 时,它的通过率下降近 44 个百分点(降到 56.2%)。这表明严重的「记忆过拟合」。
规模瓶颈(例如 Nemotron-3-Nano):更小的模型在演化上下文上挣扎。Nemotron-3-Nano 的通过率在 SAC 下降到 40.7%,在 SPC 下降到 55.9%,说明有限参数的模型缺乏可靠处理即使微小代码变更的推理深度。
并排比较这些情况揭示了 AI 辅助软件工程的一个关键局限。虽然 LLM 在公开基准的 two-shot 测试生成上非常熟练,它们对行为偏移和语法噪声的双重敏感,使它们在代码不断演化和重构的持续集成环境中成为高度不可靠的伙伴。
5.4 RQ4:SAC 下的失败归因
图 8 消歧了在语义改变型代码变更下观察到的测试失败的原因。对每个变更类别,图中报告了生成测试的总数、在被修改程序上失败的测试数,以及在原始程序上重新执行时通过的失败测试子集。
在所有类别中,我们分析了 119,163 个生成的测试,其中 23,977 个在被修改程序上执行时失败。当这些失败测试在对应的原始程序上重新执行时,23,737 个测试通过,同时仍执行被修改的代码区域,产生超过 99% 的残留对齐率。
这一模式在所有语义改变型变更类型中一致,包括算术变更、逻辑条件变更、边界偏移、参数交换和变量角色重绑定,归因率从 98.6% 到 100%。失败测试在原始代码上近乎完美的恢复表明,测试本身并非畸形;相反,它们仍与原始实现的行为对齐。
关键结论:LLM 生成的测试仍与原始代码对齐,而不是适应更新后的逻辑。
5.5 RQ5:回归意识
理想情况下,当代码演化时,测试生成器应当保留先前有效的、高质量的测试行为,只添加或修改演练更新后行为所必需的测试。为分析这一性质,我们通过识别匹配的、新生成的和丢失的测试,来追踪测试套件在程序版本间的连续性。我们用两个测试用例的覆盖率画像来判断它们是否匹配,即它们是否演练相同的代码行。
#### 5.5.1 代码变更下的测试套件演化
图 9 总结了测试套件在 SAC 和 SPC 下如何演化。我们还测量了每个变更类别修改的行数。语义改变型变更平均每个程序修改 1.0 行,而语义保持型变更平均修改 2.4 行,有些变更影响到多达 7 行。SPC 通常引入更大的结构编辑,可能导致 LLM 把改变后的程序当作不同的制品,增加了语义保持型变更下更少测试被复用的可能性。
在 8,585 个启用 SAC 的程序中,我们观察到高水平的测试套件不稳定。当功能改变时,LLM 平均生成 9.7 个全新测试,同时丢弃 8.8 个先前有效的测试,导致每次评估 18.5 个测试的套件翻动。结果,基线与被变异代码之间的平均测试匹配率仅为 29.3%。这表明当程序逻辑偏移时,模型未能适应现有测试,反而选择从头重新生成套件的大部分。因此,许多高覆盖率测试丢失,而新生成的测试达到更低的整体覆盖率,说明 LLM 产出的测试不如先前的高价值测试有效。
令人警惕的是,这种不稳定在 10,563 次 SPC 评估下恶化。尽管语义保持型变更让底层程序语义不变,LLM 表现出更高的翻动(每次评估 22.7 个测试),生成 11.8 个新测试并丢弃 10.9 个现有测试。这把平均匹配率拉低到 18.1%。而且由于 SPC 后重新生成的测试套件的总覆盖率总是下降,被丢弃的测试比新生成的测试更具覆盖率效益。
关键结论:LLM 在代码变更时依赖语法线索;它们经常从头重新生成测试套件,丢弃高覆盖率测试并用低覆盖率测试替代。
6. 讨论
我们研究的结果揭示了演化代码下 LLM 测试生成行为的两个关键洞察。第一,模型严重依赖表面语法线索,而不是变更的真实语义影响。结果是,即使程序行为保持不变,结构重构也能显著扰乱生成的测试套件。第二,更大的表面编辑似乎把模型推离简单模式匹配,鼓励它们尝试对代码逻辑做更深入的推理。这表明代码变更的幅度强烈影响 LLM 如何解读和响应被修改的程序。
这些观察指向改进 LLM 驱动测试生成的潜在方向。一个有前景的方法是在把代码变更呈现给模型之前先做预处理。例如,计算代码 diff 并显式高亮被修改的区域,可能有助于把模型的注意力引导到程序的相关部分。先前关于生成代码 diff 的自然语言摘要和自动提交信息的工作,也可能有助于传达修改背后的意图,使模型更好地理解程序行为如何演化。此外,静态程序分析技术可以提取结构信息,例如程序版本间的控制流和数据流差异,为变更的功能影响提供更清晰的信号。总体而言,这些方向表明未来系统应纳入帮助 LLM 推理代码变更的幅度和性质的机制,而不是仅仅依赖原始源代码输入。
7. 有效性威胁
内部有效性:为降低在 SPC 期间加入意外功能偏移的风险,我们用原始基线测试套件测试更新后的代码,并验证在原始代码上 100% 的通过率,确保 SPC 是行为中性的、不造成任何功能偏移。另一个威胁是 LLM 固有的非确定性。模型输出可能在不同尝试间变化,潜在影响测试通过率和覆盖率指标。我们通过为每个生成任务使用全新的、独立的模型实例来防止上下文泄漏,并严格评估即时输出以捕捉模型的基线推理,来应对这一点。
外部有效性:我们的评估基于来自 Project CodeNet 数据集的 22,374 个程序变体,特别聚焦 Java 和 Python 实现。虽然这些代表了广泛使用的语言和常见算法任务,发现可能无法完全推广到存在深层上下文依赖的高度复杂、多文件企业代码库。然而,因为模型已经在这些相对简单、自包含的程序上难以维持语义根基,我们认为观察到的性能退化是一个保守基线——引入更大的架构复杂性很可能只会进一步降低测试可靠性。此外,我们的研究评估了 8 个最先进 LLM 的一个特定子集。随着模型架构快速演化,未来的迭代可能对代码演化表现出不同的敏感性。
构念有效性:我们依赖行覆盖率、分支覆盖率和测试通过率来评估测试套件质量。虽然这些是标准的测试质量指标,它们并未完全捕捉测试套件的找错能力或确切的开发者意图。然而,在评估语义根基和残留对齐的语境中,这些指标结合我们的失败归因分析,提供了一个稳健、客观的框架来量化模型对功能和结构变更的敏感性。
相关工作
传统自动化测试生成。 自动化测试生成已被广泛研究,采用的传统技术包括反馈导向的随机测试(如 Randoop 和 JCrasher)、基于搜索的软件测试(SBST,如 EvoSuite 和 Pynguin),以及符号执行框架(如 KLEE 和 DART)。这些方法生成测试输入以最大化代码覆盖率,例如分支或语句。大多数现有方法与特定编程语言耦合,需要大量工程努力来设计和维护测试生成框架。尽管有这种复杂性,它们仍缺乏自动化、高效测试生成所需的许多特性。
基于 LLM 的自动化测试生成。 近期工作探索了用 LLM 进行测试生成,一些提出提示策略和微调来生成开发者式单元测试,同时达到高结构覆盖率和语义可读性。例如 Schäfer 等人和 Lops 等人为 LLM 驱动的单元测试提供了全面的实证评估和生成系统。进一步推进这些生成技术,SymPrompt 使用与执行路径对齐的多阶段、代码感知的提示过程,而 IntUT 使用显式的测试意图(例如输入、mock 和期望结果)来引导生成。类似地,Yuan 等人提出 ChatUniTest,一个利用自适应焦点上下文的自动化框架,以在为复杂 Java 项目生成测试套件时最小化幻觉。Meta 的单元测试改进框架在工业工作流中改进和扩展现有测试,E-Test 使用从生产日志收集的執行场景持续扩充测试套件。大量基准测试工作也试图量化这些生成能力。Siddiq 等人、Yuan 等人和 Jiang 等人评估了各种 LLM 生成单元测试的基线能力。尽管有这些有前景的结果,当前评估主要依赖广为人知的公开基准。现有研究未能把这种潜在的记忆与真正的语义推理隔离开。
代码修改与 LLM 稳健性。 当面对标准编程任务的变体时,LLM 生成能力的脆弱性也被研究。例如 Wang 等人引入 ReCode,一个评估代码生成模型对语义保持型变异稳健性的框架。类似地,Rabin 等人和 Yefet 等人调查了神经程序分析器在语义保持型变异下的泛化能力,证明像变量重命名这样的简单变化就能严重降低模型性能。其他实证研究,如 Dong 等人和 Yang 等人的,评估了对抗性重构和自然变异如何影响漏洞检测和代码摘要等下游任务。这些工作主要评估当代码或提示被改变时,模型能否仍产出正确的代码片段或识别 bug,大多测试 LLM 的生成能力。它们没有评估测试生成所需的 LLM 代码理解能力。
记忆与语义根基。 评估 LLM 的一个根本挑战是区分真正推理与对预训练数据的记忆。Carlini 等人和 Lee 等人证明 LLM 能精确复现其训练语料的很大一部分。近期综述和聚焦污染的研究也凸显了基准污染如何能抬高代码生成模型的表观性能。
8. 结论
我们考察了代码变更如何影响基于 LLM 的测试生成的可靠性。使用一个自动化的、变异驱动的评测框架,在超过 22,374 个程序变体上,我们评估了模型在语义改变型和语义保持型变更下的行为。虽然 LLM 在未修改程序上取得很强的基线性能(测试全部通过时 79.3% 行覆盖率和 76.1% 分支覆盖率),一旦引入代码变更,测试质量就大幅下降。在语义改变型变更下,许多生成的测试仍与原始程序行为对齐,超过 99% 的失败测试在原始代码上通过。在语义保持型变更下,尽管功能不变,覆盖率和通过率仍下降。这些结果表明,当前基于 LLM 的测试生成无法推理代码变更的语义影响,而主要响应代码中语法差异的幅度。
署名与许可:原文 Evaluating LLM-Based Test Generation Under Software Evolution,作者 Sabaat Haroon(Virginia Tech)、Mohammad Taha Khan(Carnegie Mellon University)、Muhammad Ali Gulzar(Virginia Tech),arXiv:2603.23443v1 [cs.SE],2026-03-24。原文以 CC BY 4.0 许可发布。中文全译由智测团队完成,译文同样以 CC BY 4.0 发布;图 1–9 版权归原作者。原文地址:https://arxiv.org/abs/2603.23443 。数据与制品:https://doi.org/10.5281/zenodo.18898624 。译文中省略了参考文献列表与文内引注编号;模型名、基准名、指标名保留英文。如译文与原文有出入,以英文原文为准。
Found it useful? Pass it on
Scan with WeChat to open it on your phone and forward it.