Guide to the ModelAssert Library for JSON – 用于JSON的ModelAssert库指南

最后修改: 2021年 6月 21日

中文/混合/英文(键盘快捷键:t)

1. Overview

1.概述

When writing automated tests for software that uses JSON, we often need to compare JSON data with some expected value.

在为使用JSON的软件编写自动测试时,我们经常需要将JSON数据与一些预期值进行比较。

In some cases, we can treat the actual and expected JSON as strings and perform string comparison, but this has many limitations.

在某些情况下,我们可以将实际和预期的JSON视为字符串,并进行字符串比较,但这有很多限制。

In this tutorial, we’ll look at how to write assertions and comparisons between JSON values using ModelAssert. We’ll see how to construct assertions on individual values within a JSON document and how to compare documents. We’ll also cover how to handle fields whose exact values cannot be predicted, such as dates or GUIDs.

在本教程中,我们将了解如何使用ModelAssert编写断言和在JSON值之间进行比较。我们将看到如何对JSON文档中的单个值构建断言以及如何比较文档。我们还将讨论如何处理那些无法预测确切值的字段,如日期或GUID。

2. Getting Started

2.入门

ModelAssert is a data assertion library with a syntax similar to AssertJ and features comparable to JSONAssert. It’s based on Jackson for JSON parsing and uses JSON Pointer expressions to describe paths to fields in the document.

ModelAssert是一个数据断言库,其语法类似于AssertJ,功能与JSONAssert相当。它基于Jackson进行JSON解析,并使用JSON Pointer表达式来描述文档中字段的路径。

Let’s start by writing some simple assertions for this JSON:

让我们先为这个JSON写一些简单的断言。

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

2.1. Dependency

2.1. 依赖性

To start, let’s add ModelAssert to our pom.xml:

首先,让我们把ModelAssert添加到我们的pom.xml

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

2.2. Assert a Field in a JSON Object

2.2.断言JSON对象中的一个字段

Let’s imagine that the example JSON has been returned to us as a String, and we want to check that the name field is equal to Baeldung:

让我们想象一下,例子中的JSON已经作为一个String,返回给我们,我们想检查name字段是否等于Baeldung

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

The assertJson method will read JSON from various sources, including StringFilePath, and Jackson’s JsonNode. The object returned is an assertion, upon which we can use the fluent DSL (domain-specific language) to add conditions.

assertJson方法将从各种来源读取JSON,包括StringFilePath和Jackson的JsonNode。返回的对象是一个断言,我们可以在此基础上使用流畅的DSL(特定领域语言)来添加条件。

The at method describes a place within the document where we wish to make a field assertion. Then, isText specifies that we expect a text node with the value Baeldung.

at方法描述了我们希望在文档中进行字段断言的一个地方。然后,isText指定我们期待一个值为Baeldung的文本节点。

We can assert a path within the topics array by using a slightly longer JSON Pointer expression:

我们可以通过使用一个稍长的JSON Pointer表达式来断言topics数组内的路径。

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

While we can write field assertions one by one, we can also combine them into a single assertion:

虽然我们可以一个一个地写字段断言,我们也可以把它们组合成一个断言

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

2.3. Why String Comparison Doesn’t Work

2.3.为什么字符串比较不起作用

Often we want to compare a whole JSON document with another. String comparison, though possible in some cases, often gets caught out by irrelevant JSON formatting issues:

通常我们想把整个JSON文档与另一个文档进行比较。字符串比较,虽然在某些情况下是可能的,但经常会被不相关的JSON格式化问题所困住。

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

A failure message like this is common:

像这样的失败信息是很常见的。

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. Comparing Trees Semantically

2.4.从语义上对树进行比较

To make a whole document comparison, we can use isEqualTo:

为了进行整个文档的比较,我们可以使用isEqualTo

assertJson(jsonString)
  .isEqualTo(EXPECTED_JSON_PATH);

