跳至主要内容

使用 GitHub Copilot 编写测试

使用 Copilot 生成单元和集成测试,并帮助提高代码质量。

简介

GitHub Copilot 可以帮助你快速开发测试并提高生产力。在本文中,我们将演示如何使用 Copilot 编写单元测试和集成测试。虽然 Copilot 在为基本功能生成测试方面表现良好,但复杂场景需要更详细的提示和策略。本文将逐步介绍使用 Copilot 分解任务和验证代码正确性的实用示例。

先决条件

在开始之前,你必须具备以下条件:

使用 Copilot 聊天编写单元测试

在本节中,我们将探讨如何使用 GitHub Copilot 聊天为 Python 类生成单元测试。此示例演示了如何使用 Copilot 为类似于 BankAccount 类的类创建单元测试。我们将向你展示如何提示 Copilot 生成测试、执行测试和验证结果。

示例类:BankAccount

让我们从一个包含存款、取款和获取账户余额方法的 BankAccount 类开始。在 GitHub 存储库中创建一个名为 bank_account.py 的新文件,并在 Python 中添加以下 BankAccount 类。

class BankAccount:
    def __init__(self, initial_balance=0):
        if initial_balance < 0:
            raise ValueError("Initial balance cannot be negative.")
        self.balance = initial_balance

    def deposit(self, amount):
        if amount <= 0:
            raise ValueError("Deposit amount must be positive.")
        self.balance += amount

    def withdraw(self, amount):
        if amount <= 0:
            raise ValueError("Withdrawal amount must be positive.")
        if amount > self.balance:
            raise ValueError("Cannot withdraw more than the current balance.")
        self.balance -= amount

    def get_balance(self):
        return self.balance

使用 Copilot 聊天生成单元测试

你可以提示 Copilot 编写涵盖一系列场景的测试。请明确你的测试需求以获得最佳结果。以下是你可用于为 BankAccount 类生成单元测试的示例提示。

提示:为 Python 中的 BankAccount() 函数开发一套全面的单元测试。编写多个测试方法,涵盖各种场景,包括边界情况、异常处理和数据验证。

此提示执行了一些重要操作

  • 范围:它明确要求提供一套全面的单元测试,以便 Copilot 生成的不只是基本测试,而是涵盖各种潜在用例。
  • 场景:提示指定测试应包括
    • 边界情况:测试边界条件(例如,存款或取款的负值、取款金额超过可用余额)。
    • 异常处理:测试在发生某些无效操作时应引发的错误(例如,取款金额超过可用余额、存款金额为负)。
    • 数据验证:确保正确验证和处理输入。

Copilot 将给出如下响应。

import unittest
from bank_account import BankAccount

class TestBankAccount(unittest.TestCase):
    def setUp(self):
        self.account = BankAccount()

    def test_initial_balance(self):
        self.assertEqual(self.account.get_balance(), 0)

    def test_deposit_positive_amount(self):
        self.account.deposit(100)
        self.assertEqual(self.account.get_balance(), 100)

    def test_withdraw_within_balance(self):
        self.account.deposit(100)
        self.account.withdraw(50)
        self.assertEqual(self.account.get_balance(), 50)

    def test_deposit_negative_amount_raises_error(self):
        with self.assertRaises(ValueError):
            self.account.deposit(-100)

    def test_withdraw_negative_amount_raises_error(self):
        with self.assertRaises(ValueError):
            self.account.withdraw(-50)

    def test_withdraw_more_than_balance_raises_error(self):
        self.account.deposit(100)
        with self.assertRaises(ValueError):
            self.account.withdraw(200)

    def test_initial_balance_negative_raises_error(self):
        with self.assertRaises(ValueError):
            BankAccount(-100)

if __name__ == '__main__':
    unittest.main()

运行测试用例

Copilot 生成的测试套件包含提示中指定的每个场景的测试用例。BankAccount 类中的每个函数都针对一系列条件进行了测试,包括存款和取款等有效操作,以及负金额和尝试取款金额超过可用余额等边界情况。

一旦 Copilot 生成了令你满意的测试套件,请将代码添加到一个名为 test_bank_account.py 的新文件中。你可以询问它如何运行测试。

