1. 概要

JSONを使用するソフトウェアの自動テストを作成する場合、JSONデータを期待値と比較する必要があることがよくあります。

場合によっては、実際のJSONと予想されるJSONを文字列として扱い、文字列の比較を実行できますが、これには多くの制限があります。

このチュートリアルでは、ModelAssertを使用してアサーションとJSON値間の比較を作成する方法を見ていきます。 JSONドキュメント内の個々の値にアサーションを作成する方法とドキュメントを比較する方法を見ていきます。 また、日付やGUIDなど、正確な値を予測できないフィールドの処理方法についても説明します。

2. 入門

ModelAssertは、 AssertJ と同様の構文を持ち、JSONAssertと同等の機能を備えたデータアサーションライブラリです。 これは、JSON解析用の Jackson に基づいており、 JSONPointer式を使用してドキュメント内のフィールドへのパスを記述します。

このJSONの簡単なアサーションを作成することから始めましょう。

{
   "name": "Baeldung",
   "isOnline": true,
   "topics": [ "Java", "Spring", "Kotlin", "Scala", "Linux" ]
}

2.1. 依存

まず、ModelAssertpom.xmlに追加しましょう。

<dependency>
    <groupId>uk.org.webcompere</groupId>
    <artifactId>model-assert</artifactId>
    <version>1.0.0</version>
    <scope>test</scope>
</dependency>

2.2. JSONオブジェクトのフィールドをアサートする

サンプルのJSONがString、として返され、nameフィールドがBaeldungと等しいことを確認したいとします。

assertJson(jsonString)
  .at("/name").isText("Baeldung");

assertJson メソッドは、 String File Path、、Jacksonの JsonNode など、さまざまなソースからJSONを読み取ります。 。 返されるオブジェクトはアサーションであり、その上で流暢なDSL(ドメイン固有言語)を使用して条件を追加できます。

at メソッドは、フィールドアサーションを作成するドキュメント内の場所を記述します。 次に、 isText は、値がBaeldungのテキストノードを期待することを指定します。

少し長いJSONポインター式を使用して、topics配列内のパスをアサートできます。

assertJson(jsonString)
  .at("/topics/1").isText("Spring");

フィールドアサーションを1つずつ記述できますが、それらを組み合わせて1つのアサーションにすることもできます

assertJson(jsonString)
  .at("/name").isText("Baeldung")
  .at("/topics/1").isText("Spring");

2.3. 文字列比較が機能しない理由

多くの場合、JSONドキュメント全体を別のドキュメントと比較したいと思います。 文字列の比較は、場合によっては可能ですが、多くの場合、無関係なJSONフォーマットの問題によって検出されます。

String expected = loadFile(EXPECTED_JSON_PATH);
assertThat(jsonString)
  .isEqualTo(expected);

このような失敗メッセージは一般的です:

org.opentest4j.AssertionFailedError: 
expected: "{
    "name": "Baeldung",
    "isOnline": true,
    "topics": [ "Java", "Spring", "Kotlin", "Scala", "Linux" ]
}"
but was : "{"name": "Baeldung","isOnline": true,"topics": [ "Java", "Spring", "Kotlin", "Scala", "Linux" ]}"

2.4. 木を意味的に比較する

ドキュメント全体を比較するには、isEqualToを使用できます。

assertJson(jsonString)
  .isEqualTo(EXPECTED_JSON_PATH);

この場合、実際のJSONの文字列は assertJson によって読み込まれ、予想されるJSONドキュメント( Path で記述されたファイル)はisEqualTo内に読み込まれます。 ]。 比較はデータに基づいて行われます。

2.5. さまざまなフォーマット

ModelAssertは、Jacksonによって JsonNode に変換できるJavaオブジェクト、およびyaml形式もサポートしています。

Map<String, String> map = new HashMap<>();
map.put("name", "baeldung");

assertJson(map)
  .isEqualToYaml("name: baeldung");

yaml 処理の場合、 isEqualToYaml メソッドを使用して、文字列またはファイルの形式を示します。 ソースがyamlの場合、これにはassertYamlが必要です。

assertYaml("name: baeldung")
  .isEqualTo(map);

3. フィールドアサーション

これまで、いくつかの基本的なアサーションを見てきました。 DSLの詳細を見てみましょう。

3.1. 任意のノードでのアサート

