如何使用 GitHub Copilot 生成单元测试:技巧和示例
了解如何使用 GitHub Copilot 来生成单元测试,并查看具体示例、教程以及最佳实践。
开发者真的会写足够多的单元测试吗?当然了,而且我周五下午的代码从来不会出现任何 Bug。
无论你是初入职场的开发者还是经验丰富的资深人士,编写测试——或者说编写足够多的测试——都是一项挑战。对于单元测试尤其如此,它们能够帮助开发者及早发现 Bug、验证代码、辅助重构、提高代码质量,并在测试驱动开发(TDD)中扮演核心角色。
换句话说,你可以通过自动化测试生成(并借助 AI 编程工具),大幅节省时间,同时编写更好、更健壮的代码。
GitHub Copilot 是 GitHub 的 AI 驱动编程助手,它能帮助你即时生成测试用例并节省时间。坦白说:我在自己的工作流中也非常依赖 GitHub Copilot 来生成测试——但我依然会手动编写一部分测试来帮助我思考问题。
在本文中,我将向你展示为什么单元测试至关重要、GitHub Copilot 如何协助生成单元测试,以及如何最大化利用 Copilot 的测试生成功能。我们还将深入探讨一些跨语言和框架的具体示例,帮助你开始使用 Copilot 生成单元测试。
哦,对了,如果你想知道的话,本文后面的单元测试示例是我使用 Anthropic 的 Claude 模型生成的(如果你没注意到,GitHub Copilot 现在已支持 Anthropic 的 Claude、Google 的 Gemini 和 OpenAI 的 GPT o1 模型)。
让我们开始吧。
https://www.youtube.com/watch?v=smdBqEu7fx4
想直接浏览文档?
在我们的文档中了解如何使用 GitHub Copilot 编写单元测试,以及更多相关内容。
为什么单元测试很重要(以及优秀单元测试和糟糕单元测试的区别)
如果你已经熟悉这些内容,可以直接跳过这一部分。但以防万一,单元测试是创建可靠且可维护软件的基础。当你编写代码时,对函数或类等独立单元进行测试,能够帮助你确保各个组件按预期工作。这提升了代码库的可靠性,简化了调试流程,并促进团队协作,因为其他开发者可以理解并信任这份代码。
然而,编写单元测试往往耗时费力,更糟糕的是,很多时候仅仅编写单元测试并不意味着它们真的能发挥应有的价值。只是为了完成任务或达到某个指标而去写测试,不会使它们变得有用;你需要明确它们的目的,并确保它们能真正带来价值。
你应该始终从单元测试的目的以及最终受众和作用出发。以下是一些值得考虑的要点:
考虑你的测试理念。 你是想隔离类和依赖项,还是想编写验证整体行为的高层测试,以满足需求?这并非二选一——但你应该考虑想要达到什么结果。
明确测试的目的和受众。 清晰说明每个测试的目的,以帮助后续开发者判断什么时候可以删除这些测试。测试应当清晰对应需求、类或 API。测试也应当针对它们的受众来编写。也许你的目标是满足产品负责人、帮助 QA、让新团队成员了解系统,或者支持重构工作。
注重实用性。 永远优先考虑对项目最有用、最需要的部分。比如,TDD(测试驱动开发)需要一定的实践,并且应该让你更快、更有信心,而不是拖慢你的节奏。
GitHub Copilot 如何帮助生成单元测试
GitHub Copilot 使用生成式 AI 在你的 IDE 中(以及通过基于对话的功能,在 IDE 和整个 GitHub 项目中)提供实时的代码建议。
基于你代码中的上下文或对话式查询(甚至在你高亮特定代码块后使用斜杠命令),Copilot 可以给出与之相关的单元测试建议,涵盖诸如边界值、常见输入和错误场景等典型情形。这种可以预判和生成测试代码的功能有助于提升测试覆盖率并让应用更加健壮。
那么,在实际开发中是怎么运作的呢? 假设你正在测试一段业务逻辑——比如使用正则表达式来验证输入。由于需要针对不同的边界情况进行测试,单元测试往往让人感觉(并且实际上也)很重复、很费时。
你可以通过高亮代码或逻辑,使用 GitHub Copilot 来为你生成测试,而不是手动编写每一个测试用例,然后让 Copilot 来建议各种输入和边界条件下的测试用例。
使用 GitHub Copilot 生成单元测试的方式有很多种。 例如,你可以选中要测试的代码,在 IDE 中右键单击并选择 Copilot->Generate Tests。也可以先选中要测试的代码或逻辑,然后在 IDE 中使用斜杠命令 /tests 来生成测试。再者,你还可以使用 GitHub Copilot Chat(无论是在 IDE 中还是在 GitHub 在线体验里)来进行对话式提示,让它查找已有测试或者生成新的测试。
如何使用对话角色或斜杠命令来让 GitHub Copilot 生成单元测试?
我个人几乎总是在用 GitHub Copilot 生成单元测试时使用斜杠命令——你也可以在 IDE 或 Copilot Chat 中输入 /tests。这个命令会调用 @workspace(一种对话角色),从而让 Copilot 拥有更大的上下文窗口,可以分析你的整个项目来生成测试。这样一来,Copilot 就能自动发现我之前已经写过的测试或者与我当前工作相关的内容。通常如果我已知项目中存在某些内容,最好还是明确告诉它。可这条 test 命令却能让它拥有更广阔的上下文范围。
什么时候不应该使用 GitHub Copilot 来生成单元测试?
通常我会在那些我明确知道自己想要实现什么的场景下手动编写测试,就像我直接手动编写代码一样,因为我知道要怎么做,会更快。有时我也需要先梳理一下思路,手写测试的过程能帮助我确定目标和思路。在此基础上,我才会让 GitHub Copilot 来扩充我已经写好的部分。
使用 GitHub Copilot 生成单元测试的主要好处
即使我不会在所有情况下都使用 GitHub Copilot 来生成单元测试,但我在单元测试场景下使用它的频率还是非常高。使用 GitHub Copilot 生成单元测试所带来的一些主要好处包括:
节省常规性工作的时间。 单元测试通常都是编写各种重复性的用例,非常适合自动化处理。有了 Copilot,你可以将大量的重复劳动交给它,从而将精力集中在功能开发而不是手动编写测试用例上。
支持 TDD。 TDD 的核心是在实现代码之前先编写测试,如果没有任何自动补全工具做建议时,会让人感到负担很重。而 Copilot 改变了这一点。它会“相信”你对应用的描述,帮助你为尚未实现的功能生成测试。比如,你可以向 Copilot 描述你想要开发的应用功能,它就会为这些功能生成测试,然后你再去实现代码来满足这些测试,从而实现 TDD 流程。
提高测试覆盖率。 让 Copilot 来处理初始的测试生成,可以快速覆盖大量场景。然后你可以对这些测试进行完善和扩展,确保它们完全符合你的需求。通过这种迭代方式,可以增强对测试套件以及被测代码的信心。
使用 GitHub Copilot 生成单元测试的最佳实践
在使用 GitHub Copilot 进行测试生成的过程中,我总结了一些个人认为有价值的最佳实践,希望对你有帮助。
高亮你想要测试的代码。 在生成测试前,最好先高亮你想让 Copilot 关注的代码或逻辑,然后再使用斜杠命令。我的经验是这一步非常直观,但我经常发现新手会忽略这一点。
在提示中明确说明你要测试什么。 Copilot 与人类的思维方式不同。如果我自己创建一个函数,我会关注它的作用和实现原理。Copilot 并不会真正“阅读”代码;它只是依靠模式匹配。如果你知道函数中的某个具体部分需要测试,就告诉 Copilot“注意这块代码”或者“注意这段逻辑”。
提供上下文。 当使用 Copilot 时,可以在代码里添加注释或文档字符串,解释代码的预期行为。你还可以使用
#[file]
命令让 Copilot 去查看你已经写好的测试文件。这些信息能帮助 Copilot 生成更精准、更有意义的测试。仔细审查建议。 与人类编写的代码一样,永远不要在没有经过常规审查的情况下就直接信任 Copilot 生成的测试。你需要自己审查输出、通过 linter 运行检查并仔细查看代码。
保持灵活并不断迭代。 归根结底,单元测试本质上就是“描述”代码的代码。Copilot 初次生成的测试未必就是你想要的。根据我的经验,它有时不会生成需要的模拟对象(mock),有时也会“幻觉式地”胡乱猜测。不要害怕重构你的提示或提问。
询问 Copilot 是否还有遗漏的测试。 你随时可以问 Copilot “我还有哪些没测试的?”——根据我的经验,Copilot 往往能给出一些我没想到的场景,例如边界情况、需求验证等。我发现这个功能非常有用。另外,我也喜欢使用 Copilot 来生成错误条件和会抛出预期失败的代码路径的测试。因为测试错误输入和错误处理能力跟测试正确输入一样重要,以确保应用可以优雅地应对错误。
使用测试覆盖率工具。 比如在 JavaScript 中使用 Jest 的 coverage,在 Java 中使用 Cobertura 等,用来评估 Copilot 的测试覆盖率并填补可能的空白。💡 小技巧: 如果你把 Copilot 和代码覆盖率工具结合起来,就可以快速定位尚未测试的代码路径,然后使用 Copilot 生成相应的测试来降低潜在风险。
使用 GitHub Copilot 生成单元测试:实用示例
现在,让我们来看看在 Python 和 TypeScript 中的一些实际示例——这两种语言是 GitHub 上最常用的语言之一。
示例 1:使用 GitHub Copilot 在 Python 中创建单元测试
假设我们有一个 Python 函数用来检查价格是否大于 0 且小于等于 1000。让我们用 Copilot 来为它生成测试用例。
def validate_price(price: float):
if price <= 0:
raise ValueError("Price must be greater than 0")
if price > 1000:
raise ValueError("Price must be less than or equal to 1000")
在启用 Copilot 的情况下,你可以在 Copilot Chat 中输入以下斜杠命令和提示:/tests create tests for validate_price
。注意也要检查边界值。
之后,你会看到类似如下的输出(注意:由于 LLM 是非确定性的,你的输出可能略有不同):
import unittest
from generate_unit_tests import validate_price
class TestValidatePrice(unittest.TestCase):
def test_price_zero(self):
with self.assertRaises(ValueError) as context:
validate_price(0)
self.assertEqual(str(context.exception), "Price must be greater than 0")
def test_price_negative(self):
with self.assertRaises(ValueError) as context:
validate_price(-10)
self.assertEqual(str(context.exception), "Price must be greater than 0")
def test_price_above_limit(self):
with self.assertRaises(ValueError) as context:
validate_price(1500)
self.assertEqual(str(context.exception), "Price must be less than or equal to 1000")
def test_price_edge_case_zero(self):
with self.assertRaises(ValueError) as context:
validate_price(0)
self.assertEqual(str(context.exception), "Price must be greater than 0")
def test_price_edge_case_max(self):
try:
validate_price(1000)
except ValueError:
self.fail("validate_price() raised ValueError unexpectedly!")
if __name__ == '__main__':
unittest.main()
在这里,我们看到 GitHub Copilot 为 validate_price
函数生成了一些测试用例。以下是各测试的功能说明:
test_price_zero: 确认调用
validate_price(0)
会抛出ValueError
,并且错误信息为 “Price must be greater than 0”。这测试了价格为无效下界时的情况。test_price_negative: 确认调用
validate_price(-10)
会抛出同样的错误信息,以确保负值被正确处理。test_price_above_limit: 验证调用
validate_price(1500)
时抛出的错误信息是 “Price must be less than or equal to 1000”,用来测试无效价格的上界。test_price_edge_case_zero: 与
test_price_zero
重复(测试了相同条件),属于多余测试,可以删除。test_price_edge_case_max: 验证调用
validate_price(1000)
时不应该抛出错误。如果抛出了错误,这个测试就会失败。
虽然这些测试可以工作,但也可以通过后续提示进行改进,比如删除冗余的 test_price_edge_case_zero
,或添加对有效价格范围(例如 validate_price(500)
) 的测试。此外要记住:GitHub Copilot 由非确定性 LLM 提供支持,并不总能输出同样的结果。在使用 Copilot 生成的代码时,你始终应该进行审查,并可通过重新提示 Copilot 来改进这段代码。
示例 2:使用 GitHub Copilot 在 TypeScript 中用 Jest 创建单元测试
接下来,我们在 TypeScript 中写出相同的函数,用来检查给定数值型价格是否在特定范围内。可以是这样:
function validatePrice(price: number) {
if (price < 0) {
throw new Error('Price must be greater than 0');
}
if (price > 1000) {
throw new Error('Price must be less than 1000');
}
}
你可以在 Copilot Chat 中输入与之前相同的斜杠命令和提示:/tests create tests for validate_price
,注意也要检查边界值。
生成的单元测试可能类似如下所示(由于 LLM 的非确定性,你的输出可能会有些差异):
describe('validatePrice', () => {
// Valid cases
it('should accept valid price in middle range', () => {
expect(() => validatePrice(500)).not.toThrow();
});
it('should accept price of 0', () => {
expect(() => validatePrice(0)).not.toThrow();
});
it('should accept price of 1000', () => {
expect(() => validatePrice(1000)).not.toThrow();
});
// Error cases
it('should throw error for negative price', () => {
expect(() => validatePrice(-1)).toThrow('Price must be greater than 0');
});
it('should throw error for price over 1000', () => {
expect(() => validatePrice(1001)).toThrow('Price must be less than 1000');
});
// Edge cases
it('should accept small decimal price', () => {
expect(() => validatePrice(0.01)).not.toThrow();
});
it('should accept price close to maximum', () => {
expect(() => validatePrice(999.99)).not.toThrow();
});
it('should throw error for NaN', () => {
expect(() => validatePrice(NaN)).toThrow('Price must be greater than 0');
});
it('should throw error for Infinity', () => {
expect(() => validatePrice(Infinity)).toThrow('Price must be less than 1000');
});
});
你会看到 Copilot 生成了 Jest 的单元测试,它与上面 Python 示例中的测试功能类似——验证了有效价格场景、报错场景以及一些边界值,以确保 validatePrice
函数按预期运行。
带走这些要点
单元测试是软件开发中必不可少的一环,但编写测试通常既琐碎又耗时。借助 GitHub Copilot,你可以自动完成其中的大部分过程,轻松生成有意义且覆盖面广的测试,而避免重复劳动。无论你是要验证复杂的业务逻辑、在 TDD 工作流中工作,还是扩展已有的测试套件,Copilot 都可以成为强有力的助手。
想充分利用 Copilot,关键在于清晰的沟通和持续迭代。要在提示中给出足够的细节,高亮需要测试的代码,并且不要犹豫去改进你的提示(或是 Copilot 的输出)。你也可以使用斜杠命令或 Copilot Chat 来提供更宽泛的上下文或请求额外的测试用例。与此同时,切记要对 Copilot 生成的任何测试进行审查和验证,以保证其准确性。最后,祝你测试愉快!