リソースを端から端までテストする

作成、取得、更新、削除、そしてそれぞれが本当に起きたかの確認。

ほとんどの API は、本、注文、ユーザー、請求書といったリソースを中心に作られています。各リソースには、たいてい作成、取得、 変更、削除の方法があります。テストの世界ではこれを CRUD(create、read、update、delete)と呼び、CRUD テストは 1 つのレコードを生涯にわたって追いかけます。ほかのほとんどの機能がこれに依存しているので、新しいリソースで最初に テストすべきことです。

このレッスンでは /api/Booksサインインして表示 を使います。すべての操作に対応しており、リファレンスページにフィールドの一覧があります。メソッドについては HTTP メソッド ですでに学びました。ここでの問いは別のものです。それぞれの操作が言ったとおりのことをしたと、どう証明するのでしょうか。

レスポンスは主張であって、証拠ではない

サーバーが POST に 201 とレコードのコピーで応えるとき、それは保存したと自分が思っているものを伝えているにすぎません。その コピーはたいてい、保存しようとしていたのと同じオブジェクトから作られるので、保存が失敗していても、データベースへの途中で フィールドが抜け落ちていても、完璧に見えることがあります。何が保存されたかを知る唯一の方法は、別のリクエストでもう一度 尋ねることです。

そこで CRUD テストでは、何かをすることと、新たに読み直して確かめることを交互に行います。

  1. レコードを作る。レスポンスを確かめる。
  2. ID で読み直す。すべてのフィールドを送ったものと比べる。
  3. 一覧のエンドポイントで見つける。
  4. PUT で置き換える。読み直す。
  5. PATCH で 1 つのフィールドを変える。読み直す。
  6. 削除する。読み直して 404 を期待する。
  7. もう一度削除して、どうなるかを見る。

作成

Terminal
curl -i -X POST https://api.testingapis.com/api/Books \
  -H "Content-Type: application/json" \
  -d '{
    "title": "Testing Web APIs",
    "author": "Mark Winteringham",
    "isbn": "9781617299537",
    "genre": "testing",
    "publishedYear": 2022,
    "pages": 264,
    "language": "English",
    "price": 49.99,
    "rating": 4.6,
    "available": true,
    "tags": ["api", "testing"]
  }'
この例はサインインしたメンバー向けのエンドポイントを使います。サインイン無料アカウントを作成
レスポンス
HTTP/1.1 201 Created
Content-Type: application/json; charset=utf-8

{
  "message": "Book created successfully",
  "data": {
    "id": 26,
    "title": "Testing Web APIs",
    "author": "Mark Winteringham",
    "isbn": "9781617299537",
    "genre": "testing",
    "publishedYear": 2022,
    "pages": 264,
    "language": "English",
    "price": 49.99,
    "rating": 4.6,
    "available": true,
    "tags": ["api", "testing"]
  }
}

このレスポンスで確かめることを、おおよそ重要な順に挙げます。

  • ステータスが 200 ではなく 201 であること。どちらも成功ですが、201 は新しいリソースができたことを示し、クライアントは それで処理を分けることがあります。
  • id があり、数値(またはドキュメントが約束する型)であること。保存しておきましょう。この後のすべての手順で必要です。
  • 送ったすべてのフィールドが同じ値で返ってくること。退屈に見えるものも含めて 1 つずつ比べます。49.99 の価格が 49.9899999 や "49.99" で返ってきたらバグです。
  • 新しいレコードを指す Location ヘッダーがあるかどうか。多くの REST API は 201 と一緒に送りますが、この API は送りません。これは欠陥というより設計上の選択ですが、自分の API がどちらなのかを知り、テストに書いておくべきです。

では、ボディに小さな間違いがあるとどうなるかを見てみましょう。フィールド名は publishedYear ですが、ここでは year として送っています。

Terminal
curl -X POST https://api.testingapis.com/api/Books \
  -H "Content-Type: application/json" \
  -d '{"title": "The Test Book", "author": "Ada Tester", "year": 2026}'
この例はサインインしたメンバー向けのエンドポイントを使います。サインイン無料アカウントを作成
レスポンス(201 Created)
{
  "message": "Book created successfully",
  "data": {
    "id": 26,
    "title": "The Test Book",
    "author": "Ada Tester",
    "isbn": "",
    "genre": "",
    "publishedYear": 0,
    "pages": 0,
    "language": "",
    "price": 0,
    "rating": 0,
    "available": false,
    "tags": []
  }
}

サーバーはリクエストを受け付け、year を黙って無視し、年として 0 を保存しました。ステータスコードには何かがおかしいことを示すものは何もありません。保存されたレコードを、送るつもりだった ものと比べてはじめて気づけます。黙って無視されるフィールドは、「データが間違っているのに誰も理由がわからない」たぐいの バグのいちばんよくある原因の 1 つです。未知のフィールドを拒否する API もあり、それは次のレッスンで扱います。

サーバーが埋めるフィールド

