🐥

json-unit-assertjが便利

に公開

JSONのアサーションを専門に扱うライブラリ「json-unit-assertj」がある。このライブラリはJSON文字列を受け取り、内容をパースして結果を事細かにassertする際に利用できる。AssertJとの統合が果たされており、配列に対するcontainsExactlyInAnyOrderなどの便利関数をそのままJSON配列に対して使用することができる。

https://github.com/lukas-krecan/JsonUnit

仕事で多々JSONのassertをかける場面があるものの、とくに配列の要素の順序が一定とならずflakyテストになってしまうケースに直面した際、AssertJのcontainsExactlyInAnyOrderあたりがJSON文字列に対して使えればと思ったのが動機であった。Geminiに相談してみたところ、このライブラリを教えてもらった。

使い方

build.gradle.ktsに設定を書くと利用可能になる。注意点として、内部的にjackson-databindを使用しているからか、依存にそれを追加する必要がある点が挙げられる。Jacksonの依存への追加を忘れると、テスト実行時に「クラスパスにJacksonなどのライブラリが入っていない」と怒られる。

dependencies {
    testImplementation(kotlin("test"))
    testImplementation("com.fasterxml.jackson.core:jackson-databind:2.19.0")
    testImplementation("net.javacrumbs.json-unit:json-unit-assertj:4.1.1")
}

さてたとえば次のようなJSONを与えるとする。このJSONは配列の中にいくつかのオブジェクトを持っており、いわゆるenvelopeなプロパティは持ち合わせていない。

