跳至主要内容

使用 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 类按预期工作。

斜杠命令

此外,您可以使用 /tests 斜杠命令提示 Copilot 编写完整的单元测试套件。确保在 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(向用户发送通知)进行交互,并使用 mock 对系统行为进行测试,无需真实连接。集成测试的目标是验证 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 方法编写集成测试。使用 mock 模拟 NotificationSystem,并验证在存款后该系统被正确调用。”

此提示包含了若干重要要点

  • 范围:它指定了集成测试,关注 deposit 方法与 NotificationSystem 之间的交互,而非仅仅单元测试。
  • Mock:它明确要求使用 mock 来模拟 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

使用 Copilot Spaces 改进测试建议

Copilot Spaces 是一种功能,允许您组织并与 Copilot 共享特定任务的上下文。这有助于提升收到的建议的相关性。通过向 Copilot 提供更多项目上下文,您可以获得更佳的测试建议。

例如,您可以创建一个包含以下内容的空间:

  • 您正在测试的模块(例如 payments.js
  • 当前的测试套件(例如 payments.test.js
  • 测试覆盖率报告或关于缺失内容的备注

在该空间中,您可以向 Copilot 提问,例如:

基于 payments.js 中的逻辑,payments.test.js 缺少哪些测试用例?

refund.js 中的退款逻辑编写单元测试,遵循现有测试套件的结构。

欲了解有关使用 Copilot Spaces 的更多信息,请参阅 关于 GitHub Copilot Spaces

© . This site is unofficial and not affiliated with GitHub, Inc.