In this instance, the string of the actual JSON is loaded by assertJson, and the expected JSON document – a file described by a Path – is loaded inside the isEqualTo. The comparison is made based on the data.

在这个例子中,实际JSON的字符串被assertJson加载,而预期的JSON文档–一个由Path描述的文件–被加载到isEqualTo里面。比较是根据数据进行的。

2.5. Different Formats

2.5.不同的格式

ModelAssert also supports Java objects that can be converted to JsonNode by Jackson, as well as the yaml format.

ModelAssert还支持可以被Jackson转换为JsonNode的Java对象,以及yaml格式。

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

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

For yaml handling, the isEqualToYaml method is used to indicate the format of the string or file. This requires assertYaml if the source is yaml:

对于yaml处理,isEqualToYaml方法被用来指示字符串或文件的格式。如果源是yaml,这需要assertYaml

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

3. Field Assertions

3.现场断言

So far, we’ve seen some basic assertions. Let’s look at more of the DSL.

到目前为止,我们已经看到了一些基本的断言。让我们来看看DSL的更多内容。

3.1. Asserting at Any Node

3.1.在任何节点断言

The DSL for ModelAssert allows nearly every possible condition to be added against any node in the tree. This is because JSON trees may contain nodes of any type at any level.

ModelAssert的DSL允许针对树中的任何节点添加几乎所有可能的条件。这是因为JSON树可能包含任何级别的任何类型的节点。

Let’s look at some assertions we might add to the root node of our example JSON:

让我们看看我们可能添加到JSON例子的根节点上的一些断言。

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

As the assertion object has these methods available on its interface, our IDE will suggest the various assertions we can add the moment we press the “.” key.

由于断言对象在其接口上有这些方法,我们的IDE将在我们按下“.”键的瞬间建议我们可以添加各种断言。

In this example, we have added lots of unnecessary conditions since the last condition already implies a non-null object.

在这个例子中,我们添加了很多不必要的条件,因为最后一个条件已经暗示了一个非空对象。

Most often, we use JSON Pointer expressions from the root node in order to perform assertions on nodes lower down the tree:

最常见的是,我们使用根节点的JSON指针表达式,以便对树下的节点执行断言。

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

This assertion uses hasSize to check that the array in the topic field has five elements. The hasSize method operates on objects, arrays, and strings. An object’s size is its number of keys, a string’s size is its number of characters, and an array’s size is its number of elements.

这个断言使用hasSize来检查topic字段中的数组是否有五个元素。hasSize方法对对象、数组和字符串进行操作。一个对象的大小是它的键数,一个字符串的大小是它的字符数,一个数组的大小是它的元素数。

Most assertions we need to make on fields depend on the exact type of the field. We can use the methods numberarraytextbooleanNode, and object to move into a more specific subset of the assertions when we’re trying to write assertions on a particular type. This is optional but can be more expressive:

我们需要对字段进行的大多数断言取决于字段的确切类型。我们可以使用numberarraytextbooleanNodeobject等方法,在我们试图对特定类型编写断言时,进入更具体的断言子集。这是可选的,但可以更有表现力。

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

When we press the “.” key in our IDE after booleanNode, we only see autocomplete options for boolean nodes.

当我们在IDE中在booleanNode之后按“.”键时,我们只看到布尔节点的自动完成选项。

3.2. Text Node

3.2 文本节点

When we’re asserting text nodes, we can use isText to compare using an exact value. Alternatively, we can use textContains to assert a substring:

当我们断言文本节点时,我们可以使用isText来比较使用一个精确的值。或者,我们可以使用textContains来断言一个子串。

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

We can also use regular expressions via matches:

我们还可以通过matches使用regular expressions

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

This example asserts that the name starts with a capital letter.

这个例子断言,name以大写字母开头。

3.3. Number Node

3.3.数字节点

For number nodes, the DSL provides some useful numeric comparisons:

对于数字节点,DSL提供了一些有用的数字比较。

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

We can also specify the Java numeric type we’re expecting:

我们还可以指定我们所期望的Java数字类型。

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

The isEqualTo method is reserved for whole tree matching, so for comparing numeric equality, we use isNumberEqualTo:

isEqualTo方法是保留给整棵树匹配的,所以对于比较数字的平等性,我们使用isNumberEqualTo

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

3.4. Array Node

3.4.阵列节点

We can test the contents of an array with isArrayContaining:

我们可以用isArrayContaining测试一个数组的内容。

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

This tests for the presence of the given values and allows the actual array to contain additional items. If we wish to assert a more exact match, we can use isArrayContainingExactlyInAnyOrder:

这测试了给定值的存在,并允许实际数组包含额外的项目。如果我们希望断言一个更精确的匹配,我们可以使用isArrayContainingExactlyInAnyOrder

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

We can also make this require the exact order:

我们也可以使这需要准确的顺序。

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

This is a good technique for asserting the contents of arrays that contain primitive values. Where an array contains objects, we may wish to use isEqualTo instead.

这是一个很好的技术,用于断言包含原始值的数组的内容。如果数组包含对象,我们可能希望使用isEqualTo代替。

4. Whole Tree Matching

4.整树匹配

While we can construct assertions with multiple field-specific conditions to check out what’s in the JSON document, we often need to compare a whole document against another.

虽然我们可以用多个字段的特定条件构建断言来检查JSON文档中的内容,但我们经常需要将整个文档与另一个文档进行比较。

The isEqualTo method (or isNotEqualTo) is used to compare the whole tree. This can be combined with at to move to a subtree of the actual before making the comparison:

isEqualTo方法(或isNotEqualTo)是用来比较整个树。这可以与at结合起来,在进行比较之前移动到实际的一个子树上。

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

Whole tree comparison can hit problems when the JSON contains data that is either:

当JSON包含的数据为以下两种情况时,整棵树的比较会遇到问题。

  • the same, but in a different order
  • comprised of some values that cannot be predicted

The where a method is used to customize the next isEqualTo operation to get around these.

where a方法用于定制下一个isEqualTo操作以绕过这些。

4.1. Add Key Order Constraint

4.1.添加键序约束

Let’s look at two JSON documents that seem the same:

让我们看看两个看起来相同的JSON文档。

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

We should note that this isn’t strictly JSON format. ModelAssert allows us to use the JavaScript notation for JSON, as well as the wire format that usually quotes the field names.

我们应该注意,这并不是严格意义上的JSON格式。ModelAssert允许我们使用JSON的JavaScript符号,以及通常引用字段名的wire格式。

These two documents have exactly the same keys underneath “a”, but they’re in a different order. An assertion of these would fail, as ModelAssert defaults to strict key order.

这两个文档在“a”下面有完全相同的键,但它们的顺序不同。对这些的断言会失败,因为ModelAssert默认为严格的键序

We can relax the key order rule by adding a where configuration:

我们可以通过添加一个where配置来放松键序规则。

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

This allows any object in the tree to have a different order of keys from the expected document and still match.

这允许树中的任何对象具有与预期文件不同的键的顺序,但仍然可以匹配。

We can localize this rule to a specific path:

我们可以将这个规则定位到一个特定的路径。

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

This limits the keysInAnyOrder to just the “a” field in the root object.

这就把keysInAnyOrder限制在根对象的“a”字段。

The ability to customize the comparison rules allows us to handle many scenarios where the exact document produced cannot be fully controlled or predicted.

自定义比较规则的能力使我们能够处理许多无法完全控制或预测所产生的确切文件的情况

4.2. Relaxing Array Constraints

4.2.放宽阵列约束

If we have arrays where the order of values can vary, then we can relax the array ordering constraint for the whole comparison:

如果我们的数组的值的顺序可以变化,那么我们可以放松整个比较的数组排序约束。

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

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

