# GitHub Copilot を使用したテストの記述

Copilotを使用して単体テストと統合テストを生成し、コード品質の向上に役立ちます。

## はじめに

GitHub Copilot は、テストを迅速に開発し、生産性を向上させるのに役立つ可能性があります。 この記事では、 Copilot を使用して単体テストと統合テストの両方を記述する方法について説明します。
Copilotは基本的な関数のテストを生成するときに適切に機能しますが、複雑なシナリオでは、より詳細なプロンプトと戦略が必要です。 この記事では、 Copilot を使用してタスクを分解し、コードの正確性を確認する実際の例について説明します。

## 前提条件

開始する前に、次のものが必要です。

* [
  GitHub Copilot サブスクリプション プラン](/ja/copilot/get-started/plans)。
* Visual Studio、 Visual Studio Code、または JetBrains IDE。
* IDE にインストールされている [GitHub Copilot 拡張機能](/ja/copilot/how-tos/set-up/install-copilot-extension) 。

## Copilot Chat を使用した単体テストの作成

このセクションでは、GitHub Copilot Chatを使用して、Python クラスの単体テストを生成する方法について説明します。 この例では、 Copilot を使用して、 `BankAccount`などのクラスの単体テストを作成する方法を示します。 テストの生成、テストの実行、結果の検証を Copilot に求める方法について説明します。

### クラスの例: `BankAccount`

まず、`BankAccount` クラスから始めましょう。これには、口座への預金、口座残高の引き出し、取得を行うメソッドが含まれています。
`bank_account.py` リポジトリに新しいファイル GitHubを作成し、Pythonに次の`BankAccount` クラスを追加します。

```python
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 では、次のような応答が返されます。

```python
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`にコードを追加します。 テストの実行方法を尋ねることができます。

**Prompt:***"unittest フレームワークを使用して Python でこれらの単体テストをどのように実行しますか？"*

Copilot により、次の bash コマンドが表示されます。

```bash
python -m unittest test_bank_account.py
```

テストを実行すると、その出力がターミナルまたは IDE に表示されます。 すべてのテストに合格したら、`BankAccount` クラスが期待どおりに動作していることを確信できます。

#### スラッシュ コマンド

さらに、Copilotスラッシュ コマンドを使用して、単体テストの完全なスイートを記述するように`/tests`に求めることができます。 IDE の現在のタブでファイルが開かれていることを確認し、 Copilot はそのファイルの単体テストを生成します。
Copilot生成されるテストはすべてのシナリオに対応しているわけではないため、生成されたコードを常に確認し、必要なテストを追加する必要があります。

> \[!TIP] 単体テストでまだカバーされていないコード ファイルのテストを記述するように Copilot に依頼した場合は、エディターの隣接するタブで 1 つ以上の既存のテスト ファイルを開くことで、 Copilot に便利なコンテキストを提供できます。
> Copilot を使用すると、使用するテスト フレームワークを確認でき、既存のテストと一貫性のあるテストを作成する可能性が高くなります。

Copilot では、次のような単体テスト スイートが生成されます。

```python
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` は、テストが必要になる統合を表しています。

次のコード スニペットを使って、`BankAccount` ファイルの `bank_account.py` クラスを更新します。

```python
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 生成するのに役立ちます。

**プロンプト:** "*`deposit` クラスの `BankAccount` 関数の統合テストを記述します。モックを使用して `NotificationSystem` をシミュレートし、それが預金後に正しく呼び出されることを検証します*"

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

* **スコープ**: 単なる単体テストではなく、`deposit` 関数と `NotificationSystem` の対話に焦点を当てて、統合テストを指定しています。
* **モック**: 明示的にモックを使用して `NotificationSystem` をシミュレートし、その実際の実装に依存せずに外部システムとの対話をテストするように求めています。
* **検証**: プロンプトでは、預金後に `NotificationSystem` が正しく呼び出されることを検証するように強調しており、各コンポーネントの統合が期待どおりに動作することを確かめています。
* **具体性**: プロンプトでは、テストするメソッド (`deposit`) とクラス (`BankAccount`) が明確に示されています。

> \[!TIP]
> Copilotが無効なテストを生成している場合は、テストする関数の入力と出力の例を指定します。 これにより、関数の期待される動作を Copilot 評価するのに役立ちます。

Copilot では、次のようなテスト スイートが生成されます。

```python
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 では、次のようなテスト ケースが生成されます。

```python
    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 では、次のような更新されたテスト スイートが生成されます。

<details>
  <summary>クリックして、生成された完全なコード例を展開します</summary>

```python
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()
```

</details>

Copilot が満足のいくテスト スイートを生成したら、次のコマンドを使ってテストを実行し、その結果を確認します。

```bash
python -m unittest test_bank_account_integration.py
```

## Copilot Spacesを使用してテストの提案を改善する

Copilot Spaces は、タスク固有のコンテキストを整理して Copilotと共有できる機能です。 これは、ユーザーが受け取る提案の関連性を向上させるのに役立ちます。 プロジェクトに関するより多くのコンテキストを Copilot 提供することで、より良いテスト候補を得ることができます。

たとえば、次のものを含むスペースを作成できます。

* テスト対象のモジュール (`payments.js` など)
* 現在のテスト スイート (`payments.test.js` など)
* テスト カバレッジ レポートまたは不足している項目に関するメモ書き

このスペースでは、次のような Copilot 質問をすることができます。

> `payments.test.js` のロジックを基にすると、`payments.js` にはどのようなテスト ケースが不足していますか?

または:

> 既存のテスト スイートの構造に従って、`refund.js` の払戻ロジックの単体テストを記述します。

Copilot Spacesの使用方法の詳細については、「[GitHub コピロット スペースについて](/ja/copilot/concepts/context/spaces)」を参照してください。