Open21

APIテストやっていき

もっくま(Mistletoe)もっくま(Mistletoe)

Sonnet3.5

REST APIの自動テストのための主要なツールをご紹介します:

Postman


最も広く使われているAPIテストツール
GUIベースで操作が直感的
テストの作成、実行、自動化が可能
Newman(CLIツール)でCI/CD環境での実行も可能


JUnit/TestNG + RestAssured


Javaベースのテストフレームワーク
読みやすい文法でAPIテストを記述可能
CI/CDとの統合が容易


PyTest + Requests


Pythonでのテスト自動化に最適
シンプルで扱いやすい
豊富なアサーション機能


Karate


APIテスト専用のDSL
BDD形式でテストを記述可能
Javaベースだが、コーディング不要


SoapUI


オープンソースのAPI テストツール
SOAPとRESTの両方に対応
負荷テストも可能


Jest + Supertest


Node.jsアプリケーションのテストに最適
JavaScriptベース
モックの作成が容易


k6


パフォーマンステストに特化
JavaScriptでテストを記述
スケーラブルな負荷テストが可能


Cypress


モダンなE2EテストフレームワークでAPIテストも可能
リアルタイムのテスト実行結果確認
デバッグが容易

各ツールの選択基準として考慮すべき点:

プロジェクトで使用している主要な言語
チームの技術スタック
必要な機能(認証、暗号化、パフォーマンステストなど)
CI/CDパイプラインとの統合のしやすさ

これらのツールについて、より詳しい情報や具体的な使用例を知りたい場合は、お申し付けください。
もっくま(Mistletoe)もっくま(Mistletoe)

ChatGPT

APIの自動テストを実現するためのツールを以下にリストアップします。

Postman

API開発とテストのための人気ツール。
スクリプトを使用した自動テストや、Newmanを使ったコマンドラインでのテスト実行が可能。
SoapUI

SOAPおよびREST APIのテストに特化したツール。
オープンソース版と有料版(SoapUI Pro)があります。
Katalon Studio

API、Web、モバイルの自動テストが可能な統合環境。
スクリプト不要でテストケースを作成可能。
JMeter

主にパフォーマンステスト向けだが、APIの機能テストにも利用可能。
スクリプトなしでテスト計画を作成可能。
Rest Assured

JavaでRESTfulなAPIをテストするためのライブラリ。
BDDスタイルでの記述が可能。
Swagger/OpenAPI

APIの設計、構築、ドキュメンテーションをサポート。
Swagger InspectorでAPIのテストが可能。
Karate DSL

CucumberベースのAPIテストフレームワーク。
シンプルな構文でAPIテストを記述可能。
API Fortress

継続的なAPIテストのためのプラットフォーム。
視覚的なテスト作成と自動リグレッションテストを提供。
Pact

マイクロサービス間の契約テストを行うためのツール。
コンシューマ主導の契約テストをサポート。
Tavern

Pytestプラグインとして動作するAPIテストツール。
YAMLファイルでテストを定義。
Pytest + Requests

PythonでAPIテストを行うための組み合わせ。
柔軟なテストケースの作成が可能。
Gatling

スケーラブルなパフォーマンステストツール。
APIの負荷テストに適用可能。
Newman

Postmanコレクションをコマンドラインで実行するためのツール。
CI/CDパイプラインに統合可能。
Runscope

APIのテストとモニタリングを行うSaaSツール。
異常検知と通知機能を提供。
Tricentis Tosca

エンタープライズ向けの総合テストツール。
APIテストもサポート。
Apigee

API管理とテストを行うためのプラットフォーム。
Google Cloudが提供。
Citrus Framework

メッセージングプロトコルやデータフォーマットの統合テストを行うフレームワーク。
Assertible

APIの信頼性テストに焦点を当てたツール。
デプロイ後の自動テストをサポート。
Insomnia

APIクライアント兼テストツール。
GraphQLやgRPCもサポート。
TestNG

Java向けのテストフレームワーク。
APIテストに利用可能。
Mocha + Chai

Node.js環境でのAPIテストに使用されるJavaScriptテストフレームワーク。
Locust

Python製の負荷テストツール。
スクリプトでAPIの負荷シナリオを記述可能。
Airborne