サンプルのJSON(自動生成)
// language=json  
val example = """  
    [      {        "_id": "6846c6f696325afb2a86008b",  
        "index": 0,  
        "guid": "edd738f8-0ba0-4d94-9186-8025c1661f8b",  
        "isActive": true,  
        "balance": "${'$'}3,556.56",  
        "picture": "http://placehold.it/32x32",  
        "age": 40,  
        "eyeColor": "blue",  
        "name": "Karyn Craft",  
        "gender": "female",  
        "company": "ZANITY",  
        "email": "karyncraft@zanity.com",  
        "phone": "+1 (938) 588-3731",  
        "address": "826 Highland Place, Brethren, Oregon, 2884",  
        "about": "In amet reprehenderit id non reprehenderit sit minim consequat est cupidatat enim quis duis. Ad ea adipisicing culpa consequat nisi est. Irure Lorem labore duis enim. Minim velit culpa cupidatat pariatur adipisicing sunt proident proident est magna. Ipsum consectetur reprehenderit ea nulla qui dolor magna aliqua nostrud excepteur magna est cupidatat. Magna dolore cupidatat laborum sunt enim et sunt duis veniam non.\r\n",  
        "registered": "2023-04-25T07:25:01 -09:00",  
        "latitude": 83.011871,  
        "longitude": -134.828124,  
        "tags": [  
          "sunt",          "laborum",          "et",          "esse",          "adipisicing",          "exercitation",          "ut"        ],        "friends": [  
          {            "id": 0,  
            "name": "Suarez Walters"  
          },          {            "id": 1,  
            "name": "Mueller Ratliff"  
          },          {            "id": 2,  
            "name": "Abby Fields"  
          }        ],        "greeting": "Hello, Karyn Craft! You have 4 unread messages.",  
        "favoriteFruit": "banana"  
      },      {        "_id": "6846c6f66d220bc8ad3395dc",  
        "index": 1,  
        "guid": "d69d196d-c4dd-4152-8428-dd891dca594a",  
        "isActive": false,  
        "balance": "${'$'}2,468.28",  
        "picture": "http://placehold.it/32x32",  
        "age": 39,  
        "eyeColor": "brown",  
        "name": "Warren Hines",  
        "gender": "male",  
        "company": "KRAG",  
        "email": "warrenhines@krag.com",  
        "phone": "+1 (849) 534-3483",  
        "address": "634 Hamilton Walk, Nescatunga, Georgia, 6568",  
        "about": "Minim labore laboris aute culpa amet sunt veniam pariatur est reprehenderit consequat consequat. Culpa dolore fugiat sunt nostrud labore sit deserunt. Enim commodo do eiusmod cillum cupidatat anim incididunt sint aliquip quis proident.\r\n",  
        "registered": "2021-01-24T08:57:17 -09:00",  
        "latitude": 42.129283,  
        "longitude": 96.832804,  
        "tags": [  
          "elit",          "nostrud",          "Lorem",          "et",          "anim",          "anim",          "nostrud"        ],        "friends": [  
          {            "id": 0,  
            "name": "Ila Nichols"  
          },          {            "id": 1,  
            "name": "Sherri Hood"  
          },          {            "id": 2,  
            "name": "Jodi Sampson"  
          }        ],        "greeting": "Hello, Warren Hines! You have 4 unread messages.",  
        "favoriteFruit": "apple"  
      },      {        "_id": "6846c6f6de6ded2d495c4491",  
        "index": 2,  
        "guid": "3304b666-9f3b-4353-9046-d1e274fdb1c8",  
        "isActive": true,  
        "balance": "${'$'}1,901.36",  
        "picture": "http://placehold.it/32x32",  
        "age": 28,  
        "eyeColor": "brown",  
        "name": "Patton Guerrero",  
        "gender": "male",  
        "company": "HOMELUX",  
        "email": "pattonguerrero@homelux.com",  
        "phone": "+1 (942) 466-2224",  
        "address": "578 Caton Place, Vicksburg, Iowa, 3449",  
        "about": "Laborum magna amet sunt dolor consequat ex ullamco proident in reprehenderit fugiat labore fugiat ad. Pariatur et ea ex anim nostrud commodo eiusmod exercitation nostrud esse mollit labore. Et et nisi mollit ea nisi reprehenderit et enim qui laboris. Occaecat cillum voluptate irure cillum. Reprehenderit sunt do deserunt reprehenderit. Do duis consectetur aute ipsum reprehenderit aliquip sit dolor veniam adipisicing consectetur minim non.\r\n",  
        "registered": "2019-09-12T04:41:34 -09:00",  
        "latitude": -55.234946,  
        "longitude": 124.221837,  
        "tags": [  
          "tempor",          "minim",          "deserunt",          "fugiat",          "excepteur",          "pariatur",          "in"        ],        "friends": [  
          {            "id": 0,  
            "name": "Carol French"  
          },          {            "id": 1,  
            "name": "Leigh Acevedo"  
          },          {            "id": 2,  
            "name": "Snyder Winters"  
          }        ],        "greeting": "Hello, Patton Guerrero! You have 8 unread messages.",  
        "favoriteFruit": "banana"  
      },      {        "_id": "6846c6f6ab4667ca49e18d4a",  
        "index": 3,  
        "guid": "52df1eab-8684-46e8-8359-78fd261cdf7b",  
        "isActive": false,  
        "balance": "${'$'}1,424.68",  
        "picture": "http://placehold.it/32x32",  
        "age": 31,  
        "eyeColor": "green",  
        "name": "Watkins Head",  
        "gender": "male",  
        "company": "INTERGEEK",  
        "email": "watkinshead@intergeek.com",  
        "phone": "+1 (820) 463-3682",  
        "address": "776 Oriental Boulevard, Rew, Tennessee, 7259",  
        "about": "Sit qui reprehenderit ad sint aliquip non proident deserunt fugiat aliquip in. Est magna laboris dolor consectetur cupidatat reprehenderit proident. Consectetur nisi eu et laborum. Duis amet mollit nostrud deserunt ut est reprehenderit nulla. Occaecat in consequat proident ipsum cillum laboris excepteur aliquip est qui. Non culpa excepteur ut laboris ad. Sunt qui eiusmod culpa pariatur.\r\n",  
        "registered": "2024-01-06T01:52:12 -09:00",  
        "latitude": 34.359496,  
        "longitude": -88.012374,  
        "tags": [  
          "ut",          "veniam",          "dolore",          "eiusmod",          "sunt",          "excepteur",          "anim"        ],        "friends": [  
          {            "id": 0,  
            "name": "Lillian Glass"  
          },          {            "id": 1,  
            "name": "Phelps Rollins"  
          },          {            "id": 2,  
            "name": "Stephanie Riggs"  
          }        ],        "greeting": "Hello, Watkins Head! You have 8 unread messages.",  
        "favoriteFruit": "apple"  
      },      {        "_id": "6846c6f6ab28ceb229111001",  
        "index": 4,  
        "guid": "bb2937cb-6fdc-48e8-b25a-5c019efa32fd",  
        "isActive": false,  
        "balance": "${'$'}1,525.43",  
        "picture": "http://placehold.it/32x32",  
        "age": 31,  
        "eyeColor": "brown",  
        "name": "Joseph Holder",  
        "gender": "male",  
        "company": "BYTREX",  
        "email": "josephholder@bytrex.com",  
        "phone": "+1 (846) 576-2368",  
        "address": "776 Delmonico Place, Salvo, South Dakota, 7832",  
        "about": "Id ea dolor labore nulla. Ea amet cupidatat fugiat tempor do. Excepteur occaecat eiusmod laborum laboris reprehenderit dolor minim. Ex nulla tempor eiusmod adipisicing esse consectetur occaecat. Sint adipisicing ut duis et proident cillum ipsum cupidatat. Laboris amet laboris adipisicing aute labore aliquip esse ullamco consectetur Lorem occaecat qui minim.\r\n",  
        "registered": "2019-09-29T01:32:42 -09:00",  
        "latitude": 49.10058,  
        "longitude": -35.773968,  
        "tags": [  
          "qui",          "proident",          "anim",          "minim",          "sint",          "elit",          "duis"        ],        "friends": [  
          {            "id": 0,  
            "name": "Montoya Durham"  
          },          {            "id": 1,  
            "name": "Whitney Wilder"  
          },          {            "id": 2,  
            "name": "Shirley Shelton"  
          }        ],        "greeting": "Hello, Joseph Holder! You have 4 unread messages.",  
        "favoriteFruit": "apple"  
      },      {        "_id": "6846c6f6bd42fd6b05a78249",  
        "index": 5,  
        "guid": "ef8417e6-4333-427a-aa0f-d4d0792c7ec3",  
        "isActive": false,  
        "balance": "${'$'}1,177.21",  
        "picture": "http://placehold.it/32x32",  
        "age": 31,  
        "eyeColor": "brown",  
        "name": "Schroeder Morton",  
        "gender": "male",  
        "company": "QUINEX",  
        "email": "schroedermorton@quinex.com",  
        "phone": "+1 (912) 557-3932",  
        "address": "755 Sullivan Place, Sunriver, District Of Columbia, 5888",  
        "about": "Adipisicing id cupidatat cillum ut aliqua velit nisi aliquip est. Veniam exercitation mollit cillum nostrud ea voluptate cillum deserunt consectetur reprehenderit culpa. Fugiat fugiat ex irure qui. Magna officia esse amet in. Qui aute exercitation duis minim anim.\r\n",  
        "registered": "2015-08-31T12:47:06 -09:00",  
        "latitude": -65.132682,  
        "longitude": -72.963713,  
        "tags": [  
          "cillum",          "culpa",          "nisi",          "dolore",          "magna",          "ex",          "tempor"        ],        "friends": [  
          {            "id": 0,  
            "name": "Peters Mcintosh"  
          },          {            "id": 1,  
            "name": "Cummings Quinn"  
          },          {            "id": 2,  
            "name": "Michelle Oneill"  
          }        ],        "greeting": "Hello, Schroeder Morton! You have 6 unread messages.",  
        "favoriteFruit": "strawberry"  
      },      {        "_id": "6846c6f6260b4cac670badca",  
        "index": 6,  
        "guid": "bae31243-7780-4269-bcb4-f2d0e7831087",  
        "isActive": false,  
        "balance": "${'$'}2,131.09",  
        "picture": "http://placehold.it/32x32",  
        "age": 37,  
        "eyeColor": "blue",  
        "name": "Alissa Booth",  
        "gender": "female",  
        "company": "CINESANCT",  
        "email": "alissabooth@cinesanct.com",  
        "phone": "+1 (945) 517-3147",  
        "address": "652 Otsego Street, Caln, Alaska, 8434",  
        "about": "Irure irure mollit ut amet nostrud eu nisi amet adipisicing aliquip fugiat eu consectetur. Officia ex aliquip enim in minim commodo tempor minim officia. Commodo id eu duis nostrud nisi dolor labore aliqua commodo. Quis magna reprehenderit nostrud culpa veniam ad fugiat pariatur. Dolor nulla consectetur mollit magna quis anim elit aute magna duis dolor amet do. Anim duis consequat do culpa enim id. Quis nulla nisi sunt labore ea quis.\r\n",  
        "registered": "2017-06-23T12:07:55 -09:00",  
        "latitude": -9.870575,  
        "longitude": -95.793013,  
        "tags": [  
          "minim",          "esse",          "eu",          "Lorem",          "dolor",          "laboris",          "voluptate"        ],        "friends": [  
          {            "id": 0,  
            "name": "Ollie Rutledge"  
          },          {            "id": 1,  
            "name": "Carissa Fry"  
          },          {            "id": 2,  
            "name": "Melody Dickerson"  
          }        ],        "greeting": "Hello, Alissa Booth! You have 9 unread messages.",  
        "favoriteFruit": "strawberry"  
      }    ]""".trimIndent()