提示:“如何使用 unittest 框架在 Python 中运行这些单元测试?”

Copilot 将提供以下 bash 命令。

python -m unittest test_bank_account.py

运行测试后,你将在终端或 IDE 中看到输出。如果所有测试都通过,则可以确信你的 BankAccount 类按预期工作。

斜杠命令

此外,你可以提示 Copilot 使用 /tests 斜杠命令编写完整的单元测试套件。确保你在 IDE 的当前选项卡中打开了该文件,Copilot 将为该文件生成单元测试。Copilot 生成的测试可能无法涵盖所有场景,因此你应始终审查生成的代码并添加任何必要的其他测试。

提示

如果你要求 Copilot 为尚未包含单元测试的代码文件编写测试,则可以通过在编辑器的相邻选项卡中打开一个或多个现有测试文件来为 Copilot 提供有用的上下文。Copilot 将能够看到你使用的测试框架,并且更有可能编写与现有测试一致的测试。

Copilot 将生成如下单元测试套件。

import unittest
from bank_account import BankAccount

class TestBankAccount(unittest.TestCase):
    def setUp(self):
        self.account = BankAccount()

    def test_initial_balance(self):
        self.assertEqual(self.account.get_balance(), 0)

使用 Copilot 编写集成测试

集成测试对于确保系统各个组件组合在一起时能够正确工作至关重要。在本节中,我们将扩展我们的 BankAccount 类以包含与外部服务 NotificationSystem 的交互,并使用模拟来测试系统的行为,而无需真实的连接。集成测试的目标是验证 BankAccount 类和 NotificationSystem 服务之间的交互,确保它们能够协同工作。

示例类:带有通知服务的 BankAccount

让我们更新 BankAccount 类以包含与外部服务的交互,例如向用户发送通知的 NotificationSystemNotificationSystem 表示需要测试的集成。

使用以下代码片段更新 bank_account.py 文件中的 BankAccount 类。

class BankAccount:
    def __init__(self, initial_balance=0, notification_system=None):
        if initial_balance < 0:
            raise ValueError("Initial balance cannot be negative.")
        self.balance = initial_balance
        self.notification_system = notification_system

    def deposit(self, amount):
        if amount <= 0:
            raise ValueError("Deposit amount must be positive.")
        self.balance += amount
        if self.notification_system:
            self.notification_system.notify(f"Deposited {amount}, new balance: {self.balance}")

    def withdraw(self, amount):
        if amount <= 0:
            raise ValueError("Withdrawal amount must be positive.")
        if amount > self.balance:
            raise ValueError("Cannot withdraw more than the current balance.")
        self.balance -= amount
        
        if self.notification_system:
            self.notification_system.notify(f"Withdrew {amount}, new balance: {self.balance}")
        
    def get_balance(self):
        return self.balance

在这里,我们将为 Copilot 编写 BankAccount 类的集成测试的请求分解成更小、更易于管理的部分。这将帮助 Copilot 生成更准确和相关的测试。

提示:“为 BankAccount 类中的 deposit 函数编写集成测试。使用模拟来模拟 NotificationSystem 并验证在存款后是否正确调用了它。”

此提示执行了一些重要操作

  • 范围:它指定集成测试,重点关注 deposit 函数和 NotificationSystem 之间的交互,而不是仅仅是单元测试。
  • 模拟:它明确要求使用模拟来模拟 NotificationSystem,确保在不依赖于其实际实现的情况下测试与外部系统的交互。
  • 验证:提示强调验证在存款后是否正确调用了 NotificationSystem,确保组件之间的集成按预期工作。
  • 具体性:提示明确说明了要测试的方法 (deposit) 和类 (BankAccount)。

提示

如果 Copilot 生成了无效的测试,请提供要测试的函数的输入和输出示例。这将帮助 Copilot 评估函数的预期行为。

Copilot 将生成如下测试套件。

import unittest
from unittest.mock import Mock
from bank_account import BankAccount