Ruby用のAPIテストフレームワーク。
RSpecスタイルでテストを記述。
Dredd

APIのドキュメントと実装の整合性をテストするツール。
OpenAPI仕様に基づく。
Cypress

フロントエンドテスト向けだが、APIテストにも利用可能。
JavaScriptでテストを記述。
これらのツールは、APIの自動テストを効率的に行うための機能を提供しています。プロジェクトの要件や技術スタックに合わせて最適なものを選択してください。
もっくま(Mistletoe)もっくま(Mistletoe)

まぁいわゆるユニットテスト・フレームワークでもできるしE2Eテストフレームワークでもできるし、ツール自体は何でもいいんだろうな。達成したい目的に対しては

もっくま(Mistletoe)もっくま(Mistletoe)

Postmanにあるサンプルのテストコードはこんな感じ

pm.sendRequest("https://postman-echo.com/get", function (err, response) {
    console.log(response.json());
});
pm.test("Status code is 200", function () {
    pm.response.to.have.status(200);
});
pm.test("Your test name", function () {
    var jsonData = pm.response.json();
    pm.expect(jsonData.value).to.eql(100);
});
pm.test("Body is correct", function () {
    pm.response.to.have.body("response_body_string");
});
pm.test("Content-Type is present", function () {
    pm.response.to.have.header("Content-Type");
});
pm.test("Response time is less than 200ms", function () {
    pm.expect(pm.response.responseTime).to.be.below(200);
});

もっくま(Mistletoe)もっくま(Mistletoe)

0からこのAPIリクエスト/レスポンスのテストができる状態に持っていかなくてもすぐできるのはまぁ楽でいいかも

もっくま(Mistletoe)もっくま(Mistletoe)

リグレッションテストのサンプルもテンプレートがあったので作って動かしてみた。

READMEっぽいのができる

いくつかのテストケースが整備されていて

各リクエストの設定とアサーションテストがある感じ。共通利用するような変数定義とかもやってる。
認証周りの共通処理とかも共通した形で利用できた気がする。この辺のシナリオの組み合わせとかもやろうと思えばできそう。

もっくま(Mistletoe)もっくま(Mistletoe)

さて、これをCICDとかで実行したり、開発資産的にコードで運用したりすることはできるんかな?

もっくま(Mistletoe)もっくま(Mistletoe)

知らなかったけどユーザーログインしているのでPostmanのSaaS側にはローカルのクライアントで作業していた内容は同期されている感じなんですね。

このプロジェクト単位でCLI経由で実行するというのは素直にできそう。

やけどもう少しSaaSとは独立したものやとおもってたな、普通のテストフレームワークよろしく。
そんなんできないのかな

もっくま(Mistletoe)もっくま(Mistletoe)

お、なんかexport&実行ができるらしい

https://qiita.com/developer-kikikaikai/items/74cedc67643ca93d2e0b#作ったテストをjsonファイルに出力

exportするとこんな感じ

