Skip to main content

使用 GitHub Copilot 编写测试

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

简介

GitHub Copilot 可帮助你快速开发测试并提高工作效率。 本文将演示如何使用 Copilot 编写单元测试和集成测试。 尽管 Copilot 在生成基本函数测试时表现良好,但复杂方案需要更详细的提示和策略。 本文将逐步介绍使用 Copilot 拆分任务并验证代码正确性的实际示例。

先决条件

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

使用 Copilot Chat

编写单元测试

在本部分中,我们将了解如何使用 GitHub Copilot Chat 生成针对 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 Chat

生成单元测试

可以通过请求来提示 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 生成的测试可能并未涵盖所有场景,因此应始终评审生成的代码并添加可能需要的任何其他测试。

Tip

如果要求 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 类,使其包含与外部服务(例如向用户发送通知的 NotificationSystem)的交互。 NotificationSystem 表示需要测试的集成。

使用下面的代码片段更新 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)****。

Tip

如果 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