{"meta":{"title":"GitHub Copilot を使用したテストの記述","intro":"Copilot を使って単体テストと統合テストを生成し、コードの品質を向上させます。","product":"GitHub Copilot","breadcrumbs":[{"href":"/ja/copilot","title":"GitHub Copilot"},{"href":"/ja/copilot/tutorials","title":"チュートリアル"},{"href":"/ja/copilot/tutorials/write-tests","title":"テストを記述する"}],"documentType":"article"},"body":"# GitHub Copilot を使用したテストの記述\n\nCopilot を使って単体テストと統合テストを生成し、コードの品質を向上させます。\n\n## はじめに\n\nGitHub Copilot は、テストをすばやく開発し、生産性を向上させるのに役立つ場合があります。 この記事では、Copilot を使って単体テストと統合テストの両方を記述する方法について説明します。 Copilot は、基本的な関数のテストを生成する場合はうまく機能しますが、複雑なシナリオではより詳細なプロンプトと戦略が必要になります。 この記事では、Copilot を使ってタスクを分解し、コードの正しさを検証する実際の例について説明していきます。\n\n## 前提条件\n\n開始する前に、次のものが必要です。\n\n* [GitHub Copilot のサブスクリプション プラン](/ja/copilot/about-github-copilot/subscription-plans-for-github-copilot).\n* Visual Studio、Visual Studio Code、または JetBrains IDE。\n* JetBrains IDE にインストールされた [GitHub Copilot 拡張機能](/ja/copilot/managing-copilot/configure-personal-settings/installing-the-github-copilot-extension-in-your-environment)\n\n## コパイロットチャット\n\nを使って単体テストを作成する\n\nこのセクションでは、ギットハブ コパイロット チャット を使って Python クラスの単体テストを生成する方法について説明します。 この例では、Copilot を使って、`BankAccount` のようなクラスの単体テストを作成する方法を示します。 Copilot へのプロンプトを作成して、テストの生成、実行、結果の検証を行う方法を示します。\n\n### クラスの例: `BankAccount`\n\nまず、`BankAccount` クラスから始めましょう。これには、口座への預金、口座残高の引き出し、取得を行うメソッドが含まれています。 GitHub リポジトリに新しいファイル `bank_account.py` を作成し、Python で次の `BankAccount` クラスを追加します。\n\n```python\nclass BankAccount:\n    def __init__(self, initial_balance=0):\n        if initial_balance < 0:\n            raise ValueError(\"Initial balance cannot be negative.\")\n        self.balance = initial_balance\n\n    def deposit(self, amount):\n        if amount <= 0:\n            raise ValueError(\"Deposit amount must be positive.\")\n        self.balance += amount\n\n    def withdraw(self, amount):\n        if amount <= 0:\n            raise ValueError(\"Withdrawal amount must be positive.\")\n        if amount > self.balance:\n            raise ValueError(\"Cannot withdraw more than the current balance.\")\n        self.balance -= amount\n\n    def get_balance(self):\n        return self.balance\n```\n\n### コパイロットチャット\n\nを使った単体テストの生成\n\nCopilot に対して、さまざまなシナリオに対応したテストを記述するように求めるプロンプトを作成できます。 最善の結果を得るために、テスト要件は具体的に記述してください。\n`BankAccount` クラスの単体テストを生成するために使用できるプロンプトの例を次に示します。\n\n```\n          **プロンプト:** BankAccount() 関数に対する包括的な単体テスト スイートを Python で開発します。__ エッジ ケース、例外処理、データ検証など、さまざまなシナリオに対応する複数のテスト メソッドを記述します。\n```\n\nこのプロンプトでは、いくつかの重要なことを行っています。\n\n* **スコープ**: 「包括的な単体テスト スイート」を明示的に要求しています。そのため、Copilot は基本的なテストだけでなく、考えられるさまざまなユース ケースに対応したテストを生成します。\n* **シナリオ**: このプロンプトは、テストに以下を含める必要があることを指定しています。\n  * **エッジ ケース**: 境界条件のテスト (負の金額の預金または引き出し、利用可能な残高を超える引き出しなど)。\n  * **例外処理**: 特定の無効なアクションが行われたときに発生させる必要があるエラーのテスト (利用可能な残高を超える引き出し、負の金額の預金など)。\n  * **データ検証**: 入力がメソッドによって正しく検証され、処理されることを確認します。\n\nCopilot は、次のような応答を返します。\n\n```python\nimport unittest\nfrom bank_account import BankAccount\n\nclass TestBankAccount(unittest.TestCase):\n    def setUp(self):\n        self.account = BankAccount()\n\n    def test_initial_balance(self):\n        self.assertEqual(self.account.get_balance(), 0)\n\n    def test_deposit_positive_amount(self):\n        self.account.deposit(100)\n        self.assertEqual(self.account.get_balance(), 100)\n\n    def test_withdraw_within_balance(self):\n        self.account.deposit(100)\n        self.account.withdraw(50)\n        self.assertEqual(self.account.get_balance(), 50)\n\n    def test_deposit_negative_amount_raises_error(self):\n        with self.assertRaises(ValueError):\n            self.account.deposit(-100)\n\n    def test_withdraw_negative_amount_raises_error(self):\n        with self.assertRaises(ValueError):\n            self.account.withdraw(-50)\n\n    def test_withdraw_more_than_balance_raises_error(self):\n        self.account.deposit(100)\n        with self.assertRaises(ValueError):\n            self.account.withdraw(200)\n\n    def test_initial_balance_negative_raises_error(self):\n        with self.assertRaises(ValueError):\n            BankAccount(-100)\n\nif __name__ == '__main__':\n    unittest.main()\n```\n\n### テストケースの実行\n\nCopilot によって生成されたテスト スイートには、プロンプトで指定した各シナリオのテスト ケースが含まれています。\n`BankAccount` クラスの各関数は、さまざまな条件についてテストされます。たとえば、預金や引き出しなどの有効な操作だけでなく、負の金額、利用可能な残高を超える引き出しの試行などのエッジ ケースについてもテストされます。\n\nCopilot によって満足のいくテスト スイートが生成されたら、そのコードを新しいファイル `test_bank_account.py` に追加します。 テストの実行方法を尋ねることができます。\n\n```\n          **Prompt:**_\"unittest フレームワークを使用して Python でこれらの単体テストをどのように実行しますか？\"_\n```\n\nCopilot が次のような bash コマンドを返します。\n\n```bash\npython -m unittest test_bank_account.py\n```\n\nテストを実行すると、その出力がターミナルまたは IDE に表示されます。 すべてのテストに合格したら、`BankAccount` クラスが期待どおりに動作していることを確信できます。\n\n#### スラッシュ コマンド\n\nさらに、`/tests` スラッシュ コマンドを使って、Copilot に対して単体テストのフル スイートを記述するように求めるプロンプトを作成することもできます。 IDE の現在のタブでファイルが開かれていることを確認します。Copilot はそのファイルに対する単体テストを生成します。 Copilot が生成するテストはすべてのシナリオに対応していない場合があるため、必ず生成されたコードをレビューし、必要なテストを追加する必要があります。\n\n> \\[!TIP] まだ単体テストの対象になっていないコード ファイルのテストを記述するように Copilot に求める場合は、エディターの隣接するタブで既存の 1 つ以上のテスト ファイルを開くことで、Copilot に有益なコンテキストを提供できます。 使用するテスト フレームワークを Copilot が確認できるため、既存のテストと一貫性のあるテストが記述される可能性が高くなります。\n\nCopilot は、次のような単体テスト スイートを生成します。\n\n```python\nimport unittest\nfrom bank_account import BankAccount\n\nclass TestBankAccount(unittest.TestCase):\n    def setUp(self):\n        self.account = BankAccount()\n\n    def test_initial_balance(self):\n        self.assertEqual(self.account.get_balance(), 0)\n```\n\n## Copilot を使って統合テストを記述する\n\n統合テストは、システムのさまざまなコンポーネントを組み合わせたときに正しく動作することを確認するために不可欠です。 このセクションでは、`BankAccount` クラスを拡張して外部サービス `NotificationSystem` との対話を追加し、またモックを使って、実際の接続を必要とせずにシステムの動作をテストします。 統合テストの目標は、`BankAccount` クラスと `NotificationSystem` サービスの間の対話を検証し、それらが正しく連携していることを確認することです。\n\n### クラスの例: 通知サービスを使用する `BankAccount`\n\n```\n          `BankAccount` クラスを更新して、ユーザーに通知を送信する `NotificationSystem` などの外部サービスとの対話を含めましょう。 \n          `NotificationSystem` は、テストが必要になる統合を表しています。\n```\n\n次のコード スニペットを使って、`BankAccount` ファイルの `bank_account.py` クラスを更新します。\n\n```python\nclass BankAccount:\n    def __init__(self, initial_balance=0, notification_system=None):\n        if initial_balance < 0:\n            raise ValueError(\"Initial balance cannot be negative.\")\n        self.balance = initial_balance\n        self.notification_system = notification_system\n\n    def deposit(self, amount):\n        if amount <= 0:\n            raise ValueError(\"Deposit amount must be positive.\")\n        self.balance += amount\n        if self.notification_system:\n            self.notification_system.notify(f\"Deposited {amount}, new balance: {self.balance}\")\n\n    def withdraw(self, amount):\n        if amount <= 0:\n            raise ValueError(\"Withdrawal amount must be positive.\")\n        if amount > self.balance:\n            raise ValueError(\"Cannot withdraw more than the current balance.\")\n        self.balance -= amount\n\n        if self.notification_system:\n            self.notification_system.notify(f\"Withdrew {amount}, new balance: {self.balance}\")\n\n    def get_balance(self):\n        return self.balance\n```\n\nここでは、Copilot に対する要求を分解して、`BankAccount` クラスの統合テストをより小さくより管理しやすい部分に分けて記述します。 そうすることで、Copilot がより正確で関連性の高いテストを生成しやすくなります。\n\n```\n          **プロンプト:** \"_`deposit` クラスの `BankAccount` 関数の統合テストを記述します。モックを使用して `NotificationSystem` をシミュレートし、それが預金後に正しく呼び出されることを検証します_\"\n```\n\nこのプロンプトでは、いくつかの重要なことを行っています。\n\n* **スコープ**: 単なる単体テストではなく、`deposit` 関数と `NotificationSystem` の対話に焦点を当てて、統合テストを指定しています。\n* **モック**: 明示的にモックを使用して `NotificationSystem` をシミュレートし、その実際の実装に依存せずに外部システムとの対話をテストするように求めています。\n* **検証**: プロンプトでは、預金後に `NotificationSystem` が正しく呼び出されることを検証するように強調しており、各コンポーネントの統合が期待どおりに動作することを確かめています。\n* **具体性**: プロンプトでは、テストするメソッド (`deposit`) とクラス (`BankAccount`) が明確に示されています。\n\n> \\[!TIP] Copilot が無効なテストを生成する場合は、テストする関数の入力と出力の例を提供してください。 これにより、Copilot がその関数の期待される動作を評価しやすくなります。\n\nCopilot は、次のようなテスト スイートを生成します。\n\n```python\nimport unittest\nfrom unittest.mock import Mock\nfrom bank_account import BankAccount\n\nclass TestBankAccountIntegration(unittest.TestCase):\n    def setUp(self):\n        self.notification_system = Mock()\n\n    def test_deposit_with_notification(self):\n        account = BankAccount(initial_balance=100, notification_system=self.notification_system)\n        account.deposit(50)\n        self.assertEqual(account.get_balance(), 150)\n        self.notification_system.notify.assert_called_once_with(\"Deposited 50, new balance: 150\")\n\nif __name__ == '__main__':\n    unittest.main()\n```\n\n生成されたコードを新しいファイル `test_bank_account_integration.py` に追加します。\n\n### テストケースを改善すること\n\n上記のプロンプトでは、有効な預金が行われたときに `NotificationSystem` が呼び出されることを確認する 1 つのテスト ケースが生成されました。 しかし、預金時にエラーが発生するケースについては考慮されていません。 そのようなシナリオでは、`NotificationSystem` を呼び出すべきではありません。 無効な預金を扱うテスト ケースを追加し、通知システムがトリガーされないことを確認する必要があります。\n\n```\n          **プロンプト:** \"_無効な預金額を使用するテスト ケースを追加して、関数が正しい例外を発生させ、`NotificationService` が呼び出されないことを確認します_\"\n```\n\nCopilot は、次のようなテスト ケースを生成します。\n\n```python\n    def test_deposit_negative_amount_raises_error(self):\n        account = BankAccount(initial_balance=100, notification_system=self.notification_system)\n        with self.assertRaises(ValueError):\n            account.deposit(0)\n        self.notification_system.notify.assert_not_called()\n```\n\n### 改善できる部分を尋ねる\n\n預金の統合機能を検証するテスト ケースを記述したので、このテスト スイート内の改善点を調べる良い機会です。 現在のテストでも機能していますが、Copilot に対して、コード カバレッジを評価し、改善できる部分を提案するように求めるプロンプトを作成できます。\n\n```\n          **プロンプト:** \"_`BankAccount` クラスと `NotificationSystem` の統合を完全にカバーするためには、どのような追加のテストを含めるべきですか?_\"\n```\n\nCopilot にこのような質問をすれば、見落としているかもしれないテスト ケースを特定することができます。\nこの場合では、有効な預金と無効な預金はテストしましたが、引き出し機能はまだテストしていません。\n\nCopilot は、次のような更新されたテスト スイートを生成します。\n\n<details>\n  <summary>クリックして、生成された完全なコード例を展開します</summary>\n\n```python\nimport unittest\nfrom unittest.mock import Mock\nfrom bank_account import BankAccount\n\nclass TestBankAccountIntegration(unittest.TestCase):\n    def setUp(self):\n        self.notification_system = Mock()\n\n    def test_deposit_with_notification(self):\n        account = BankAccount(initial_balance=100, notification_system=self.notification_system)\n        account.deposit(50)\n        self.assertEqual(account.get_balance(), 150)\n        self.notification_system.notify.assert_called_once_with(\"Deposited 50, new balance: 150\")\n\n    def test_deposit_negative_amount_raises_error(self):\n        account = BankAccount(initial_balance=100, notification_system=self.notification_system)\n        with self.assertRaises(ValueError):\n            account.deposit(-50)\n        self.notification_system.notify.assert_not_called()\n\n    def test_deposit_zero_amount_raises_error(self):\n        account = BankAccount(initial_balance=100, notification_system=self.notification_system)\n        with self.assertRaises(ValueError):\n            account.deposit(0)\n        self.notification_system.notify.assert_not_called()\n\n    def test_withdraw_with_notification(self):\n        account = BankAccount(initial_balance=100, notification_system=self.notification_system)\n        account.withdraw(30)\n        self.assertEqual(account.get_balance(), 70)\n        self.notification_system.notify.assert_called_once_with(\"Withdrew 30, new balance: 70\")\n\n    def test_withdraw_exceeding_balance_raises_error(self):\n        account = BankAccount(initial_balance=100, notification_system=self.notification_system)\n        with self.assertRaises(ValueError):\n            account.withdraw(150)\n        self.notification_system.notify.assert_not_called()\n\n    def test_withdraw_negative_amount_raises_error(self):\n        account = BankAccount(initial_balance=100, notification_system=self.notification_system)\n        with self.assertRaises(ValueError):\n            account.withdraw(-30)\n        self.notification_system.notify.assert_not_called()\n\n    def test_withdraw_zero_amount_raises_error(self):\n        account = BankAccount(initial_balance=100, notification_system=self.notification_system)\n        with self.assertRaises(ValueError):\n            account.withdraw(0)\n        self.notification_system.notify.assert_not_called()\n\n    def test_initial_negative_balance_raises_error(self):\n        with self.assertRaises(ValueError):\n            BankAccount(initial_balance=-100, notification_system=self.notification_system)\n\nif __name__ == '__main__':\n    unittest.main()\n```\n\n</details>\n\nCopilot が満足のいくテスト スイートを生成したら、次のコマンドを使ってテストを実行し、その結果を確認します。\n\n```bash\npython -m unittest test_bank_account_integration.py\n```\n\n## Copilotスペース を使ったテスト提案の改善\n\nCopilotスペース は、タスク固有のコンテキストを整理して Copilot と共有できる機能です。 これは、ユーザーが受け取る提案の関連性を向上させるのに役立ちます。 Copilot にプロジェクトのコンテキストをより多く提供することで、テストの提案が改善されます。\n\nたとえば、次のものを含むスペースを作成できます。\n\n* テスト対象のモジュール (`payments.js` など)\n* 現在のテスト スイート (`payments.test.js` など)\n* テスト カバレッジ レポートまたは不足している項目に関するメモ書き\n\nスペース内では、Copilot に次のような質問をすることができます。\n\n>\n\n```\n          `payments.test.js` のロジックを基にすると、`payments.js` にはどのようなテスト ケースが不足していますか?\n```\n\nまたは:\n\n> 既存のテスト スイートの構造に従って、`refund.js` の払戻ロジックの単体テストを記述します。\n\nCopilotスペース の使用方法の詳細については、「[GitHub コピロット スペースについて](/ja/copilot/using-github-copilot/copilot-spaces/about-organizing-and-sharing-context-with-copilot-spaces)」を参照してください。"}