自分では送らないフィールドもあります。ID と、多くのリソースでは createdAt や updatedAt のようなタイムスタンプです。実行のたびに変わるので、固定の値とは比べられません。代わりにその性質をテストします。

  • ID があり、型が正しく、以前に作った ID と違うこと。
  • createdAt がドキュメントどおりの形式の有効な日付で、リクエストを送った時刻から数秒以内にあること。自分のマシンとサーバーの時計の ずれも考慮します。
  • 更新の後、updatedAt は変わり、createdAt は変わらないこと。
  • ボディに自分で id を入れても、それを選べないこと。Books のドキュメントには、ボディの ID は無視されるとあります。"id": 1 で試し、その後で本 1 に何も変化がないことを確かめてください。

読み直す

受け取った ID でレコードを取得し、ボディ全体を送ったものと比べます。次に一覧を確かめます。この API ではタイトルで検索できます。

GET/api/Books?q=web%20apis
このリクエストはサインインしたメンバー向けのエンドポイントを使います。サインイン無料アカウントを作成

結果に自分の本がなければ、作成のレスポンスが嘘をついたか、一覧が単一レコードのエンドポイントとは別の場所(キャッシュ、 検索インデックス、読み取りレプリカ)から読んでいるかのどちらかです。どれも実際のシステムで起きることです。一覧を確かめると、 もっと目立たない問題も見つかります。レコードは保存されているのに一覧から除外されている、たとえば既定のフィルターが空の フィールドを持つレコードを隠している、といったものです。

PUT と PATCH で更新する

PUT はレコード全体を置き換えます。省いたものは、たいてい空か既定の値にリセットされます。クライアント側でもサーバー側でも これはしょっちゅう忘れられるので、意図してテストしてください。ここでは、3 つのフィールドだけで本を更新しています。

Terminal
curl -X PUT https://api.testingapis.com/api/Books/27 \
  -H "Content-Type: application/json" \
  -d '{"title": "Lessons Learned in Software Testing", "author": "Cem Kaner", "price": 39.0}'
この例はサインインしたメンバー向けのエンドポイントを使います。サインイン無料アカウントを作成
レスポンス(200 OK)
{
  "message": "Book updated successfully",
  "data": {
    "id": 27,
    "title": "Lessons Learned in Software Testing",
    "author": "Cem Kaner",
    "isbn": "",
    "genre": "",
    "publishedYear": 0,
    "pages": 0,
    "language": "",
    "price": 39.0,
    "rating": 0,
    "available": false,
    "tags": []
  }
}

ISBN、ページ数、年、タグが消えています。これは PUT として正しい振る舞いです。サーバーが古い値を残していたら PATCH のように振る舞っていることになり、PUT でフィールドを空にしようとするクライアントは決してそれができません。同じリクエストでは ボディに "id": 1 も送っていましたが、レコードの ID は 27 のままで、別の GET で本 1 が変わっていないことも確かめました。

必須フィールドが欠けた PUT は、当然ながら 400 で失敗します。{"price": 35} だけを送ると Missing required field: title が返ります。

PATCH は送ったフィールドだけを変更します。この API は JSON merge patch を使い、レスポンスには変更したものと無視したものが並びます。

Terminal
curl -X PATCH https://api.testingapis.com/api/Books/26 \
  -H "Content-Type: application/merge-patch+json" \
  -d '{"price": 35, "available": false, "colour": "blue", "id": 999}'
この例はサインインしたメンバー向けのエンドポイントを使います。サインイン無料アカウントを作成
レスポンス(200 OK)
{
  "message": "Book patched successfully",
  "changed": ["price", "available"],
  "ignored": ["colour", "id"],
  "data": {
    "id": 26,
    "title": "Testing Web APIs",
    "author": "Mark Winteringham",
    "isbn": "9781617299537",
    "genre": "testing",
    "publishedYear": 2022,
    "pages": 264,
    "language": "English",
    "price": 35,
    "rating": 4.6,
    "available": false,
    "tags": ["api", "testing"]
  }
}

名前の挙がった 2 つのフィールドが変わったこと、ほかのすべてのフィールドが元の値のままであること、無視された id が本当に無視されたことを確かめます。それから新たに GET して、すべてをもう一度確かめます。レスポンスは更新されたのに保存された レコードは更新されていない PATCH はまれですが、起きたときにはまさに本番まで届いてしまう種類のバグです。ページを再読み込み するまで、クライアントには新しい値が表示されているからです。

削除し、もう一度削除する

ターミナル
$ curl -i -X DELETE https://api.testingapis.com/api/Books/26
HTTP/1.1 200 OK

{"message":"Book deleted successfully"}

$ curl -i https://api.testingapis.com/api/Books/26
HTTP/1.1 404 Not Found

{"status":404,"error":"Not Found","message":"Book with ID 26 does not exist."}

$ curl -i -X DELETE https://api.testingapis.com/api/Books/26
HTTP/1.1 404 Not Found