{
	"info": {
		"_postman_id": "543e4e65-0498-4277-9f41-00ca3422381c",
		"name": "Regression testing",
		"description": "# 🧪 Get started here\n\nThis template helps ensure your API continues to work as expected after updates or changes. It contains sample requests and [scripts](https://learning.postman.com/docs/writing-scripts/intro-to-scripts/) for a series of tests that validate different aspects of an API, including:\n\n1. **Status code verification:** We'll check if the API returns the expected status codes for various requests, ensuring reliability and proper error handling.\n    \n2. **JSON schema verification:** We'll make sure the JSON response conforms to a specified JSON Schema, ensuring API contracts are honored.\n    \n3. **Response time verification:** We'll verify that the API's response time is within the acceptable range, providing a smooth user experience and optimizing performance.\n    \n4. **Response body verification:** We'll validate the structure and content of the response body for accuracy, ensuring the integrity of the data and preventing potential issues downstream.\n    \n5. **Header verification:** We'll confirm the presence and correct values of specific headers in the response, enabling secure and well-structured communication between your app and its users.\n    \n\n## 🔖 **How to use this template**\n\n**Step 1:** Send the sample requests and view the response and test results.\n\n**Step 2:** Replace the sample request URLs with your desired API endpoints.\n\n**Step 3:** Customize the tests in the \"Scripts\" tab if needed. Don't forget to save your changes.\n\n**Step 4:** [Run the collection](https://learning.postman.com/docs/collections/running-collections/intro-to-collection-runs/) by selecting \"Run\".\n\n<img src=\"https://content.pstmn.io/666cfa65-6e00-4308-871b-f831128bc0ac/Y29sbGVjdGlvbi1ydW5uZXItYnV0dG9uLmpwZWc=\" width=\"246\" height=\"102\">\n\n## 💡Related templates\n\n[Integration testing](https://go.postman.co/redirect/workspace?type=personal&collectionTemplateId=fe506090-ca91-4340-bea9-82d2c3d2bb9a&sourceTemplateId=7ab0f80e-a964-46ee-b948-4a4e3a85809c)  \n[Performance testing](https://go.postman.co/redirect/workspace?type=personal&collectionTemplateId=3302192d-87dd-4612-b381-61894de21203&sourceTemplateId=7ab0f80e-a964-46ee-b948-4a4e3a85809c)  \n[Mock data generation](https://go.postman.co/redirect/workspace?type=personal&collectionTemplateId=da8ceebc-d4da-478c-ab8a-37634faefc74&sourceTemplateId=7ab0f80e-a964-46ee-b948-4a4e3a85809c)  \n[Common API vulnerabilities check](https://go.postman.co/redirect/workspace?type=personal&collectionTemplateId=e1edbae8-6429-453d-890e-00385eea1811&sourceTemplateId=7ab0f80e-a964-46ee-b948-4a4e3a85809c)",
		"schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json",
		"_exporter_id": "13354141"
	},
	"item": [
		{
			"name": "Status code verification",
			"event": [
				{
					"listen": "test",
					"script": {
						"exec": [
							"pm.test(\"Status code is 200\", function () {",
							"    pm.response.to.have.status(200);",
							"});",
							""
						],
						"type": "text/javascript"
					}
				}
			],
			"request": {
				"method": "GET",
				"header": [],
				"url": {
					"raw": "{{baseUrl}}/get",
					"host": [
						"{{baseUrl}}"
					],
					"path": [
						"get"
					]
				},
				"description": "The test on this request asserts that the reponse has a \"200 OK\" status.\n\nYou can check for any HTTP status code using the same mechanism, for example `pm.response.to.have.status(403)` if your API should return an \"Unauthorized\" response."
			},
			"response": []
		},
		{
			"name": "JSON schema response validation",
			"event": [
				{
					"listen": "test",
					"script": {
						"exec": [
							"// Describe the expected response shape with JSON Schema",
							"// https://json-schema.org/",
							"const expectedSchema = {",
							"  \"type\": \"object\",",
							"  \"properties\": {",
							"    \"data\": {",
							"      \"type\": \"array\",",
							"      \"items\": {",
							"        \"type\": \"object\",",
							"        \"properties\": {",
							"          \"title\": {",
							"            \"type\": \"string\"",
							"          },",
							"          \"author\": {",
							"            \"type\": \"string\"",
							"          },",
							"          \"published\": {",
							"            \"type\": \"integer\"",
							"          }",
							"        },",
							"        \"required\": [",
							"          \"title\",",
							"          \"author\",",
							"          \"published\"",
							"        ]",
							"      }",
							"    }",
							"  }",
							"};",
							"",
							"// Test that the response matches the expected schema using built-in schema verification",
							"pm.test('response matches JSON schema', () => {",
							"    pm.response.to.have.jsonSchema(expectedSchema);",
							"});"
						],
						"type": "text/javascript"
					}
				}
			],
			"request": {
				"method": "POST",
				"header": [],
				"body": {
					"mode": "raw",
					"raw": "[{\n    \"title\": \"Sapiens\",\n    \"author\": \"Yuval Noah Harari\",\n    \"published\": 2011\n}, {\n    \"title\": \"Moneyball\",\n    \"author\": \"Michael Lewis\",\n    \"published\": 2010\n}, {\n    \"title\": \"Women & Power\",\n    \"author\": \"Mary Beard\",\n    \"published\": 2017\n}]",
					"options": {
						"raw": {
							"language": "json"
						}
					}
				},
				"url": {
					"raw": "{{baseUrl}}/post",
					"host": [
						"{{baseUrl}}"
					],
					"path": [
						"post"
					]
				},
				"description": "This request has a test using Postman's built-in support for JSON Schema verification to ensure that the response data matches the expected shape.\n\nSchema validation can be used to verify your API returns data in the right format, without worrying about the exact values. This might be helpful when testing various inputs for a consistent output format.\n\nLearn more about JSON Schema at [https://json-schema.org](https://json-schema.org/.)."
			},
			"response": []
		},
		{
			"name": "Custom response validation",
			"event": [
				{
					"listen": "test",
					"script": {
						"exec": [
							"const getData = () => pm.response.json().data;",
							"",
							"pm.test(\"Returns a book object\", () => {",
							"    const data = getData();",
							"",
							"    // Check the expected properties exist",
							"    pm.expect(data).to.have.property(\"title\");",
							"    pm.expect(data).to.have.property(\"author\");",
							"    pm.expect(data).to.have.property(\"published\");",
							"",
							"    // Check the values have the expected types",
							"    pm.expect(data.title).to.be.a('string');",
							"    pm.expect(data.author).to.be.a('string');",
							"    pm.expect(data.published).to.be.a('number');",
							"});",
							"",
							"pm.test(\"Return correct details for 'Sapiens' book\", () => {",
							"    const data = getData();",
							"",
							"    // Check expected values are present",
							"    pm.expect(data.title).to.equal(\"Sapiens\");",
							"    pm.expect(data.author).to.equal(\"Yuval Noah Harari\");",
							"    pm.expect(data.published).to.equal(2011);",
							"});"
						],
						"type": "text/javascript"
					}
				}
			],
			"request": {
				"method": "POST",
				"header": [],
				"body": {
					"mode": "raw",
					"raw": "{\n    \"title\": \"Sapiens\",\n    \"author\": \"Yuval Noah Harari\",\n    \"published\": 2011\n}",
					"options": {
						"raw": {
							"language": "json"
						}
					}
				},
				"url": {
					"raw": "{{baseUrl}}/post",
					"host": [
						"{{baseUrl}}"
					],
					"path": [
						"post"
					]
				},
				"description": "This request uses tests to verify both the structure (existence and type of individual properties) and exact values of the response data.\n\nUse `pm.expect(subject)` to wrap test subjects, and [Chai](https://www.chaijs.com/api/bdd/) for assertions."
			},
			"response": []
		},
		{
			"name": "Response time verification",
			"event": [
				{
					"listen": "test",
					"script": {
						"exec": [
							"pm.test(\"Verify response time is less than 5 seconds\", function () {",
							"    const fiveSecondsMs = 5_000; // 5 seconds in milliseconds",
							"    pm.expect(pm.response.responseTime).to.be.below(fiveSecondsMs);",
							"});",
							""
						],
						"type": "text/javascript"
					}
				}
			],
			"protocolProfileBehavior": {
				"disableBodyPruning": true
			},
			"request": {
				"method": "GET",
				"header": [],
				"body": {
					"mode": "raw",
					"raw": "{\n    \"foo\": \"bar\"\n}\n",
					"options": {
						"raw": {
							"language": "json"
						}
					}
				},
				"url": {
					"raw": "{{baseUrl}}/get",
					"host": [
						"{{baseUrl}}"
					],
					"path": [
						"get"
					]
				},
				"description": "This request has a test asserting that a response is received within a specific time limit (in this case 5 seconds). This is useful for ensuring consistent API performance.\n\nYou can access details of the response, such as response time, on the `pm.response` object. See the [response object documentation](https://learning.postman.com/docs/writing-scripts/script-references/postman-sandbox-api-reference/#scripting-with-response-data) for other properties available."
			},
			"response": []
		},
		{
			"name": "Header verification",
			"event": [
				{
					"listen": "test",
					"script": {
						"exec": [
							"pm.test(\"Verify response headers have expected values\", function () {",
							"    pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(\"application/json\");",
							"    pm.expect(pm.response.headers.get(\"Content-Length\")).to.exist;",
							"});",
							""
						],
						"type": "text/javascript"
					}
				}
			],
			"protocolProfileBehavior": {
				"disableBodyPruning": true
			},
			"request": {
				"method": "GET",
				"header": [],
				"body": {
					"mode": "raw",
					"raw": "",
					"options": {
						"raw": {
							"language": "json"
						}
					}
				},
				"url": {
					"raw": "{{baseUrl}}/get",
					"host": [
						"{{baseUrl}}"
					],
					"path": [
						"get"
					]
				},
				"description": "This request has a test asserting that certain response headers are present or have an expected value.\n\nYou can access details of the response, such as headers, on the `pm.response` object. See the [response object documentation](https://learning.postman.com/docs/writing-scripts/script-references/postman-sandbox-api-reference/#scripting-with-response-data) for other properties available."
			},
			"response": []
		}
	],
	"event": [
		{
			"listen": "prerequest",
			"script": {
				"type": "text/javascript",
				"exec": [
					""
				]
			}
		},
		{
			"listen": "test",
			"script": {
				"type": "text/javascript",
				"exec": [
					""
				]
			}
		}
	],
	"variable": [
		{
			"key": "baseUrl",
			"value": "https://postman-echo.com",
			"type": "string"
		}
	]
}
もっくま(Mistletoe)もっくま(Mistletoe)

