最近久々に近所のお祭りに行ってきました、屋台の食べ物ではりんご飴が好きな菅野です。
皆さん、普段APIのテストはどのように行っておりますか?
最近は、APIのテスト自動化を行えるようなツールやサービスも増えてきているように思いますが、当社では、OSSのテスティングフレームワークである「Karate」を用いることが多いです。
比較的簡単な構文で直感的にAPIのテストができる点がよいと思います。
しかし、いかに簡単な方法でAPIのテストが記述できるからといっても、APIの数が多いとテストを作成するのは一苦労です。
今回は、そんなKarateのテストスクリプトをChatGPTを活用して作成してみようと思います。
まず、REST-APIの仕様を定義する場合、OpenAPIを利用することが多いのではないか、と思います。
ChatGPTの開発元である「OpenAI」ではないです。自分も書いていて、紛らわしくなってきましたが、「OpenAPI」です。
OpenAPI形式のYAMLファイルからAPIの実装コードはSwaggerCodegenや、OpenAPIGenerator といった自動生成ツールを用いることで作成できます。
つまり、ChatGPTでテストコードを自動生成することができれば、API仕様からソースコード生成を生成し、そのテストを行うことがほぼ自動でできることになります。
今回の検証ではGPT-4を用います。
Karateとは?
Karateは、Gherkin(ガーキン)形式を用いて記述するAPIテストのためのフレームワークで、テストスクリプトの作成を簡単にすることができます。
APIのテストだけでなく、UIテストやGatlingと連携した負荷試験等も幅広くできるツールです。
詳細は公式のGitHubやドキュメントを参照してください。
github.com
Karate | Test Automation Made Simple.
テスト対象のAPI設計
APIのテストを作成する上で、どういったAPIがあるかをChatGPTに教えてあげる必要があります。
タスク管理アプリを想定したOpenAPIの定義を以下のように作成しました。
openapi: 3.0.0 info: title: Task Management API version: 1.0.0 description: An API for managing tasks paths: /tasks: get: summary: Get all tasks responses: '200': description: A list of tasks content: application/json: schema: type: array items: $ref: '#/components/schemas/Task' post: summary: Create a new task requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/TaskInput' responses: '201': description: Task created successfully content: application/json: schema: $ref: '#/components/schemas/Task' '400': description: Invalid input /tasks/{taskId}: get: summary: Get a specific task parameters: - name: taskId in: path required: true description: ID of the task to retrieve schema: type: string responses: '200': description: Task details content: application/json: schema: $ref: '#/components/schemas/Task' '404': description: Task not found put: summary: Update a task parameters: - name: taskId in: path required: true description: ID of the task to update schema: type: string requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/TaskInput' responses: '200': description: Task updated successfully content: application/json: schema: $ref: '#/components/schemas/Task' '400': description: Invalid input '404': description: Task not found delete: summary: Delete a task parameters: - name: taskId in: path required: true description: ID of the task to delete schema: type: string responses: '204': description: Task deleted successfully '404': description: Task not found components: schemas: Task: type: object properties: id: type: string description: The unique ID of the task title: type: string description: The title of the task description: type: string description: The description of the task status: type: string description: The status of the task enum: [pending, completed] TaskInput: type: object properties: title: type: string description: The title of the task required: true description: type: string description: The description of the task status: type: string description: The status of the task enum: [pending, completed]
Karateのテストスクリプト作成
早速KarateのテストスクリプトをChatGPTに作成してもらいましょう。
今回は、localhost:5000で動作させたAPIを対象にテスト実行するように依頼をします。
(中略)
(中略)
指定したOpenAPIのYAMLファイルからどういったAPIの種類があるかを理解したうえで、Karateのテストスクリプトを作成しているように見えますね。
YAMLファイルからは作成したタスクのIDがどのように振られるかがわからない為、実装に合わせて適宜置き換えるように記載されています。
今回のサンプルAPI実装ではシーケンシャルにIDを付与するため、sampleIdを1に置換しておきましょう。
出力してくれたfeatureファイルを修正した結果が以下になります。
# KarateのFeatureファイル開始 Feature: Task Management API # ベースURLの設定 Background: * url 'http://localhost:5000' # すべてのタスクを取得するテスト Scenario: Get all tasks Given path 'tasks' When method get Then status 200 # JSONのレスポンスがタスクの配列であることを確認 And match each response == {id: '#string', title: '#string', description: '#string', status: '##string'} # 新しいタスクを作成するテスト Scenario: Create a new task Given path 'tasks' And request {title: 'Test Task', description: 'This is a test', status: 'pending'} When method post Then status 201 # JSONのレスポンスがタスクのオブジェクトであることを確認 And match response == {id: '#string', title: 'Test Task', description: 'This is a test', status: 'pending'} # 特定のタスクを取得するテスト Scenario: Get a specific task Given path 'tasks', '1' When method get Then status 200 And match response == {id: '1', title: '#string', description: '#string', status: '##string'} # タスクを更新するテスト Scenario: Update a task Given path 'tasks', '1' And request {title: 'Updated Task', description: 'This task is updated', status: 'completed'} When method put Then status 200 And match response == {id: '1', title: 'Updated Task', description: 'This task is updated', status: 'completed'} # タスクを削除するテスト Scenario: Delete a task Given path 'tasks', '1' When method delete Then status 204
テスト対象のAPI実装
今回のテストを実行する上で、対象のopenapi.ymlの内容で動作するサンプルAPIをFlaskを用いて作成しました。
こちらを対象にKarateのテストを実行してみます。
from flask import Flask, jsonify, request, abort app = Flask(__name__) tasks = [] class Task: def __init__(self, id, title, description, status): self.id = id self.title = title self.description = description self.status = status def to_dict(self): return { "id": self.id, "title": self.title, "description": self.description, "status": self.status } @app.route("/tasks", methods=["GET"]) def get_tasks(): return jsonify([task.to_dict() for task in tasks]) @app.route("/tasks", methods=["POST"]) def create_task(): if not request.json or not "title" in request.json: abort(400) title = request.json["title"] description = request.json.get("description", "") status = request.json.get("status", "pending") id = str(len(tasks) + 1) new_task = Task(id, title, description, status) tasks.append(new_task) return jsonify(new_task.to_dict()), 201 @app.route("/tasks/<string:task_id>", methods=["GET"]) def get_task(task_id): task = next((task for task in tasks if task.id == task_id), None) if not task: abort(404) return jsonify(task.to_dict()) @app.route("/tasks/<string:task_id>", methods=["PUT"]) def update_task(task_id): task = next((task for task in tasks if task.id == task_id), None) if not task: abort(404) if not request.json: abort(400) task.title = request.json.get("title", task.title) task.description = request.json.get("description", task.description) task.status = request.json.get("status", task.status) return jsonify(task.to_dict()) @app.route("/tasks/<string:task_id>", methods=["DELETE"]) def delete_task(task_id): global tasks tasks = [task for task in tasks if task.id != task_id] return jsonify({}), 204 if __name__ == "__main__": app.run(debug=True)
テスト実行
アプリを起動したら、Karateのテスト実行してみます。
Karateのテスト実行方法には、Maven, Gradleといった、Javaビルドツールを用いるほかにも、Karate実行用のJarファイルを用いる方法もあります。
Karateのリリースページにアクセスし、Assetsのkarate-X.X.X.jar(2023年8月現在の最新は1.4.0)からダウンロードできます。
ダウンロードしたjarファイルの実行方法は以下のURLに記載されています。
https://github.com/karatelabs/karate/tree/master/karate-netty#standalone-jar
単純なテスト実行方法は以下になります。
# 1ファイルのテスト実行 java -jar karate-X.X.X.jar my-test.feature # ディレクトリに配置されているfeatureファイルのテストをすべて実行 java -jar karate-X.X.X.jar some/folder
>java -jar karate-1.4.0.jar task_management.feature 10:04:10.258 [main] INFO com.intuit.karate - Karate version: 1.4.0 10:04:10.521 [main] INFO com.intuit.karate.Suite - backed up existing 'target\karate-reports' dir to: target\karate-reports_1693357450512 10:04:11.638 [main] DEBUG com.intuit.karate - request: 1 > GET http://localhost:5000/tasks 1 > Host: localhost:5000 1 > Connection: Keep-Alive 1 > User-Agent: Apache-HttpClient/4.5.14 (Java/20.0.2) 1 > Accept-Encoding: gzip,deflate 10:04:11.689 [main] DEBUG com.intuit.karate - response time in milliseconds: 50 1 < 200 1 < Server: Werkzeug/2.3.7 Python/3.10.5 1 < Date: Wed, 30 Aug 2023 01:04:11 GMT 1 < Content-Type: application/json 1 < Content-Length: 3 1 < Connection: close [] 10:04:11.832 [main] DEBUG com.intuit.karate - request: 1 > POST http://localhost:5000/tasks 1 > Content-Type: application/json; charset=UTF-8 1 > Content-Length: 71 1 > Host: localhost:5000 1 > Connection: Keep-Alive 1 > User-Agent: Apache-HttpClient/4.5.14 (Java/20.0.2) 1 > Accept-Encoding: gzip,deflate {"title":"Test Task","description":"This is a test","status":"pending"} 10:04:11.835 [main] DEBUG com.intuit.karate - response time in milliseconds: 3 1 < 201 1 < Server: Werkzeug/2.3.7 Python/3.10.5 1 < Date: Wed, 30 Aug 2023 01:04:11 GMT 1 < Content-Type: application/json 1 < Content-Length: 98 1 < Connection: close { "description": "This is a test", "id": "1", "status": "pending", "title": "Test Task" } 10:04:11.846 [main] DEBUG com.intuit.karate - request: 1 > GET http://localhost:5000/tasks/1 1 > Host: localhost:5000 1 > Connection: Keep-Alive 1 > User-Agent: Apache-HttpClient/4.5.14 (Java/20.0.2) 1 > Accept-Encoding: gzip,deflate 10:04:11.851 [main] DEBUG com.intuit.karate - response time in milliseconds: 5 1 < 200 1 < Server: Werkzeug/2.3.7 Python/3.10.5 1 < Date: Wed, 30 Aug 2023 01:04:11 GMT 1 < Content-Type: application/json 1 < Content-Length: 98 1 < Connection: close { "description": "This is a test", "id": "1", "status": "pending", "title": "Test Task" } 10:04:11.857 [main] DEBUG com.intuit.karate - request: 1 > PUT http://localhost:5000/tasks/1 1 > Content-Type: application/json; charset=UTF-8 1 > Content-Length: 82 1 > Host: localhost:5000 1 > Connection: Keep-Alive 1 > User-Agent: Apache-HttpClient/4.5.14 (Java/20.0.2) 1 > Accept-Encoding: gzip,deflate {"title":"Updated Task","description":"This task is updated","status":"completed"} 10:04:11.861 [main] DEBUG com.intuit.karate - response time in milliseconds: 3 1 < 200 1 < Server: Werkzeug/2.3.7 Python/3.10.5 1 < Date: Wed, 30 Aug 2023 01:04:11 GMT 1 < Content-Type: application/json 1 < Content-Length: 109 1 < Connection: close { "description": "This task is updated", "id": "1", "status": "completed", "title": "Updated Task" } 10:04:11.869 [main] DEBUG com.intuit.karate - request: 1 > DELETE http://localhost:5000/tasks/1 1 > Host: localhost:5000 1 > Connection: Keep-Alive 1 > User-Agent: Apache-HttpClient/4.5.14 (Java/20.0.2) 1 > Accept-Encoding: gzip,deflate 10:04:11.875 [main] DEBUG com.intuit.karate - response time in milliseconds: 5 1 < 204 1 < Server: Werkzeug/2.3.7 Python/3.10.5 1 < Date: Wed, 30 Aug 2023 01:04:11 GMT 1 < Content-Type: application/json 1 < Connection: close --------------------------------------------------------- feature: task_management.feature scenarios: 5 | passed: 5 | failed: 0 | time: 0.5805 --------------------------------------------------------- 10:04:12.574 [main] INFO com.intuit.karate.Suite - <<pass>> feature 1 of 1 (0 remaining) task_management.feature Karate version: 1.4.0 ====================================================== elapsed: 2.15 | threads: 1 | thread time: 0.58 features: 1 | skipped: 0 | efficiency: 0.27 scenarios: 5 | passed: 5 | failed: 0 ====================================================== HTML report: (paste into browser to view) | Karate version: 1.4.0 file:///path/to/featuresfile//target/karate-reports/karate-summary.html ===================================================================
無事テストが通りました。
テスト実行結果のレポートも出力されています。
Karateは、このようにテストの実行結果もわかりやすく出力されるのがいいですね。
まとめ
今回は、KarateのテストをOpenAI形式のYAMLファイルから自動で生成してみました。
簡単なサンプルアプリではありますが、記載されている全APIの正常系テストを作成して動かすことができました。
OpenAPIの定義から、テストスクリプトの作成も自動で生成できるのは、開発効率がとても上がると感じました。
これで、REST-APIのテスト自動化も、楽に実現ができそうです。
今後もChatGPTの可能性を探っていこうと思います。
Acroquest Technologyでは、キャリア採用を行っています。少しでも上記に興味を持たれた方は、是非以下のページをご覧ください。 www.wantedly.com
- ディープラーニング等を使った自然言語/画像/音声/動画解析の研究開発
- Elasticsearch等を使ったデータ収集/分析/可視化
- マイクロサービス、DevOps、最新のOSSやクラウドサービスを利用する開発プロジェクト
- 書籍・雑誌等の執筆や、社内外での技術の発信・共有によるエンジニアとしての成長