ModelAssertのDSLを使用すると、ツリー内の任意のノードに対して、ほぼすべての可能な条件を追加できます。 これは、JSONツリーに任意のレベルの任意のタイプのノードが含まれている可能性があるためです。

サンプルJSONのルートノードに追加する可能性のあるいくつかのアサーションを見てみましょう。

assertJson(jsonString)
  .isNotNull()
  .isNotNumber()
  .isObject()
  .containsKey("name");

アサーションオブジェクトのインターフェイスでこれらのメソッドを使用できるため、IDEは、「。」キーを押した瞬間に追加できるさまざまなアサーションを提案します。

この例では、最後の条件がすでにnull以外のオブジェクトを暗示しているため、多くの不要な条件を追加しました。

ほとんどの場合、ツリーの下のノードでアサーションを実行するために、ルートノードからのJSONポインター式を使用します。

assertJson(jsonString)
  .at("/topics").hasSize(5);

このアサーションは、 hasSize を使用して、topicフィールドの配列に5つの要素があることを確認します。 hasSize メソッドは、オブジェクト、配列、および文字列を操作します。 オブジェクトのサイズはキーの数、文字列のサイズは文字数、配列のサイズは要素の数です。

フィールドに対して行う必要のあるほとんどのアサーションは、フィールドの正確なタイプによって異なります。 メソッドnumber array text booleanNode 、および object を使用して、特定のタイプでアサーションを書き込もうとしているときのアサーションの特定のサブセット。 これはオプションですが、より表現力豊かにすることができます。

assertJson(jsonString)
  .at("/isOnline").booleanNode().isTrue();

booleanNode の後にIDEで“。” キーを押すと、ブールノードのオートコンプリートオプションのみが表示されます。

3.2. テキストノード

テキストノードをアサートする場合、 isText を使用して、正確な値を使用して比較できます。 または、 textContains を使用して、サブストリングをアサートすることもできます。

assertJson(jsonString)
  .at("/name").textContains("ael");

matchsを介して正規表現を使用することもできます。

assertJson(jsonString)
  .at("/name").matches("[A-Z].+");

この例では、nameが大文字で始まることを示しています。

3.3. ナンバーノード

ナンバーノードの場合、DSLはいくつかの有用な数値比較を提供します。

assertJson("{count: 12}")
  .at("/count").isBetween(1, 25);

期待するJava数値タイプを指定することもできます。

assertJson("{height: 6.3}")
  .at("/height").isGreaterThanDouble(6.0);

isEqualTo メソッドはツリー全体のマッチング用に予約されているため、数値の同等性を比較するには、isNumberEqualToを使用します。

assertJson("{height: 6.3}")
  .at("/height").isNumberEqualTo(6.3);

3.4. 配列ノード

isArrayContaining を使用して、配列の内容をテストできます。

assertJson(jsonString)
  .at("/topics").isArrayContaining("Scala", "Spring");

これにより、指定された値の存在がテストされ、実際の配列に追加の項目を含めることができます。 より完全な一致を表明したい場合は、isArrayContainingExactlyInAnyOrderを使用できます。

assertJson(jsonString)
   .at("/topics")
   .isArrayContainingExactlyInAnyOrder("Scala", "Spring", "Java", "Linux", "Kotlin");

また、これに正確な順序を要求させることもできます。

assertJson(ACTUAL_JSON)
  .at("/topics")
  .isArrayContainingExactly("Java", "Spring", "Kotlin", "Scala", "Linux");

これは、プリミティブ値を含む配列の内容をアサートするための優れた手法です。 配列にオブジェクトが含まれている場合は、代わりにisEqualToを使用することをお勧めします。

4. ツリー全体のマッチング

複数のフィールド固有の条件を使用してアサーションを作成し、JSONドキュメントの内容を確認することはできますが、多くの場合、ドキュメント全体を別のドキュメントと比較する必要があります。

isEqualTo メソッド(または isNotEqualTo )は、ツリー全体を比較するために使用されます。 これをatと組み合わせて、比較を行う前に実際のサブツリーに移動できます。

assertJson(jsonString)
  .at("/topics")
  .isEqualTo("[ \"Java\", \"Spring\", \"Kotlin\", \"Scala\", \"Linux\" ]");

JSONに次のいずれかのデータが含まれている場合、ツリー全体の比較で問題が発生する可能性があります。

  • 同じですが、順序が異なります
  • 予測できないいくつかの値で構成されています