{"status":404,"error":"Not Found","message":"Book with ID 26 does not exist."}
この例はサインインしたメンバー向けのエンドポイントを使います。サインイン無料アカウントを作成

削除の後、GET は 404 を返さなければなりません。レコードが一覧から消えていること、件数が減ったことも確かめます(初期データでは /api/Books/countサインインして表示 は {"count": 25} を返します)。

2 回目の DELETE は API によって扱いが分かれるところです。この API は 404 を返します。最終的な状態(そのレコードはない)は 同じだという理由で、削除のたびに 204 や 200 を返す API もあります。どちらにも理があります。探すべきなのは 500 や、存在しなかったものを削除したと言い張る 200 です。削除した ID に PUT と PATCH も送ってみてください。ここでは PATCH は 404 になりますが、ボディが不完全な PUT は、レコードが存在しないことにサーバーが気づく前に、欠けたフィールドのせいで 400 になります。2 つのエラーが当てはまるときにどちらが優先されるかは、ドキュメントに一文あってしかるべきですが、たいていは 書かれていません。

自分のデータを作る

レコード 1 はいつもあるので、それに対してテストしたくなります。やめておきましょう。共有サーバーでは誰でも本 1 を変更したり削除したりでき、それを編集するテストは、それを読むほかのすべてのテストを壊します。各テストは必要なレコードを 自分で作り、受け取った ID だけを使い、最後に削除するべきです。

この API はその理由を実感するのにぴったりです。データをメモリに持ち、使う人全員で共有しているからです。このレッスンを 書いている間にも、こちらの削除と次の作成の間のほんの数秒で、別のユーザーが ID 26 の本を作りました。「次の本は 26 になる」と思い込んでいたテストは、テスト対象のコードとは何の関係もない理由で失敗していたでしょう。

同じ実験からもう 1 つわかったことがあります。本 26 を削除した後、次に作った本はまた ID 26 になりました。このサーバーは次の空き番号を割り当てるので、ID が再利用されるのです。削除の前に別のシステムが「本 26」を記録していたら、それは今では別の本を指しています。これがバグかどうかは製品次第ですが、指摘する価値のある発見です。 ほとんどのデータベースが ID を再利用しないのは、このためです。

一括処理のエンドポイント

多くの API では、1 回のリクエストで複数のレコードを作成または削除できます。興味深いのは、一部の項目が不正なときにどうなるかです。

Terminal
curl -X POST https://api.testingapis.com/api/Books/bulk \
  -H "Content-Type: application/json" \
  -d '[
    {"title": "Bulk A", "author": "Tester"},
    {"title": "Bulk B", "author": "Tester"},
    {"title": "", "author": "Tester"}
  ]'

{"status":400,"error":"Bad Request","message":"Item [2]: Missing required field: title"}
この例はサインインしたメンバー向けのエンドポイントを使います。サインイン無料アカウントを作成

3 番目の項目はタイトルが空なので、リクエスト全体が失敗し、エラーは項目をインデックスで示しています。Books のドキュメントにはこの場合は何も保存されないとあり、それを確かめるべきです。失敗したリクエストの後に「Bulk A」が存在しない ことを確認してください。最初の 2 つを保存してから失敗する API では、クライアントは何を再試行すればよいかわかりません。

一括削除は、見つかった ID と見つからなかった ID を報告します。

Terminal
curl -X DELETE "https://api.testingapis.com/api/Books/bulk?ids=27,28,5000"

{"message":"2 book record(s) deleted","deleted":[27,28],"notFound":[5000]}
この例はサインインしたメンバー向けのエンドポイントを使います。サインイン無料アカウントを作成
一括処理のケース確かめること
すべての項目が有効すべての項目が保存され、ID は重複せず、件数は送った数だけ増える。
不正な項目が 1 つ何も保存されないか、どの項目が保存されたかをレスポンスが正確に示す。
空の配列何も作らない 201 ではなく、はっきりした 400。
上限(ここでは 100)を超える上限を示す 400。
一括削除での重複した ID各レコードは 1 回だけ削除され、2 回目の出現でエラーにならない。

演習

/api/Moviesサインインして表示 で、リファレンスページのフィールドを参考に、ライフサイクル全体を試してください。すべてのフィールドを埋めた映画を作り、それから次のことをします。

  1. 読み直し、すべてのフィールドを送ったものと比べる。
  2. ?q= とタイトルの単語で見つける。
  3. 必須フィールドだけで PUT する。どのフィールドがリセットされましたか。
  4. 1 つのフィールドを PATCH し、存在しないフィールド名も 1 つ送る。ignored の一覧を読む。
  5. 削除し、もう一度 GET、PATCH、DELETE する。それぞれのステータスを書き留める。

最後に、開発者に尋ねたいことを 1 つ書き留めてください(無視されたフィールド、再利用された ID、別のエラーに優先したエラー など)。何を報告するかを決めることは、リクエストと同じくらいテストの一部です。

練習とその先へ