Or we can limit that constraint to a path, as we did with keysInAnyOrder.

或者我们可以把这个约束限制在一个路径上,就像我们对keysInAnyOrder所做的那样。

4.3. Ignoring Paths

4.3.忽略路径

Maybe our actual document contains some fields that are either uninteresting or unpredictable. We can add a rule to ignore that path:

也许我们的实际文件包含一些无趣的或不可预测的字段。我们可以添加一个规则来忽略这个路径。

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

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

We should note that the path we’re expressing is always in terms of the JSON Pointer within the actual.

我们应该注意到,我们所表达的路径是总是以实际中的JSON Pointer为单位

The extra field “url” in the actual is now ignored.

实际中的额外字段“url”现在被忽略了。

4.4. Ignore Any GUID

4.4.忽略任何GUID

So far, we’ve only added rules using at in order to customize comparison at specific locations in the document.

到目前为止,我们只添加了使用at的规则,以便在文档的特定位置进行自定义比较。

The path syntax allows us to describe where our rules apply using wildcards. When we add an at or path condition to the where of our comparison, we can also provide any of the field assertions from above to use in place of a side-by-side comparison with the expected document.

path 语法允许我们使用通配符来描述我们的规则适用的地方。当我们在比较的where中添加atpath条件时,我们也可以提供上面的任何字段断言,以代替与预期文档的并排比较。

Let’s say we had an id field that appeared in multiple places in our document and was a GUID that we couldn’t predict.

假设我们有一个id字段,它出现在我们文档的多个地方,是一个我们无法预测的GUID。

We could ignore this field with a path rule:

我们可以用一个路径规则忽略这个字段。

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);

Here, our expected value could have anything for the id field because we’ve simply ignored any field whose JSON Pointer starts “/user/credentials” then has a single node (the array index) and ends in “/id”.

在这里,我们对id字段的预期值可能是任何东西,因为我们简单地忽略了任何JSON指针开始为“/user/credentials”,然后有一个节点(数组索引)并以“/id”结束的字段。

4.5. Match Any GUID

4.5.匹配任何GUID

Ignoring fields we can’t predict is one option. It’s better instead to match those nodes by type, and maybe also by some other condition they must meet. Let’s switch to forcing those GUIDs to match the pattern of a GUID, and let’s allow the id node to appear at any leaf node of the tree:

忽略我们无法预测的字段是一种选择。更好的办法是通过类型来匹配这些节点,也许还可以通过其他一些他们必须满足的条件。让我们改成强迫那些GUID与GUID的模式相匹配,让我们允许id节点出现在树的任何叶子节点上。

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

The ANY_SUBTREE wildcard matches any number of nodes between parts of the path expression. The GUID_PATTERN comes from the ModelAssert Patterns class, which contains some common regular expressions to match things like numbers and date stamps.

ANY_SUBTREE通配符匹配路径表达式各部分之间的任何数量的节点。GUID_PATTERN来自ModelAssert的Patterns类,它包含一些常见的正则表达式来匹配数字和日期戳等东西。

4.6. Customizing isEqualTo

4.6.定制isEqualTo

The combination of where with either path or at expressions allows us to override comparisons anywhere in the tree. We either add the built-in rules for an object or array matching or specify specific alternative assertions to use for individual or classes of paths within the comparison.

wherepathat表达式的组合使我们能够在树的任何地方覆盖比较。我们可以添加对象或数组匹配的内置规则,或者指定特定的替代断言,以用于比较中的单个或类别的路径。

Where we have a common configuration, reused across various comparisons, we can extract it into a method:

如果我们有一个共同的配置,在各种比较中重复使用,我们可以把它提取到一个方法中。

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

Then, we can add that configuration to a particular assertion with configuredBy:

然后,我们可以用configuredBy将该配置添加到一个特定的断言中。

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

5. Compatibility with Other Libraries

5.与其他图书馆的兼容性

