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 クラスの単体テストを生成するために使用できるプロンプトの例を次に示します。

プロンプト: BankAccount() 関数に対する包括的な単体テスト スイートを Python で開発します。__ エッジ ケース、例外処理、データ検証など、さまざまなシナリオに対応する複数のテスト メソッドを記述します。

このプロンプトでは、いくつかの重要なことを行っています。

  • スコープ: 「包括的な単体テスト スイート」を明示的に要求しています。そのため、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 が生成するテストはすべてのシナリオに対応していない場合があるため、必ず生成されたコードをレビューし、必要なテストを追加する必要があります。

Tip

まだ単体テストの対象になっていないコード ファイルのテストを記述するように Copilot に求める場合は、エディターの隣接するタブで既存の 1 つ以上のテスト ファイルを開くことで、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 が呼び出されることを確認する 1 つのテスト ケースが生成されました。 しかし、預金時にエラーが発生するケースについては考慮されていません。 そのようなシナリオでは、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