Python ライブラリから Azure OpenAI Service にリクエストを行い OpenAPI と PlantUML からテストコードを生成・追加する

これまで 3 回にわたって、[Azure OpenAI Service](https://learn.microsoft.com/ja-jp/azure/cognitive-services/openai/overview) を使って [OpenAPI 仕様](https://www.openapis.org/) と [PlantUML](https://plantuml.com/ja/) からテストコードを生成する取り組みについての情報共有を行ってきました。

1. [Azure OpenAI Service を使って OpenAPI と PlantUML からテストコードを生成する](https://qiita.com/kske_t/items/f04c2ed629417f6b2993)
2. [PromptGenerator メソッドによって最適化されたプロンプトを使って Azure OpenAI Service で OpenAPI と PlantUML からテストコードを生成する](https://qiita.com/kske_t/items/b854e80e3a3c34513924)
3. [PromptGenerator メソッドによって最適化されたプロンプトを使って Azure OpenAI Service で OpenAPI と PlantUML からテストコードを生成する #2](https://qiita.com/kske_t/items/ab470d065a6b51e97d63)

最終的には、[PromptGenerator](https://www.skool.com/chatgpt/promptgenerator?p=1e5ede93) メソッドを用いて生成したプロンプトを使って GPT-4 でテストコードを生成するのが、最も安定的かつ致命的な間違いの無いコードになることが分かりました。

次はこれを実際の開発業務に適用することを考えると思いますが、Playground を使うというのは、プログラマとして格好悪いですし、[三大美徳](https://thethreevirtues.com/)にも反しますよね。

また、前回 GPT-4 を利用することでテストダブルを正しく使えるようになる (確率が高まる) ことがわかりましたが、一方でテストケースが GPT-3 の場合に比べて減少してしまったという問題が発生しました。

そこで、本記事では [OpenAI Python ライブラリ](https://github.com/openai/openai-python)を使ってテストコードを生成・追加する方法について解説します。


## ゴール

* テストコードを生成する。
* チャットの特徴を利用して、テストケースを追加する。
* 上記を OpenAI Python Library を使って実行する。


## 前提

* プログラミング言語には Python を用います。パッケージ管理には `pip` または `conda` がインストールされていることを想定します。
* 今回、API にはインターネットからアクセスします。([仮想ネットワーク](https://learn.microsoft.com/ja-jp/azure/cognitive-services/cognitive-services-virtual-networks?context=%2Fazure%2Fcognitive-services%2Fopenai%2Fcontext%2Fcontext&tabs=portal)は使用しません。)
* 本記事においては、環境変数に以下の値を格納しておきます。いずれの値も [Azure Portal](https://learn.microsoft.com/ja-jp/azure/azure-portal/azure-portal-overview) における Azure OpenAI リソースから、 `[リソース管理] > [キーとエンドポイント]` より取得できます。
  * `OPENAI_API_BASE`
    * Azure OpenAI Service のエンドポイントURL。例えば `https://my-openai-example-endpoint.openai.azure.com/` といった文字列です。
  * `OPENAI_API_KEY`
    * Azure OpenAI Service のキー。キー1 または キー2 のいずれかの値を設定します。

## 免責

* 本記事の内容は、執筆時点のものです。LLM の変化やゆらぎもあるため、再現性は保証されません。
* 本記事の内容は検証レベルのものです。完全な手法に関する情報を提供するものではありません。
* 本記事で使用する PlantUML は、細部まで作り込んでいるわけではありません。細かい部分で間違いがある可能性があります。
* テストケースの追加は、新たなテストクラスを作成する形となっています。本記事執筆時点において、最初に生成されたテストコードのテストクラスの中にテストケースを追加した上ですべてのテストケースを出力する方法については、確立できていません。


## 準備

### ライブラリのインストール

環境に合わせていずれかを実行してください。

```bash
pip install openai
```

```bash
conda install openai
```

## Azure OpenAI Service への API リクエスト

ステップは以下の通りです。

1. 初期化
2. OpenAPI 仕様 の定義
3. PlantUML シーケンス図の定義
4. PlantUML クラス図の定義
5. インストラクションの定義
6. テスト対象の定義
7. 各定義からプロンプトを作成
8. メッセージの作成
9. リクエストの実行およびレスポンスの取得

### 初期化

```python
import os
import openai
openai.api_type = "azure"
openai.api_base = os.getenv("OPENAI_API_BASE")
openai.api_version = "2023-03-15-preview"
openai.api_key = os.getenv("OPENAI_API_KEY")
```

反復的に実行することを想定し、後で使用する変数も初期化しておきます。(任意)

```python
messages = []
response = None
pronpt = None
code = None
code_string = None
```

### OpenAPI 仕様 の定義

これまでと同様、OpenAPI 公式のサンプル "[petstore-simple.json](https://github.com/OAI/OpenAPI-Specification/blob/main/examples/v2.0/json/petstore-simple.json)" を使用します。

```python
openapi_spec = '''{
  "swagger": "2.0",
  "info": {
    "version": "1.0.0",
    "title": "Swagger Petstore",
    "description": "A sample API that uses a petstore as an example to demonstrate features in the swagger-2.0 specification",
    "termsOfService": "http://swagger.io/terms/",
    "contact": {
      "name": "Swagger API Team"
    },
    "license": {
      "name": "MIT"
    }
  },
  "host": "petstore.swagger.io",
  "basePath": "/api",
  "schemes": [
    "http"
  ],
  "consumes": [
    "application/json"
  ],
  "produces": [
    "application/json"
  ],
  "paths": {
    "/pets": {
      "get": {
        "description": "Returns all pets from the system that the user has access to",
        "operationId": "findPets",
        "produces": [
          "application/json",
          "application/xml",
          "text/xml",
          "text/html"
        ],
        "parameters": [
          {
            "name": "tags",
            "in": "query",
            "description": "tags to filter by",
            "required": false,
            "type": "array",
            "items": {
              "type": "string"
            },
            "collectionFormat": "csv"
          },
          {
            "name": "limit",
            "in": "query",
            "description": "maximum number of results to return",
            "required": false,
            "type": "integer",
            "format": "int32"
          }
        ],
        "responses": {
          "200": {
            "description": "pet response",
            "schema": {
              "type": "array",
              "items": {
                "$ref": "#/definitions/Pet"
              }
            }
          },
          "default": {
            "description": "unexpected error",
            "schema": {
              "$ref": "#/definitions/ErrorModel"
            }
          }
        }
      },
      "post": {
        "description": "Creates a new pet in the store.  Duplicates are allowed",
        "operationId": "addPet",
        "produces": [
          "application/json"
        ],
        "parameters": [
          {
            "name": "pet",
            "in": "body",
            "description": "Pet to add to the store",
            "required": true,
            "schema": {
              "$ref": "#/definitions/NewPet"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "pet response",
            "schema": {
              "$ref": "#/definitions/Pet"
            }
          },
          "default": {
            "description": "unexpected error",
            "schema": {
              "$ref": "#/definitions/ErrorModel"
            }
          }
        }
      }
    },
    "/pets/{id}": {
      "get": {
        "description": "Returns a user based on a single ID, if the user does not have access to the pet",
        "operationId": "findPetById",
        "produces": [
          "application/json",
          "application/xml",
          "text/xml",
          "text/html"
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "description": "ID of pet to fetch",
            "required": true,
            "type": "integer",
            "format": "int64"
          }
        ],
        "responses": {
          "200": {
            "description": "pet response",
            "schema": {
              "$ref": "#/definitions/Pet"
            }
          },
          "default": {
            "description": "unexpected error",
            "schema": {
              "$ref": "#/definitions/ErrorModel"
            }
          }
        }
      },
      "delete": {
        "description": "deletes a single pet based on the ID supplied",
        "operationId": "deletePet",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "description": "ID of pet to delete",
            "required": true,
            "type": "integer",
            "format": "int64"
          }
        ],
        "responses": {
          "204": {
            "description": "pet deleted"
          },
          "default": {
            "description": "unexpected error",
            "schema": {
              "$ref": "#/definitions/ErrorModel"
            }
          }
        }
      }
    }
  },
  "definitions": {
    "Pet": {
      "type": "object",
      "allOf": [
        {
          "$ref": "#/definitions/NewPet"
        },
        {
          "required": [
            "id"
          ],
          "properties": {
            "id": {
              "type": "integer",
              "format": "int64"
            }
          }
        }
      ]
    },
    "NewPet": {
      "type": "object",
      "required": [
        "name"
      ],
      "properties": {
        "name": {
          "type": "string"
        },
        "tag": {
          "type": "string"
        }
      }
    },
    "ErrorModel": {
      "type": "object",
      "required": [
        "code",
        "message"
      ],
      "properties": {
        "code": {
          "type": "integer",
          "format": "int32"
        },
        "message": {
          "type": "string"
        }
      }
    }
  }
}
'''
```

### PlantUML シーケンス図の定義

これまでと同様、上記の OpenAPI 仕様に基づいて PlantUML で作成した、簡単なシーケンス図を使用します。
描画済みの図に関しては、[過去の記事](https://qiita.com/kske_t/items/f04c2ed629417f6b2993)を参照してください。

```python
plantuml_sequence_diagrams = '''@startuml

title ペット
header %page% of %lastpage%
footer Copyright(c) All rights reserved.

autoactivate on
autonumber "<b>[00]"

actor ユーザー as user
entity ペットストアUI as ui
entity ペットストアAPI as api
database データベース as db

== ペットのリストを取得 ==

user -> ui : ペットリストボタンをクリック
ui -> api : findPets
api -> db : SELECT * FROM pets
return
return 200, pet response
return ペットのリストを表示

== ペットの詳細を取得 ==

user -> ui : ペットの詳細ボタンをクリック
ui -> api : findPetById
api -> db : SELECT * FROM pets WHERE id = ${pet_id}
return
return 200, pet response
return ペットの詳細を表示

== ペットを購入 ==

user -> ui : ペットの購入ボタンをクリック
ui -> api : addPet
group transaction
    api -> db : transaction
    api -> db : INSERT INTO orders VALUES (${user_id}, ${pet_id}, ${datetime}, ${created_at}, ${updated_at})
    api -> db : DELETE FROM pets WHERE pet_id = ${pet_id}
    api -> db : commit
    return
    return
    return
    return
end
return 200, pet response
return 購入完了画面を表示

== ペットの登録を削除 ==

user -> ui : ペットの登録削除ボタンをクリック
ui -> api : deletePet
group transaction
    api -> db : DELETE FROM pets WHERE pet_id = (SELECT id FROM orders WHERE pet_id = ${pet_id} AND user_id = ${user_id})
    api -> db : DELETE FROM orders WHERE pet_id = ${pet_id} AND user_id = ${user_id}
    return
    return
end
return 204, pet deleted
return 削除完了画面を表示

== ペットの購入に失敗 ==

user -> ui : ペットの購入ボタンをクリック
ui -> api : addPet
group transaction
    api -> db : transaction
    api -> db : INSERT INTO orders VALUES (${user_id}, ${pet_id}, ${datetime}, ${created_at}, ${updated_at})
    api -> db !! : DELETE FROM pets WHERE pet_id = ${pet_id}
    api -> db : rollback
    return
    return
    return
end
return 500, unexpected error
return 購入失敗画面を表示

@enduml
'''
```

### PlantUML クラス図の定義

これまでと同様、上記の OpenAPI 仕様に基づいて PlantUML で作成した、簡単なクラス図を使用します。
描画済みの図に関しては、[過去の記事](https://qiita.com/kske_t/items/f04c2ed629417f6b2993)を参照してください。

```python
plantuml_class_diagrams = '''@startuml

class User {
    - id
    - name
}

class Pet {
    - id
    - name
    - type
    - tag
}

class Order {
    - id
    - user_id
    - pet_id
}

Order "1" -- "*" User
Order "1" -- "*" Pet

@enduml
'''
```

### インストラクションの定義

PromptGenerator メソッドで作成したものです。[詳細は過去の記事](https://qiita.com/kske_t/items/b854e80e3a3c34513924)を参照ください。

```python
revised_prompt = '''Generate unit test code in Python using the unittest framework and unittest.mock's patch module for given text materials such as OpenAI Specifications (JSON or YAML), PlantUML sequence diagrams (PlantUML format), and PlantUML class diagrams (PlantUML format), following the PEP 8 coding standards.

Key aspects to consider:

* Aim for more than 90% code coverage, with 100% being preferred.
* Consider edge cases and error handling from the given materials step-by-step.
* Follow the AAA (Arrange-Act-Assert) style without including comments in the test code.
* Generate test cases and prepare test data step-by-step, allowing the user to specify test targets/objectives based on the actors of the sequence diagrams.
* Base test cases on class diagrams first, then sequence diagrams, and finally OpenAPI specifications for API servers.
* Define immutable test data in the setUp method of the test class that inherits unittest.TestCase, and specify additional test data in test cases if needed.
* Use the @patch('...') decorator for mocking instead of with patch('...') as mock_foo: statement.
* Apply the spy test double pattern using the assert_called_once_with method of patch.object when applicable, and use the with patch.object('...') as mock_foo: statement in spy test double pattern cases.
* Be mindful of token limitations and avoid exceeding them when generating test code.
* Never apply test doubles like stub, mock, and the rest for the test targets/objectives.
* Ensure that the test code is MECE (mutually exclusive and collectively exhaustive), structured, organized, and clean.
'''
```

### テスト対象の定義

```python
test_target = "ペットストアAPI"
```

### 各定義からプロンプトを作成

```python
prompt = f'''/*
{revised_prompt}

* Test target: {test_target}

* OpenAPI Specifications:
"""
{openapi_spec}
"""

* PlantUML sequence diagrams:
"""
{plantuml_sequence_diagrams}
"""

* PlantUML class diagrams:
"""
{plantuml_class_diagrams}
"""
*/
'''
```

### メッセージの作成

```python
messages = [
    {"role":"system","content":"You are an AI assistant that helps people find information."},
    {"role":"user","content":prompt}
]
```

### リクエストの実行およびレスポンスの取得

```python
response = openai.ChatCompletion.create(
  engine="gpt-4-32k-0314",
  messages = messages,
  temperature=0.7,
  max_tokens=1600,
  top_p=0.95,
  frequency_penalty=0,
  presence_penalty=0,
  stop=None)
```

以下、パラメータの一覧を作成しましたので、参照してください。

| # | 項目 | 型 | 最小値 | 最大値 | 説明 |
| ---:| --- | --- | --- | --- | --- |
| 1 | engine | str | - | - | Azure OpenAI Service にデプロイしたモデルに付与した名前 |
| 2 | messages | list | - | - | ユーザーとAIアシスタントの間で交換されたメッセージのリストです。各メッセージは、{'role': 'system', 'content': '...'} 、{'role': 'user', 'content': '...'} 、または{'role': 'assistant', 'content': '...'} の形式で記述されます。roleは、メッセージの送信者を示し、contentはメッセージの内容を示します。APIは、このメッセージのコンテキストを使用して、適切な応答を生成します。  |
| 3 | temperature | float | 0 | 1 | 生成されるテキストのランダム性を制御します。 温度を下げることは、モデルがより反復的で決定論的な応答を生成することを意味します。 温度を上げると、より予想外または創造的な反応が得られます。 両方ではなく、温度またはトップ P を調整してみてください。 |
| 4 | max_tokens | float | 1 | 32768 (※1) | モデル応答ごとのトークン数に制限を設定します。 API は、プロンプト (システム メッセージ、例、メッセージ履歴、およびユーザー クエリを含む) とモデルの応答の間で共有される最大 32768 個のトークンをサポートします。 1 つのトークンは、一般的な英語のテキストで約 4 文字です。この値を低く設定すると、生成されるテキストが短くなります。 (※1) |
| 5 | top_p | float | 0 | 1 | 生成されるテキストの多様性を制御するために、トークンの選択確率に基づいて使用されるサンプリング手法です。温度と同様に、これはランダム性を制御しますが、別の方法を使用します。 トップ P を下げると、モデルのトークン選択がより可能性の高いトークンに絞り込まれます。 Top P を増やすと、モデルは可能性の高いトークンと低いトークンの両方から選択できるようになります。 両方ではなく、温度またはトップ P を調整してみてください。 |
| 6 | frequency_penalty | float | 0 | 2 | これまでにテキストに出現した頻度に基づいて、トークンを繰り返す可能性を比例的に減らします。値が高いほど、低頻度のトークンが選択される可能性が低くなります。これにより、応答でまったく同じテキストが繰り返される可能性が低くなります。 |
| 7 | presence_penalty | float | 0 | 2 | これまでにテキストに登場したトークンを繰り返す可能性を減らします。値が高いほど、繰り返しのペナルティが大きくなります。これにより、応答に新しいトピックが導入される可能性が高くなります。 |
| 8 | stop | Optional[list] | - | - | 応答の生成を停止する特定の文字列またはシーケンスのリストを指定します。これにより、APIが特定の文字列を検出したときに、その後の生成を停止できます。  |
| 9 | n | int | 1 | N/A | APIから返される異なる応答の数を指定します。nを2以上に設定すると、同じ入力に対して複数の異なる応答が返されます。これは、応答の多様性を確保するために役立ちます。 |
| 10 | return_prompt | bool | - | - | 通常はFalseに設定されていますが、これをTrueに設定すると、APIから返されるJSONレスポンスにプロンプトも含まれます。 |
| 11 | echo | bool | - | - | 通常はFalseに設定されていますが(※2)、これをTrueに設定すると、APIから返されるJSONレスポンスに入力メッセージも含まれます。 |
| 12 | best_of | int | 1 | 20 | 複数の応答を生成し、すべてのトークンで合計確率が最も高い応答のみを表示します。 未使用の候補には使用コストがかかるため、このパラメーターを慎重に使用し、最大応答長と終了トリガーのパラメーターも設定してください。 ストリーミングは、これが 1 に設定されている場合にのみ機能することに注意してください。best_of の値が大きいほど、APIはより多くの応答を生成して評価しますが、実行時間が長くなる可能性があります。 |
| 13 | log_level | str | - | - | APIから返されるログのレベルを指定します。これは、デバッグや問題解決に役立つ情報を取得するために使用できます。 例: debug, info |
| 14 | logprobs | int | 0 | N/A | 生成されたテキストのトークンごとに対数確率を取得するために使用されます。たとえば、「logprobs」が 10 の場合、API は最も可能性の高い 10 個のトークンのリストを返します。 「logprobs」が指定されている場合、API は生成されたトークンの logprob を常に返すため、応答には最大で「logprobs+1」要素が含まれる場合があります。この値は、0から設定でき、値が大きいほど、APIから返される対数確率の詳細が増えます。 |

※1 gpt-4-32k の場合。

※2 余談ですが、CLI では `True` がデフォルトになっている点が API と異なります。(ソースコード cli.py L172 参照)

※3 一部に機械翻訳またはGPT-4を使用しています。

※4 もし間違い等あればご指摘ください。


----


## レスポンスの処理

取得したレスポンスを処理し、コードを取り出します。

ステップは以下の通りです。

1. 内容の確認 (任意)
2. コードの取得

### 内容の確認 (任意)

```python
print(response)
```

```json
{
  "choices": [
    {
      "finish_reason": "stop",
      "index": 0,
      "message": {
        "content": "Based on the given OpenAPI Specifications and PlantUML diagrams, I have generated the following test code using the unittest framework and unittest.mock's patch module:\n\n```python\nimport unittest\nfrom unittest.mock import patch\nfrom petstore_api import PetStoreAPI, Pet, Order\n\n\nclass TestPetStoreAPI(unittest.TestCase):\n    def setUp(self):\n        self.api = PetStoreAPI()\n        self.pets = [\n            Pet(1, \"Dog\", \"Bulldog\", \"friendly\"),\n            Pet(2, \"Cat\", \"Persian\", \"lazy\"),\n            Pet(3, \"Bird\", \"Parrot\", \"talkative\")\n        ]\n        self.orders = [\n            Order(1, 1, 1),\n            Order(2, 2, 2)\n        ]\n\n    @patch('petstore_api.PetStoreAPI._get_pets_from_db')\n    def test_find_pets(self, mock_get_pets_from_db):\n        mock_get_pets_from_db.return_value = self.pets\n        result = self.api.find_pets()\n        self.assertEqual(result, self.pets)\n        mock_get_pets_from_db.assert_called_once_with()\n\n    @patch('petstore_api.PetStoreAPI._get_pet_from_db_by_id')\n    def test_find_pet_by_id(self, mock_get_pet_from_db_by_id):\n        pet_id = 1\n        mock_get_pet_from_db_by_id.return_value = self.pets[0]\n        result = self.api.find_pet_by_id(pet_id)\n        self.assertEqual(result, self.pets[0])\n        mock_get_pet_from_db_by_id.assert_called_once_with(pet_id)\n\n    @patch('petstore_api.PetStoreAPI._delete_pet_from_db')\n    @patch('petstore_api.PetStoreAPI._delete_order_from_db')\n    def test_delete_pet(self, mock_delete_order_from_db, mock_delete_pet_from_db):\n        pet_id = 1\n        user_id = 1\n        self.api.delete_pet(pet_id, user_id)\n        mock_delete_order_from_db.assert_called_once_with(pet_id, user_id)\n        mock_delete_pet_from_db.assert_called_once_with(pet_id)\n\n    @patch('petstore_api.PetStoreAPI._add_pet_to_db')\n    @patch('petstore_api.PetStoreAPI._create_order_in_db')\n    def test_add_pet(self, mock_create_order_in_db, mock_add_pet_to_db):\n        user_id = 3\n        pet_id = 3\n        mock_create_order_in_db.return_value = Order(3, user_id, pet_id)\n        mock_add_pet_to_db.return_value = self.pets[2]\n        result = self.api.add_pet(user_id, pet_id)\n        self.assertEqual(result, self.pets[2])\n        mock_create_order_in_db.assert_called_once_with(user_id, pet_id)\n        mock_add_pet_to_db.assert_called_once_with(pet_id)\n\n\nif __name__ == '__main__':\n    unittest.main()\n```\n\nNote that this test code assumes the existence of a `petstore_api` module that implements the `PetStoreAPI`, `Pet`, and `Order` classes, as well as their corresponding database access methods. Please make sure to implement these classes and methods in your application for the tests to work correctly.",
        "role": "assistant"
      }
    }
  ],
  "created": 1681315820,
  "id": "chatcmpl-*****************************",
  "model": "gpt-4-32k",
  "object": "chat.completion",
  "usage": {
    "completion_tokens": 680,
    "prompt_tokens": 2278,
    "total_tokens": 2958
  }
}
```

### コードの取得

正規表現を使うと処理時間が長くなるため、通常の文字列の処理を採用しました。

```python
code = response.choices[0].message.content.split('```')[1].lstrip('python\n')
code_string = f'''{code}'''
print(code)
```

```python
import unittest
from unittest.mock import patch
from petstore_api import PetStoreAPI, Pet, Order


class TestPetStoreAPI(unittest.TestCase):
    def setUp(self):
        self.api = PetStoreAPI()
        self.pets = [
            Pet(1, "Dog", "Bulldog", "friendly"),
            Pet(2, "Cat", "Persian", "lazy"),
            Pet(3, "Bird", "Parrot", "talkative")
        ]
        self.orders = [
            Order(1, 1, 1),
            Order(2, 2, 2)
        ]

    @patch('petstore_api.PetStoreAPI._get_pets_from_db')
    def test_find_pets(self, mock_get_pets_from_db):
        mock_get_pets_from_db.return_value = self.pets
        result = self.api.find_pets()
        self.assertEqual(result, self.pets)
        mock_get_pets_from_db.assert_called_once_with()

    @patch('petstore_api.PetStoreAPI._get_pet_from_db_by_id')
    def test_find_pet_by_id(self, mock_get_pet_from_db_by_id):
        pet_id = 1
        mock_get_pet_from_db_by_id.return_value = self.pets[0]
        result = self.api.find_pet_by_id(pet_id)
        self.assertEqual(result, self.pets[0])
        mock_get_pet_from_db_by_id.assert_called_once_with(pet_id)

    @patch('petstore_api.PetStoreAPI._delete_pet_from_db')
    @patch('petstore_api.PetStoreAPI._delete_order_from_db')
    def test_delete_pet(self, mock_delete_order_from_db, mock_delete_pet_from_db):
        pet_id = 1
        user_id = 1
        self.api.delete_pet(pet_id, user_id)
        mock_delete_order_from_db.assert_called_once_with(pet_id, user_id)
        mock_delete_pet_from_db.assert_called_once_with(pet_id)

    @patch('petstore_api.PetStoreAPI._add_pet_to_db')
    @patch('petstore_api.PetStoreAPI._create_order_in_db')
    def test_add_pet(self, mock_create_order_in_db, mock_add_pet_to_db):
        user_id = 3
        pet_id = 3
        mock_create_order_in_db.return_value = Order(3, user_id, pet_id)
        mock_add_pet_to_db.return_value = self.pets[2]
        result = self.api.add_pet(user_id, pet_id)
        self.assertEqual(result, self.pets[2])
        mock_create_order_in_db.assert_called_once_with(user_id, pet_id)
        mock_add_pet_to_db.assert_called_once_with(pet_id)


if __name__ == '__main__':
    unittest.main()
```


----


## 追加のリクエスト

先のリクエスト・レスポンスのコンテキストを引き継いだまま、続けてリクエストを行います。

ステップは以下の通りです。

1. メッセージの更新
2. リクエストの実行およびレスポンスの取得
3. レスポンス内容の確認 (任意)
4. コードの取得

### メッセージの更新

```python
messages.append(response.choices[0].message)
```

```python
prompt = "I would like you to add test cases of transaction failure on delete_pet. You should change the test class name to be different from existing test class names."

messages.append({"role":"user","content":prompt})
```

### リクエストの実行およびレスポンスの取得

```python
response = openai.ChatCompletion.create(
  engine="gpt-4-32k-0314",
  messages = messages,
  temperature=0.7,
  max_tokens=1600,
  top_p=0.95,
  frequency_penalty=0,
  presence_penalty=0,
  stop=None)
```

### レスポンス内容の確認 (任意)

```python
print(response)
```

```json
{
  "choices": [
    {
      "finish_reason": "stop",
      "index": 0,
      "message": {
        "content": "I have added a new test class named `TestPetStoreAPITransactionFailures` with test cases for transaction failure on `delete_pet`. The existing test cases are preserved in the `TestPetStoreAPI` class.\n\n```python\nimport unittest\nfrom unittest.mock import patch\nfrom petstore_api import PetStoreAPI, Pet, Order\n\n\nclass TestPetStoreAPI(unittest.TestCase):\n    # ... (previous test cases)\n\n    # Add any new test cases for the existing class here\n\n\nclass TestPetStoreAPITransactionFailures(unittest.TestCase):\n    def setUp(self):\n        self.api = PetStoreAPI()\n        self.pets = [\n            Pet(1, \"Dog\", \"Bulldog\", \"friendly\"),\n            Pet(2, \"Cat\", \"Persian\", \"lazy\"),\n            Pet(3, \"Bird\", \"Parrot\", \"talkative\")\n        ]\n        self.orders = [\n            Order(1, 1, 1),\n            Order(2, 2, 2)\n        ]\n\n    @patch('petstore_api.PetStoreAPI._delete_pet_from_db')\n    @patch('petstore_api.PetStoreAPI._delete_order_from_db')\n    def test_delete_pet_transaction_failure(self, mock_delete_order_from_db, mock_delete_pet_from_db):\n        pet_id = 1\n        user_id = 1\n        mock_delete_order_from_db.side_effect = Exception(\"Transaction failure\")\n\n        with self.assertRaises(Exception) as context:\n            self.api.delete_pet(pet_id, user_id)\n\n        self.assertEqual(str(context.exception), \"Transaction failure\")\n        mock_delete_order_from_db.assert_called_once_with(pet_id, user_id)\n        mock_delete_pet_from_db.assert_not_called()\n\n\nif __name__ == '__main__':\n    unittest.main()\n```\n\nThe new test class `TestPetStoreAPITransactionFailures` contains a test case for transaction failure on `delete_pet`. The test case uses the `side_effect` attribute of the mock object to simulate an exception during the transaction, and the test verifies that the exception is raised and the `_delete_pet_from_db` method is not called.",
        "role": "assistant"
      }
    }
  ],
  "created": 1681350212,
  "id": "chatcmpl-*****************************",
  "model": "gpt-4-32k",
  "object": "chat.completion",
  "usage": {
    "completion_tokens": 439,
    "prompt_tokens": 3073,
    "total_tokens": 3512
  }
}
```

### コードの取得

```python
code = response.choices[0].message.content.split('```')[1].lstrip('python\n')
code_string = f'''{code}'''
print(code_string)
```

```python
import unittest
from unittest.mock import patch
from petstore_api import PetStoreAPI, Pet, Order


class TestPetStoreAPI(unittest.TestCase):
    # ... (previous test cases)

    # Add any new test cases for the existing class here


class TestPetStoreAPITransactionFailures(unittest.TestCase):
    def setUp(self):
        self.api = PetStoreAPI()
        self.pets = [
            Pet(1, "Dog", "Bulldog", "friendly"),
            Pet(2, "Cat", "Persian", "lazy"),
            Pet(3, "Bird", "Parrot", "talkative")
        ]
        self.orders = [
            Order(1, 1, 1),
            Order(2, 2, 2)
        ]

    @patch('petstore_api.PetStoreAPI._delete_pet_from_db')
    @patch('petstore_api.PetStoreAPI._delete_order_from_db')
    def test_delete_pet_transaction_failure(self, mock_delete_order_from_db, mock_delete_pet_from_db):
        pet_id = 1
        user_id = 1
        mock_delete_order_from_db.side_effect = Exception("Transaction failure")

        with self.assertRaises(Exception) as context:
            self.api.delete_pet(pet_id, user_id)

        self.assertEqual(str(context.exception), "Transaction failure")
        mock_delete_order_from_db.assert_called_once_with(pet_id, user_id)
        mock_delete_pet_from_db.assert_not_called()


if __name__ == '__main__':
    unittest.main()
```

以前のコンテキストを引き継いだまま、追加のリクエスト通りにコードを取得することができました。


----


## まとめ

Azure OpenAI Service について、 Python ライブラリを使ってテストコードを生成・追加する方法について解説しました。

プログラマブルな点、すなわち `openai.ChatCompletion.create` で多彩なパラメータが指定できたり、`messages` のリストを操作することができる点、また、少なくともリクエストに関しては再現性を持たせられる点が、 Playground に対する大きなメリットです。

また、この取り組みについて反復的な検証をする際に、Jupyter Notebook を利用して効率的に行うことができます。

実装自体は上記の通り簡単にできますので、ぜひトライしてみてください。