ModelAssert was built for interoperability. So far, we’ve seen the AssertJ style assertions. These can have multiple conditions, and they will fail on the first condition that’s not met.

ModelAssert是为互操作性而建立的。到目前为止,我们已经看到了AssertJ风格的断言。这些断言可以有多个条件,并且它们将在第一个未满足的条件下失败。

However, sometimes we need to produce a matcher object for use with other types of tests.

然而,有时我们需要产生一个匹配器对象,供其他类型的测试使用。

5.1. Hamcrest Matcher

5.1.哈姆克雷斯特匹配器

Hamcrest is a major assertion helper library supported by many tools. We can use the DSL of ModelAssert to produce a Hamcrest matcher:

Hamcrest是一个由许多工具支持的主要断言辅助库。我们可以使用ModelAssert的DSL来产生一个Hamcrest匹配器

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

The json method is used to describe a matcher that will accept a String with JSON data in it. We could also use jsonFile to produce a Matcher that expects to assert the contents of a File. The JsonAssertions class in ModelAssert contains multiple builder methods like this to start building a Hamcrest matcher.

json方法用于描述一个匹配器,它将接受一个包含JSON数据的String。我们也可以使用jsonFile来产生一个Matcher,它期望断定File的内容。ModelAssert中的JsonAssertions类包含多个构建器方法,像这样开始构建一个Hamcrest匹配器。

The DSL for expressing the comparison is identical to assertJson, but the comparison isn’t executed until something uses the matcher.

表达比较的DSL与assertJson相同,但比较在使用匹配器的情况下才会执行。

We can, therefore, use ModelAssert with Hamcrest’s MatcherAssert:

因此,我们可以使用ModelAssert与Hamcrest的MatcherAssert

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

5.2. Using With Spring Mock MVC

5.2.使用Spring Mock MVC

While using response body verification in Spring Mock MVC, we can use Spring’s built-in jsonPath assertions. However, Spring also allows us to use Hamcrest matchers to assert the string returned as response content. This means we can perform sophisticated content assertions using ModelAssert.

在使用Spring Mock MVC中的响应体验证时,我们可以使用Spring的内置jsonPath断言。但是,Spring还允许我们使用Hamcrest匹配器来断言作为响应内容返回的字符串。这意味着我们可以使用 ModelAssert 执行复杂的内容断言。

5.3. Use With Mockito

5.3.与Mockito一起使用

Mockito is already interoperable with Hamcrest. However, ModelAssert also provides a native ArgumentMatcher. This can be used both to set up the behavior of stubs and to verify calls to them:

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()));
}

In this example, the Mockito argThat is used in both the setup of a mock and the verify. Inside that, we use the Hamcrest style builder for the matcher – json. Then we add conditions to it, converting to Mockito’s ArgumentMatcher at the end with toArgumentMatcher.

在这个例子中,Mockito的argThat被用于模拟的设置和验证。在这里面,我们使用Hamcrest风格的构建器作为匹配器 – json。然后我们向它添加条件,在最后用toArgumentMatcher转换为Mockito的ArgumentMatcher

6. Conclusion

6.结语

In this article, we looked at the need to compare JSON semantically in our tests.

在这篇文章中,我们研究了在测试中对JSON进行语义比较的必要性。

We saw how ModelAssert can be used to build an assertion on individual nodes within a JSON document as well as whole trees. Then we saw how to customize tree comparison to allow for unpredictable or irrelevant differences.

我们看到ModelAssert如何被用来对JSON文档中的单个节点以及整个树建立断言。然后我们看到如何定制树的比较,以允许不可预测或不相关的差异。

Finally, we saw how to use ModelAssert with Hamcrest and other libraries.

最后,我们看到了如何用Hamcrest和其他库来使用ModelAssert。

As always, the example code from this tutorial is available over on GitHub.

一如既往,本教程中的示例代码可在GitHub上获取。