where メソッドは、これらを回避するために次のisEqualTo操作をカスタマイズするために使用されます。

4.1. キー順序制約の追加

同じように見える2つのJSONドキュメントを見てみましょう。

String actualJson = "{a:{d:3, c:2, b:1}}";
String expectedJson = "{a:{b:1, c:2, d:3}}";

これは厳密にはJSON形式ではないことに注意してください。 ModelAssertを使用すると、JSON のJavaScript表記と、通常はフィールド名を引用するワイヤー形式を使用できます。

これらの2つのドキュメントは、「a」の下にまったく同じキーがありますが、順序が異なります。 ModelAssertのデフォルトは厳密なキー順序であるため、これらのアサーションは失敗します。

where 構成を追加することで、キーの順序規則を緩和できます。

assertJson(actualJson)
  .where().keysInAnyOrder()
  .isEqualTo(expectedJson);

これにより、ツリー内の任意のオブジェクトが、予想されるドキュメントとは異なるキーの順序を持ち、それでも一致することができます。

このルールを特定のパスにローカライズできます。

assertJson(actualJson)
  .where()
    .at("/a").keysInAnyOrder()
  .isEqualTo(expectedJson);

これにより、 keysInAnyOrder がルートオブジェクトの“ a”フィールドのみに制限されます。

比較ルールをカスタマイズする機能により、作成された正確なドキュメントを完全に制御または予測できない多くのシナリオを処理できます。

4.2. 配列制約の緩和

値の順序が変化する可能性のある配列がある場合は、比較全体で配列の順序の制約を緩和できます。

String actualJson = "{a:[1, 2, 3, 4, 5]}";
String expectedJson = "{a:[5, 4, 3, 2, 1]}";

assertJson(actualJson)
  .where().arrayInAnyOrder()
  .isEqualTo(expectedJson);

または、 keysInAnyOrder で行ったように、その制約をパスに制限することもできます。

4.3. パスを無視する

たぶん、私たちの実際のドキュメントには、興味がないか予測できないフィールドがいくつか含まれています。 そのパスを無視するルールを追加できます。

String actualJson = "{user:{name: \"Baeldung\", url:\"http://www.baeldung.com\"}}";
String expectedJson = "{user:{name: \"Baeldung\"}}";

assertJson(actualJson)
  .where()
    .at("/user/url").isIgnored()
  .isEqualTo(expectedJson);

ここで表現しているパスは、実際の内のJSONポインターに関して常にであることに注意してください。

実際の余分なフィールド“ url”は無視されるようになりました。

4.4. GUIDを無視する

これまでのところ、ドキュメント内の特定の場所での比較をカスタマイズするために、atを使用するルールのみを追加しました。

path 構文を使用すると、ワイルドカードを使用してルールが適用される場所を記述できます。 比較のwhereatまたはpath条件を追加すると、上から任意のフィールドアサーションを提供することもできます予想されるドキュメントと並べて比較する代わりに使用します。

ドキュメント内の複数の場所に表示され、予測できないGUIDであるidフィールドがあるとします。

パスルールを使用すると、このフィールドを無視できます。

String actualJson = "{user:{credentials:[" +
  "{id:\"a7dc2567-3340-4a3b-b1ab-9ce1778f265d\",role:\"Admin\"}," +
  "{id:\"09da84ba-19c2-4674-974f-fd5afff3a0e5\",role:\"Sales\"}]}}";
String expectedJson = "{user:{credentials:" +
  "[{id:\"???\",role:\"Admin\"}," +
  "{id:\"???\",role:\"Sales\"}]}}";

assertJson(actualJson)
  .where()
    .path("user","credentials", ANY, "id").isIgnored()
  .isEqualTo(expectedJson);

ここでは、JSONポインターが「/ user / credentials」で始まり、単一のノード(配列index)で終わり、“ / id”で終わります。

4.5. 任意のGUIDに一致

予測できないフィールドを無視することは1つのオプションです。 代わりに、これらのノードをタイプごとに一致させることをお勧めします。また、それらが満たさなければならない他の条件によっても一致させることをお勧めします。 これらのGUIDをGUIDのパターンに一致させるように切り替えて、idノードがツリーの任意のリーフノードに表示されるようにします。

assertJson(actualJson)
  .where()
    .path(ANY_SUBTREE, "id").matches(GUID_PATTERN)
  .isEqualTo(expectedJson);

