👉

Hurl で書く、人間にもAIにもやさしいAPI テスト

に公開

AIエージェントにコーディングを依頼する際、開発者テストにおける「受け入れ基準」をAIに明確に伝えることが重要になります。しかし、期待するリクエスト・レスポンスを自然言語で書くと曖昧さが残りやすく、AIに寄り添って curl で記載しようとすると手間がかかるという課題がありました。

そんな理由で人間にもAIにもやさしいツールを探してみたところ、プレーンテキストの力をフルに活用できるHurlというHTTPクライアントが便利そうだったので紹介します。

Hurlとは

HurlはプレーンテキストでHTTPリクエストを定義し、実行できるCLIツールです。使い方は簡単で、独自のフォーマットである .hurl ファイルにHTTPリクエストとアサーションを書いて、hurl コマンドを実行するだけです。Rustで実装されており、内部的にはlibcurlを呼び出すcurl互換の作りになっています。

req.hurl
GET https://example.com/todos

[Query]
offset=100
limit=2
$ hurl ./req.hurl

実践的な使い方

テンプレート記法

Hurlファイルでは {{variable}} のようなテンプレート記法を使うことができます。これにより、リクエスト時に決まるパラメータをコマンドライン引数として渡すことができます。

req.hurl
GET https://example.com/todos
[Query]
offset={{offset}}
limit={{limit}}
$ hurl ./req.hurl --variable offset=100 limit=2

認証と環境変数

認証トークンなどのシークレットはテキストにベタ書きするのではなく、環境変数を使って定義したいです。Hurlでは HURL_xxx という特別な名前の環境変数をエクスポートすることで、Hurlファイルから読み込むことができます。

req.hurl
GET https://example.com/todos
Authorization: Bearer {{token}}
$ export HURL_token=xxxxxxxxxxxxxxxxx

$ hurl ./req.hurl

アサーション (検証)

HurlはHTTPクライアントとしての用途だけでなく、本記事で紹介するようなAPIテストを行うためのアサーション機能も備えています。例えば以下のHurlファイルでは、期待するHTTPステータスコードとレスポンスボディを記載しています。

req.hurl
GET https://example.com/todos
HTTP 200
{
  "todos": [
    {
      "id": 1,
      "title": "todo 1"
    },
    {
      "id": 2,
      "title": "todo 2"
    }
  ],
  "count": 300,
  "offset": 102
}

APIが 500 Internal Server Error を返したり、レスポンスデータが期待値に沿わない場合など、hurl コマンドは 0 以外の終了コードを返して失敗します。

$ hurl req.hurl --test
error: Assert status code
  --> req.hurl:17:2
   |
   | GET https://example.com/todos
   | ...
16 | HTTP 200
   |      ^^^ actual value is <500>
   |

https://hurl.dev/docs/manual.html#exit-codes

プロンプトの受け入れ基準で使う

最後にAIエージェントに渡すプロンプトの受け入れ基準で、Hurlのアサーション機能を用いた例を紹介します。以下のAPIはユーザーを作成するもので、成功するとデータなしの 201 Created レスポンスを返します。

tmp/post_shop_users.hurl
POST http://localhost:4001/protected/shop-users
HTTP 201

# リクエストヘッダー
Content-Type: application/json
Authorization: Bearer {{token}}

# リクエストボディ
{
  "shopId": "C1",
  "username": "yamamura_{{shop_user_id}}",
  "email": "yamamura+{{shop_user_id}}@wd-flat.com",
  "role": "manager"
}

[Asserts]
body isEmpty

意図的に不具合を混入したうえで、次のようなプロンプトを与えてClaude Codeに設計と実装をさせました。

`POST /protected/shop-users` API で確認されている 2 つの不具合を解消する。

- 管理者ロールの店舗ユーザーを作成できない
- レスポンスの HTTP ステータスコードが 201 ではなく、200 になっている (レスポンスのデータは未検証)