JSON配列の中に含まれる1つ目の要素を取り出すには、次のように独自の記法で書いてやる必要がある。

assertThatJson(example).node("[0]")

.<プロパティ名>で配列の中の要素にアクセスできる。

assertThatJson(example).node("[0].tags")

.isArray.isObjectなどの関数をメソッドチェーンの中に書くと、以降の処理を配列やオブジェクトとして扱うことができる。種別を特定する関数を呼び出すと、以降はたとえば配列なら配列、オブジェクトならオブジェクトに対するAssertJの関連関数を呼び出せるようになる。たとえば、配列でのみ呼び出しできるcontainsExactlyInAnyOrderを呼び出すには次のように書けば良い。

        assertThatJson(example).node("[0].tags").isArray.containsExactlyInAnyOrder(
            "sunt",
            "laborum",
            "esse",
            "et",
            "adipisicing",
            "exercitation",
            "ut"
        )

配列の中にある要素にアクセスするには、たとえば次のようにネストさせてassertThatJsonを呼び出せば良いようだ。

        assertThatJson(example).isArray.satisfies({ elem ->
            assertThatJson(elem[0]!!).node("tags").isArray.containsAnyElementsOf(listOf("sunt", "laborum"))
        })

isArrayの部分の内部実装の仕組みが少し気になるので深掘りしておく。内部的には単に配列だったらそのままそのノードを配列であると判定させておき、JsonListAssertというAssertJのリスト用の関数を呼び出せるようになるクラスを返している。

    public @NotNull JsonListAssert isArray() {
        Node node = this.assertType(NodeType.ARRAY);
        return (JsonListAssert)this.createListAssert(node).as("Node \"%s\"", new Object[]{this.path});
    }

困りごと

Kotlin側からの利用で気になる点をあげておくと、JacksonなどJavaのJSONパース用のライブラリの使用が前提とされている点である。たとえばKotlin向けのJSONパース用のライブラリであるkotlinx.serializationを使用している場合、そのプロジェクトではライブラリを使用することはできない。コントリビューションチャンスなのかもしれないが。

まとめ

AssertJときれいに連携できるJSON assertに便利なライブラリを紹介した。Kotlinでも利用できるが元々はJavaのライブラリであるため、使用するJSONパース用のライブラリ面での制約がある点に注意が必要である。

紹介した以外にもさまざまな機能を持つようなので、一度GitHubリポジトリのREADMEを眺めてみることもおすすめする。

Discussion