理系学生日記

おまえはいつまで学生気分なのか

Stubby4JにおけるJSONでの正規表現、あるいはリクエスト中の特定のフィールドでレスポンスを変更する

Stubby4J を使用したテストにおいて、API リクエストとして送る JSON の中の値によって、レスポンスを切り替えたい場合がある。 もうちょっと具体的にいうと、

  1. リクエストの JSON の中の特定のフィールドの値によってのみ、レスポンスを切り替える
  2. 他のフィールドの値は実行時まで予想できない

こういうケースでレスポンスを切り替えるのは Stubby4J (v3.3.0) ではできないということが分かったので、その原因と、何とかする方法を考えてみる。

実装上できない理由

Stubby4J では、実際に送られてきた HTTP リクエストと、設定ファイル(YAML) 上に記述されたリクエストを StubRequest#equals(Object) で突合し、合致する場合にのみ、YAML ファイル上に記述されたレスポンスを返却するという動作になっている。 この「突合」(equals) の実装は、実際の HTTP リクエストの Content-Type によって挙動を替えるようになっている。application/json の場合は、「送信されてきた JSON」と「YAML ファイル上に記述された JSON」が、JSON のフィールド順を除いて完全一致すること が条件になっている。 以下に実装を示すけど、JSONCompare を使用しているということは、HTTP リクエスト上の JSON はもちろん、YAML 上に記述した JSON も valid である必要があるので、何らかの細工 (正規表現を記述するなど) もできない。

   private boolean postBodiesMatch(final boolean isDataStorePostStubbed, final String dataStorePostBody, final String thisAssertingPostBody) {
         // snip

         } else if (isSet(assertingContentType) && assertingContentType.contains(Common.HEADER_APPLICATION_JSON)) {
            try {
               return JSONCompare.compareJSON(dataStorePostBody, thisAssertingPostBody, JSONCompareMode.NON_EXTENSIBLE).passed();
            } catch (JSONException e) {
               return false;
            }
         } else if (isSet(assertingContentType) && assertingContentType.contains(Common.HEADER_APPLICATION_XML)) {

         // snip
   }

設計思想によってできない理由

Json request body matching by vishalmanohar · Pull Request #19 · azagniotov/stubby4j · GitHub で、JSON の中の値でレスポンスを切り替えられるような Pull Request があった。 これを利用すると、以下のようにリクエストの JSON に依存したレスポンスの設定がかけるようになる。

-  request:
      method: POST
      url: /templatize/response
   response:
      -  status: 200
         headers:
            content-type: application/json
         body: >
               <#assign requestBodyJson = requestBody?eval>
               {"foo": "${requestBodyJson.foo}"}

のだけれど、この Pull Request に対しては「HTTP リクエストと YAML 上のリクエストの突合は HTTP 1.1 レベルでしか実施しない」という設計思想が語られており、Merge されないままクローズされた。 実はこのあとに Content-Type に依存した突合の実装が取り込まれていることから鑑みるに、設計思想が変化しているみたいなのだけれど…。

何とかする方法

上記の PR と同じような修正を行ったり、あるいは現行の実装で使用している JSONCompare での比較ではなく、もっと柔軟な比較 (正規表現を許容する、jsonpath で比較すべきフィールドを指定する、JSON Schema を書けるようにする、等) をできるようにする、などはできるのだけど、とりあえず実現するだけなら以下で可能になる。

$ git diff  main/java/by/stub/yaml/stubs/StubRequest.java
diff --git a/main/java/by/stub/yaml/stubs/StubRequest.java b/main/java/by/stub/yaml/stubs/StubRequest.java
index 7e6a8a8..d76d6de 100644
--- a/main/java/by/stub/yaml/stubs/StubRequest.java
+++ b/main/java/by/stub/yaml/stubs/StubRequest.java
@@ -252,12 +252,6 @@ public class StubRequest {
          final boolean isAssertingValueSet = isSet(thisAssertingPostBody);
          if (!isAssertingValueSet) {
             return false;
-         } else if (isSet(assertingContentType) && assertingContentType.contains(Common.HEADER_APPLICATION_JSON)) {
-            try {
-               return JSONCompare.compareJSON(dataStorePostBody, thisAssertingPostBody, JSONCompareMode.NON_EXTENSIBLE).passed();
-            }

これはかなり強引な方法で、いってみれば「JSON とか知るか!! 文字列として扱うんや!!! 」という方法である。 つまり、リクエストが valid JSON でなくても、XML や Form であってもマッチしてしまうので、YAML 上でリクエストを記述するときにかなりの注意を払わなければならない。

上記修正を行った後で、gradle build -x test でビルドして実験し、以下のような YAML で Stubby4J を立ち上げる。

- request:
    method: POST
    url: /hello-world
    post: >
      \{\s*"hello"\s*:\s*"(.*?)"\s*.*\}

  response:
    status: 200
    headers:
      content-type: application/json
    body: Hello, <% post.1 %>

これに対して、リクエストを送信すると、きちんとレスポンスを切り替えられるようになる

$ curl -H'content-type: application/json' -d "{\"hello\":\"kiririmode\", \"hoge\":\"fuga\" }" http://localhost:8882/hello-world
Hello, kiririmode
$ curl -H'content-type: application/json' -d "{\"hello\":\"world\", \"var\":\"val\" }" http://localhost:8882/hello-world
Hello, world