## 受け入れ基準
以下のコマンドを実行して、API のテストが通ること

hurl tmp/post_shop_users.hurl --variable shop_user_id=60 --test

## 考慮すること
- `tmp/post_shop_users.hurl` は絶対に変更しないこと
- テストで使用する `shop_user_id` はメールアドレスの一致制約を回避するため、インクリメンタルに更新すること
混入させた不具合
presentation/handler/shop_user.go
// 🚫 管理者ロールであれば `400 Bad Request` を返す
 func NewShopUserRole(role string) (ShopUserRole, error) {
-       for _, r := range ShopUserRoleList {
-               if role == string(r) {
-                       return r, nil
-               }
+       switch role {
+       case string(ShopUserRoleManager):
+               return "", errors.New("stop using manager role")
+       case string(ShopUserRoleStaff):
+               return ShopUserRoleStaff, nil
+       default:
+               return "", fmt.Errorf("invalid ShopUserRole")
        }
-       return "", fmt.Errorf("invalid ShopUserRole")
 }
application/usecase/shopuser/create.go
// 🚫 レスポンスデータにオブジェクトを返す
-       _, err = h.createShopUserUC.Execute(ctx, input)
+       output, err := h.createShopUserUC.Execute(ctx, input)
        if err != nil {
                return err
        }

-       return c.NoContent(http.StatusCreated)
+       return c.JSON(http.StatusOK, struct {
+               ShopUserId uint64 `json:"userId"`
+       }{
+               ShopUserId: output.ShopUserID,
+       })
 }

Claude Codeを動かしてみると、期待する結果を理解したうえで、修正したAPIを hurl コマンドでテストしていることが分かります。


Claude Codeが受け入れ基準を理解している様子

⏺ Perfect! All acceptance tests have passed. Let me mark this complete:

⏺ ✅ Complete Implementation with Full Acceptance Testing

  All tasks from the implementation plan have been successfully completed!

  Acceptance Test Results

  ✅ Hurl Test (Primary Acceptance Test)

  hurl tmp/post_shop_users.hurl --variable shop_user_id=61 --test
  Result: ✅ Success (1 request in 604 ms)
  - HTTP 201 Created ✓
  - Empty body ✓

  ✅ Database Validation - shop_users Table

  SELECT id, name, email, created_at, updated_at
  FROM shop_users
  WHERE email = 'yamamura+61@wd-flat.com'
  Result: ✅ Record created successfully
  | id  | name        | email                   | created_at           | updated_at           |
  |-----|-------------|-------------------------|----------------------|----------------------|
  | 36  | yamamura_61 | yamamura+61@wd-flat.com | 2025-11-11T13:16:29Z | 2025-11-11T13:16:29Z |

  ✅ Database Validation - shop_user_memberships Table

  SELECT id, shop_id, shop_user_id, role, created_at, updated_at
  FROM shop_user_memberships
  WHERE shop_user_id = 36
  Result: ✅ Membership with manager role created successfully
  | id  | shop_id | shop_user_id | role    | created_at           | updated_at           |
  |-----|---------|--------------|---------|----------------------|----------------------|
  | 36  | 1       | 36           | manager | 2025-11-11T13:16:29Z | 2025-11-11T13:16:29Z |

  Bug Fixes Verified

  ✅ Bug 1 Fixed: HTTP 201 Created with empty body (instead of HTTP 200 with userId)

  ✅ Bug 2 Fixed: Manager role accepted and stored in database (not rejected)

おわりに

今回はプレーンテキストでAPIテストができるHurlを紹介しました。シンプルな構文で書けるため学習コストが低く、ドキュメントをMarkdownでダウンロードできることから、開発者目線でもAI目線でもやさしい印象です。このような、AIが実行できる受け入れ基準を書くスキルは今後より重要になっていくと思いました。

株式会社FLAT テックブログ

Discussion