让DeepSeek从“能写”到“写得好”,关键在于提示词工程、上下文管理和场景化约束,配合人工验证形成闭环。
用DeepSeek写单元测试这件事,很多团队已经跑通了基本流程,但实际体验差异很大。有人觉得它生成的用例覆盖面广、边界考虑周全,省下大把时间;也有人抱怨它写出来的东西“看着对、跑不通”,或者全是 happy path,根本发现不了代码里的暗坑。问题不在模型本身,而在于使用。单元测试不是让模型“自由发挥”的创作题,而是一道有明确输入、预期输出和质量标准的工程题。把这道题的条件交代清楚,DeepSeek的输出质量就会从“凑合能用”跃升到“直接进 CI”。
1. 用场景化提示词锁定测试意图
直接丢一段函数让DeepSeek“写单元测试”,得到的结果往往泛泛——它不知道该函数在系统中的角色、调用方的约定、以及哪些行为是必须锁定的。高效的用法是先把测试意图讲清楚。例如,与其说“给这个支付金额校验函数写测试”,不如说“这个函数接收订单金额和用户等级,返回折扣后金额;折扣规则是普通用户满100减10、VIP满100减20;异常输入包括负数金额和null用户等级;请针对每个折扣分支和异常输入生成测试用例”。这种提示词包含了业务规则、分支条件和边界定义,模型不需要猜测,直接映射成断言。
提示词里还需要指定测试框架和风格约束。Python项目就明确说“使用pytest,用parametrize组织多组输入输出”,Java项目则说“使用JUnit 5,用@ParameterizedTest和@CsvSource”。不指定框架,模型可能给出万能伪代码或混用风格。同时要求它遵循项目现有测试文件的命名和组织,比如“测试类放在src/test/java对应包下,方法名用should_预期结果_when_条件”。这些约束让生成的代码更容易融入现有工程,减少后期调整成本。
另一个容易忽略的点是让DeepSeek先“复述理解”再生成。在提示词末尾加一句“请先用三句话说明你对这个函数行为和测试范围的理解,再输出测试代码”。这一步能暴露模型对需求的理解偏差。如果它的复述里漏掉了某个分支或误解了异常条件,可以在它生成代码前就纠正,避免后续大段返工。这种“先对齐、再生成”的模式,比直接出代码再人工审查要高效得多。
2. 提供结构化上下文而非大段代码
DeepSeek的上下文窗口虽然大,但塞进去整个文件或整个模块并不明智。无关代码会稀释关键信息,模型可能把注意力放在不相关的依赖上,生成一堆跟目标函数无关的测试。高效的做法是只提供被测函数的完整代码、它直接依赖的接口签名或类型定义、以及相关的数据模型。比如测试一个订单状态转换函数,只需要给它状态枚举、转换规则表、以及触发转换的服务方法签名,不需要把订单仓储层和消息队列的代码也贴进去。
对于依赖外部服务的函数,用接口 mock 的提供上下文。告诉DeepSeek“这个函数调用了 userService.getUserById,返回类型是User,包含id、name、level三个字段;测试时请用mock替换该调用”。这样模型知道哪些是真实逻辑、哪些需要隔离,生成的测试才会用Mockito或unittest.mock去构造桩对象,而不是试图连接真实数据库。很多“跑不通”的测试就是因为模型不知道依赖需要mock,直接写了真实调用。
如果被测代码涉及复杂的数据结构或配置,用表格或键值对的形式单独列出,比嵌在代码注释里更有效。比如“配置项有:maxRetry=3, timeoutMs=500, fallbackEnabled=true”,模型可以直接把这些值映射到测试用例的参数化输入里。对于状态机、规则引擎这类逻辑,用“状态A + 事件B → 状态C”的简洁格式描述转换关系,模型生成的测试会精准覆盖每条转换路径,避免遗漏或重复。上下文的质量直接决定生成测试的精准度,精简但完整的输入比冗余的大段代码更高效。
3. 分轮迭代覆盖边界与异常路径
第一轮生成的测试通常覆盖主干逻辑,但边界条件和异常路径容易缺失。高效的做法是分轮迭代:第一轮让DeepSeek生成正常路径的测试,验证基本行为;第二轮专门要求“补充边界值测试,包括空输入、极值、类型边界”;第三轮要求“补充异常路径测试,模拟依赖抛出异常、超时、返回null等情况”。每轮聚焦一个维度,提示词更具体,模型输出的针对性也更强。
在边界值补充轮,可以明确列举需要覆盖的边界类型。比如对数值参数,要求覆盖最小值、最大值、零、负数、超出范围的值;对字符串参数,覆盖空串、超长串、特殊字符;对集合参数,覆盖空集合、单元素、大量元素。这些具体指令比笼统的“补充边界测试”有效得多。对于异常路径,提示词可以写“模拟userService.getUserById抛出RuntimeException,验证函数是否回滚事务并返回默认值”,模型会根据这个场景生成包含try-catch断言和mock异常抛出的完整测试方法。
迭代过程中还有一个技巧:让DeepSeek分析已有测试的覆盖盲区。把第一轮生成的测试代码和被测函数一起给它,问“这些测试还遗漏了哪些分支或条件组合”。模型会对照代码逻辑指出未覆盖的路径,比如“第12行的else if分支没有对应测试”“当a>0且b<0时没有用例”。然后针对这些盲区单独生成补充测试。这种“生成-分析-补充”的循环,比一次性要求“写全所有测试”更可靠,因为模型在分析任务上比在一次性生成任务上表现更稳定。
4. 人工验证与断言质量把关
DeepSeek生成的测试必须经过人工审查才能进入代码库,重点检查断言是否真正验证了行为。常见问题是断言过弱:比如只断言函数返回非null,但不检查返回值内容;或者只验证mock方法被调用,但不验证调用参数是否正确。审查时要确认每个测试方法都有明确的预期值断言,对于mock交互,用verify加参数匹配器检查调用细节。比如“verify(userService).getUserById(eq(123L))”比“verify(userService).getUserById(any)”严格得多。
另一个需要把关的是测试的独立性。DeepSeek有时会生成依赖执行顺序的测试,或者多个测试共享可变状态。审查时要确保每个测试方法自己准备数据、自己清理,不依赖其他测试的结果。对于涉及时间的逻辑,检查是否用了可注入的时钟或时间提供者,而不是直接调用系统当前时间。这些细节决定了测试在CI环境中能否稳定运行,而不是本地通过、流水线失败。
断言消息的质量也值得关注。DeepSeek默认生成的断言消息往往很简略,比如“assertEquals(expected, actual)”,失败时只告诉你值不匹配。可以要求它在断言里加入业务语义,比如“assertEquals(‘VIP用户满100应减20’, expected, actual)”。这样测试失败时,报错信息直接说明哪个业务规则被破坏了,排查效率更高。人工审查时把这类断言消息补全,长期来看能显著降低维护成本。经过这几轮验证和修正,DeepSeek生成的测试就能达到可提交的标准,真正融入日常开发流程。