export されたファイルを npx newman run postmansample.json で実行するとこんな感じ

npx newman run postmansample.json
Need to install the following packages:
newman@6.2.1
Ok to proceed? (y) y
npm WARN deprecated har-validator@5.1.5: this library is no longer supported
npm WARN deprecated uuid@3.4.0: Please upgrade  to version 7 or higher.  Older versions may use Math.random() in certain circumstances, which is known to be problematic.  See https://v8.dev/blog/math-random for details.
npm WARN deprecated @faker-js/faker@5.5.3: Please update to a newer version.
newman

Regression testing

→ Status code verification
  GET https://postman-echo.com/get [200 OK, 855B, 1171ms]
  ✓  Status code is 200

→ JSON schema response validation
  POST https://postman-echo.com/post [200 OK, 1.69kB, 188ms]
  ✓  response matches JSON schema

→ Custom response validation
  POST https://postman-echo.com/post [200 OK, 1.26kB, 185ms]
  ✓  Returns a book object
  ✓  Return correct details for 'Sapiens' book

→ Response time verification
  GET https://postman-echo.com/get [200 OK, 1.05kB, 187ms]
  ✓  Verify response time is less than 5 seconds

→ Header verification
  GET https://postman-echo.com/get [200 OK, 835B, 742ms]
  ✓  Verify response headers have expected values