ANY_SUBTREE ワイルドカードは、パス式の一部の間の任意の数のノードに一致します。 GUID_PATTERN は、ModelAssert Patterns クラスに由来します。このクラスには、数値や日付スタンプなどに一致する一般的な正規表現がいくつか含まれています。

4.6. isEqualToのカスタマイズ

wherepathまたはat式の組み合わせにより、ツリー内のどこでも比較をオーバーライドできます。 オブジェクトまたは配列のマッチングに組み込まれたルールを追加するか、比較内のパスの個々のクラスまたはクラスに使用する特定の代替アサーションを指定します。

さまざまな比較で再利用される共通の構成がある場合、それをメソッドに抽出できます。

private static <T> WhereDsl<T> idsAreGuids(WhereDsl<T> where) {
    return where.path(ANY_SUBTREE, "id").matches(GUID_PATTERN);
}

次に、 configureBy を使用して、その構成を特定のアサーションに追加できます。

assertJson(actualJson)
  .where()
    .configuredBy(where -> idsAreGuids(where))
  .isEqualTo(expectedJson);

5. 他のライブラリとの互換性

ModelAssertは、相互運用性のために構築されました。 これまで、AssertJスタイルのアサーションを見てきました。 これらには複数の条件があり、満たされていない最初の条件で失敗します。

ただし、他のタイプのテストで使用するために、マッチャーオブジェクトを作成する必要がある場合があります。

5.1. ハムクレストマッチャー

Hamcrest は、多くのツールでサポートされている主要なアサーションヘルパーライブラリです。 ModelAssertのDSLを使用して、Hamcrestマッチャーを作成できます

Matcher<String> matcher = json()
  .at("/name").hasValue("Baeldung");

json メソッドは、JSONデータを含むStringを受け入れるマッチャーを記述するために使用されます。 jsonFile を使用して、Fileの内容をアサートすることを期待するMatcherを生成することもできます。 ModelAssertのJsonAssertionsクラスには、Hamcrestマッチャーの構築を開始するためのこのような複数のビルダーメソッドが含まれています。

比較を表現するためのDSLはassertJsonと同じですが、何かがマッチャーを使用するまで比較は実行されません。

したがって、HamcrestのMatcherAssertでModelAssertを使用できます。

MatcherAssert.assertThat(jsonString, json()
  .at("/name").hasValue("Baeldung")
  .at("/topics/1").isText("Spring"));

5.2. SpringMockMVCでの使用

Spring Mock MVC 応答本体検証を使用している間、Springの組み込みのjsonPathアサーションを使用できます。 ただし、Springでは、 Hamcrestマッチャーを使用して、応答コンテンツとして返された文字列をアサートすることもできます。 これは、ModelAssertを使用して高度なコンテンツアサーションを実行できることを意味します。

5.3. Mockitoで使用

MockitoはすでにHamcrestと相互運用可能です。 ただし、ModelAssertはネイティブのArgumentMatcherも提供します。 これは、スタブの動作を設定するためと、スタブへの呼び出しを検証するための両方に使用できます。

public interface DataService {
    boolean isUserLoggedIn(String userDetails);
}

@Mock
private DataService mockDataService;

@Test
void givenUserIsOnline_thenIsLoggedIn() {
    given(mockDataService.isUserLoggedIn(argThat(json()
      .at("/isOnline").isTrue()
      .toArgumentMatcher())))
      .willReturn(true);

    assertThat(mockDataService.isUserLoggedIn(jsonString))
      .isTrue();

    verify(mockDataService)
      .isUserLoggedIn(argThat(json()
        .at("/name").isText("Baeldung")
        .toArgumentMatcher()));
}

この例では、Mockito argThat は、モックのセットアップとverifyの両方で使用されます。 その中で、マッチャーにHamcrestスタイルビルダー– jsonを使用します。 次に、条件を追加し、最後にtoArgumentMatcherでMockitoのArgumentMatcherに変換します。

6. 結論

この記事では、テストでJSONを意味的に比較する必要性について説明しました。

ModelAssertを使用して、JSONドキュメント内の個々のノードおよびツリー全体でアサーションを構築する方法を確認しました。 次に、ツリーの比較をカスタマイズして、予測できない、または無関係な違いを許容する方法を確認しました。

最後に、Hamcrestやその他のライブラリでModelAssertを使用する方法を確認しました。

いつものように、このチュートリアルのサンプルコードは、GitHubから入手できます。