class TestBankAccountIntegration(unittest.TestCase):
    def setUp(self):
        self.notification_system = Mock()

    def test_deposit_with_notification(self):
        account = BankAccount(initial_balance=100, notification_system=self.notification_system)
        account.deposit(50)
        self.assertEqual(account.get_balance(), 150)
        self.notification_system.notify.assert_called_once_with("Deposited 50, new balance: 150")
        
if __name__ == '__main__':
    unittest.main()

将生成的代码添加到一个名为 test_bank_account_integration.py 的新文件中。

改进测试用例

上面的提示生成了一个测试用例,用于验证在进行有效存款时是否调用了 NotificationSystem。但是,它没有涵盖在存款过程中引发错误的情况。在这些情况下,不应调用 NotificationSystem。我们需要添加一个处理无效存款的测试用例,并确保未触发通知系统。

提示:“为无效存款金额添加一个测试用例,以验证函数是否引发了正确的异常以及是否未调用 NotificationService。”

Copilot 将生成如下测试用例。

    def test_deposit_negative_amount_raises_error(self):
        account = BankAccount(initial_balance=100, notification_system=self.notification_system)
        with self.assertRaises(ValueError):
            account.deposit(0)
        self.notification_system.notify.assert_not_called()

询问改进领域

现在我们已经编写了测试用例来验证存款的集成功能,这是一个查找测试套件内改进的好机会。虽然当前测试是可行的,但我们可以提示 Copilot 评估代码覆盖率并建议改进领域。

提示:“为了确保 BankAccount 类和 NotificationSystem 之间集成的全面覆盖,应该包含哪些额外的测试?”

使用此问题提示 Copilot 可以帮助您识别可能被忽略的缺失测试用例。在这种情况下,虽然我们测试了有效和无效的存款,但尚未涵盖取款功能。

Copilot 将生成一个更新后的测试套件,例如以下内容。

点击展开完整的生成代码示例
import unittest
from unittest.mock import Mock
from bank_account import BankAccount

class TestBankAccountIntegration(unittest.TestCase):
    def setUp(self):
        self.notification_system = Mock()

    def test_deposit_with_notification(self):
        account = BankAccount(initial_balance=100, notification_system=self.notification_system)
        account.deposit(50)
        self.assertEqual(account.get_balance(), 150)
        self.notification_system.notify.assert_called_once_with("Deposited 50, new balance: 150")

    def test_deposit_negative_amount_raises_error(self):
        account = BankAccount(initial_balance=100, notification_system=self.notification_system)
        with self.assertRaises(ValueError):
            account.deposit(-50)
        self.notification_system.notify.assert_not_called()

    def test_deposit_zero_amount_raises_error(self):
        account = BankAccount(initial_balance=100, notification_system=self.notification_system)
        with self.assertRaises(ValueError):
            account.deposit(0)
        self.notification_system.notify.assert_not_called()

    def test_withdraw_with_notification(self):
        account = BankAccount(initial_balance=100, notification_system=self.notification_system)
        account.withdraw(30)
        self.assertEqual(account.get_balance(), 70)
        self.notification_system.notify.assert_called_once_with("Withdrew 30, new balance: 70")

    def test_withdraw_exceeding_balance_raises_error(self):
        account = BankAccount(initial_balance=100, notification_system=self.notification_system)
        with self.assertRaises(ValueError):
            account.withdraw(150)
        self.notification_system.notify.assert_not_called()

    def test_withdraw_negative_amount_raises_error(self):
        account = BankAccount(initial_balance=100, notification_system=self.notification_system)
        with self.assertRaises(ValueError):
            account.withdraw(-30)
        self.notification_system.notify.assert_not_called()

    def test_withdraw_zero_amount_raises_error(self):
        account = BankAccount(initial_balance=100, notification_system=self.notification_system)
        with self.assertRaises(ValueError):
            account.withdraw(0)
        self.notification_system.notify.assert_not_called()

    def test_initial_negative_balance_raises_error(self):
        with self.assertRaises(ValueError):
            BankAccount(initial_balance=-100, notification_system=self.notification_system)

if __name__ == '__main__':
    unittest.main()

一旦 Copilot 生成了您满意的测试套件,请使用以下命令运行测试以验证结果。

python -m unittest test_bank_account_integration.py