┌─────────────────────────┬─────────────────────┬─────────────────────┐
│                         │            executed │              failed │
├─────────────────────────┼─────────────────────┼─────────────────────┤
│              iterations │                   1 │                   0 │
├─────────────────────────┼─────────────────────┼─────────────────────┤
│                requests │                   5 │                   0 │
├─────────────────────────┼─────────────────────┼─────────────────────┤
│            test-scripts │                  10 │                   0 │
├─────────────────────────┼─────────────────────┼─────────────────────┤
│      prerequest-scripts │                   5 │                   0 │
├─────────────────────────┼─────────────────────┼─────────────────────┤
│              assertions │                   6 │                   0 │
├─────────────────────────┴─────────────────────┴─────────────────────┤
│ total run duration: 2.6s                                            │
├─────────────────────────────────────────────────────────────────────┤
│ total data received: 4.12kB (approx)                                │
├─────────────────────────────────────────────────────────────────────┤
│ average response time: 494ms [min: 185ms, max: 1171ms, s.d.: 400ms] │
└─────────────────────────────────────────────────────────────────────┘
もっくま(Mistletoe)もっくま(Mistletoe)

これ、実行できることは分かったけど、実際の運用する場合はどうするんだろう。
jsonの定義ファイルをimportしてpostman上で編集する感じにするのかな。

jsonの定義ファイルが見るの難しいので、このjsonファイルをコードレビューするくらいなら、テストフレームワークでHTTPリクエストできるテストを実装した方がDeveloperフレンドリーな気がする。

それか素直にPostmanのSaaS利用するのが順当ですね。
もちろんちゃんと使おうとすると有料です。
https://www.postman.com/pricing/