1. 概述
當編寫用於使用 JSON 的軟件的自動化測試時,我們經常需要將 JSON 數據與預期值進行比較。
在某些情況下,我們可以將實際的 JSON 和預期 JSON 視為字符串,並執行字符串比較,但這存在許多限制。
在本教程中,我們將探討如何使用 ModelAssert 編寫 JSON 值之間的斷言和比較。我們將看到如何在 JSON 文檔中的單個值上構造斷言,以及如何比較文檔。我們還將涵蓋如何處理無法預測的精確值,例如日期或 GUID。
2. 入門
ModelAssert 是一種數據斷言庫,其語法類似於 AssertJ,並且具有與 JSONAssert 相當的功能。它基於 Jackson 進行 JSON 解析,並使用 JSON Pointer 表達式來描述文檔中字段的路徑。
讓我們先編寫一些簡單的斷言來驗證此 JSON:
{
"name": "Baeldung",
"isOnline": true,
"topics": [ "Java", "Spring", "Kotlin", "Scala", "Linux" ]
}2.1. 依賴
首先,我們向我們的<em>pom.xml</em>中添加 ModelAssert:
<dependency>
<groupId>uk.org.webcompere</groupId>
<artifactId>model-assert</artifactId>
<version>1.0.0</version>
<scope>test</scope>
</dependency>2.2. 驗證 JSON 對象中的字段
假設示例 JSON 已作為字符串返回給我們,並且我們想要檢查 name 字段是否等於 Baeldung:
assertJson(jsonString)
.at("/name").isText("Baeldung");assertJson 方法將從各種來源讀取 JSON 數據,包括 String、File、Path 以及 Jackson 的 JsonNode。返回的對象是一個斷言,我們可以使用流暢 DSL(領域特定語言)添加條件。
at 方法描述了我們在文檔中希望進行字段斷言的位置。然後,isText 指定我們期望一個值為 Baeldung 的文本節點。
我們可以通過使用更長的 JSON 指針表達式來斷言 topics 數組中的路徑:
assertJson(jsonString)
.at("/topics/1").isText("Spring");雖然我們可以逐個編寫字段斷言,我們也可以將它們組合成一個單一的斷言:
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. 語義比較樹結構
要進行整個文檔的比較,可以使用 。
assertJson(jsonString)
.isEqualTo(EXPECTED_JSON_PATH);在此情況下,實際的JSON字符串由 assertJson 加載,期望的JSON文檔——一個通過 Path 描述的文件——則在 isEqualTo 中加載。比較基於數據。
2.5. 不同格式
ModelAssert 還支持通過 Jackson 轉換為 JsonNode 以及 yaml 格式的 Java 對象。
Map<String, String> map = new HashMap<>();
map.put("name", "baeldung");
assertJson(map)
.isEqualToYaml("name: baeldung");對於 <em yaml</em> 處理,使用 <em isEqualToYaml</em> 方法來指示字符串或文件的格式。如果源數據是 <em yaml</em>,則需要使用 <em assertYaml</em>:
assertYaml("name: baeldung")
.isEqualTo(map);3. 字段斷言
我們已經看到了一些基本的斷言。現在,讓我們更深入地瞭解DSL。
3.1. 任意節點斷言
DSL 中的 ModelAssert 允許針對樹中的幾乎任何節點添加幾乎任何條件。這是因為 JSON 樹可能包含任何類型的節點,並且這些節點可以在樹中的任何級別上出現。
下面我們來看一下我們可能添加到示例 JSON 的根節點上的斷言:
assertJson(jsonString)
.isNotNull()
.isNotNumber()
.isObject()
.containsKey("name");由於斷言對象的接口上提供了這些方法,我們的IDE會在我們按下“.”鍵時,立即提示我們可以添加的各種斷言。
在這個例子中,由於最後一個條件已經暗示了非空對象,我們添加了大量的冗餘條件。
通常情況下,我們使用根節點上的JSON Pointer表達式來對樹中較低級別的節點執行斷言。
assertJson(jsonString)
.at("/topics").hasSize(5);此斷言使用 hasSize 檢查 topic 字段中的數組是否包含五個元素。 hasSize 方法作用於對象、數組和字符串。對象的尺寸是其鍵的數量,字符串的尺寸是其字符數量,數組的尺寸是其元素數量。
我們大多數需要對字段進行斷言的需求都取決於字段的精確類型。我們可以使用 number、array、text、booleanNode 和 object 方法,以便在嘗試對特定類型進行斷言時,進入更具體的斷言子集中。這是一種可選但更具表現力的做法:
assertJson(jsonString)
.at("/isOnline").booleanNode().isTrue();當我們按下 IDE 中 “.” 鍵後,在 booleanNode 之後,我們只會看到 boolean 節點的自動補全選項。
3.2. Text 節點
當我們在斷言 Text 節點時,可以使用 <em >isText</em> 進行精確值比較。 此外,還可以使用 <em >textContains</em> 來斷言子字符串:
assertJson(jsonString)
.at("/name").textContains("ael");我們還可以通過 <em matches 來使用正則表達式:
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. 數組節點
我們可以使用 <em isArrayContaining</em> 來測試數組的內容。
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 文檔的內容,但我們經常需要將整個文檔與另一個文檔進行比較。
<em>isEqualTo</em> 方法(或 <em>isNotEqualTo</em>)用於比較整個樹。 它可以與 <em>at</em> 方法結合使用,以移動到實際樹的子樹中,然後再進行比較:
assertJson(jsonString)
.at("/topics")
.isEqualTo("[ \"Java\", \"Spring\", \"Kotlin\", \"Scala\", \"Linux\" ]");整棵樹比較在以下情況時可能會遇到問題:JSON 包含的數據是:
- 相同但順序不同
- 包含一些無法預測的值
通過使用一種方法來定製下一次 isEqualTo 操作,在where 處解決這些問題。
4.1. 添加鍵順序約束
以下是兩個看似相同的 JSON 文檔:
String actualJson = "{a:{d:3, c:2, b:1}}";
String expectedJson = "{a:{b:1, c:2, d:3}}";我們應該注意的是,這並非嚴格的JSON格式。ModelAssert 允許我們使用 JavaScript 語法進行 JSON 表示,以及通常會引用字段名稱的 wire 格式。
這兩個文檔在“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
我們之前僅使用 <em at</em> 來自定義特定文檔位置的比較規則。
<em path</em> 語法允許我們使用通配符描述規則應用的位置。當我們為比較的 <em where</em> 添加 <em at</em> 或 <em path</em> 條件時,我們還可以提供上面列出的任何字段斷言,以替代與預期文檔的並排比較。
假設我們有一個 <em id</em> 字段出現在文檔中的多個位置,並且這是一個我們無法預測的 GUID。
我們可以使用路徑規則忽略此字段:
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” 開頭,然後包含單個節點(數組索引)並以 “/id” 結尾的情況。
4.5. 匹配任意 GUID
忽略無法預測的字段是一種選擇。 最好通過類型以及其他必須滿足的條件來匹配這些節點。 讓我們強制 GUID 匹配 GUID 的模式,並且允許 節點出現在樹的任何葉節點上:
assertJson(actualJson)
.where()
.path(ANY_SUBTREE, "id").matches(GUID_PATTERN)
.isEqualTo(expectedJson);通配符 ANY_SUBTREE 可以匹配路徑表達式中部分之間任意數量的節點。 GUID_PATTERN 來自 ModelAssert 的 Patterns 類,該類包含一些用於匹配數字和日期戳的常見正則表達式。
4.6. 自定義 isEqualTo 方法
使用 `where` 表達式與 `path` 或 `at` 表達式的組合,允許我們在樹的任何位置覆蓋比較。我們既可以為對象或數組匹配添加內置規則,也可以為單個路徑或路徑類別指定特定的替代斷言,用於比較。
當存在常見的配置,並在各種比較中使用時,我們可以將其提取成一個方法:
private static <T> WhereDsl<T> idsAreGuids(WhereDsl<T> where) {
return where.path(ANY_SUBTREE, "id").matches(GUID_PATTERN);
}然後,我們可以將該配置添加到特定的斷言中,使用 configuredBy 選項:
assertJson(actualJson)
.where()
.configuredBy(where -> idsAreGuids(where))
.isEqualTo(expectedJson);5. 與其他庫的兼容性
ModelAssert 的設計目標是實現與其他庫的互操作性。我們之前已經看到了使用 AssertJ 風格斷言的情況。這些斷言可以包含多個條件,並且 只要有一個條件未滿足,就會失敗。
然而,有時我們需要生成匹配器對象,以便與其它類型的測試一起使用。
5.1. Hamcrest 匹配器
Hamcrest 是許多工具支持的主要斷言輔助庫。 我們可以使用 ModelAssert 的 DSL (領域特定語言) 來生成一個 Hamcrest 匹配器:
Matcher<String> matcher = json()
.at("/name").hasValue("Baeldung");json 方法用於描述一個匹配器,該匹配器將接受包含 JSON 數據的字符串。我們還可以使用jsonFile來生成一個匹配器,該匹配器期望斷言文件的內容。JsonAssertions 類在 ModelAssert 中包含多個構建方法,如此,用於開始構建一個 Hamcrest 匹配器。
表達比較的 DSL 與assertJson 相同,但比較只有在某些東西使用匹配器時才會執行。
因此,我們可以使用 ModelAssert 與 Hamcrest 的MatcherAssert 結合使用:
MatcherAssert.assertThat(jsonString, json()
.at("/name").hasValue("Baeldung")
.at("/topics/1").isText("Spring"));5.2. 使用 With Spring Mock MVC
在使用 Spring Mock MVC 中的響應體驗證時,我們可以使用 Spring 內置的 <em >jsonPath</em> 斷言。但是,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 被用於 mock 的設置以及 verify 的驗證。其中,我們使用 Hamcrest 風格的構建器作為匹配器,即 json。然後我們為它添加了條件,最終將其轉換為 Mockito 的 ArgumentMatcher,使用 toArgumentMatcher。
6. 結論
在本文中,我們探討了在測試中對 JSON 進行語義比較的需求。
我們瞭解到 ModelAssert 可以用於構建在 JSON 文檔中的單個節點和整個樹結構上的斷言。 此外,我們還學習瞭如何自定義樹的比較,以允許存在不可預測或不相關的差異。
最後,我們學習瞭如何使用 ModelAssert 與 Hamcrest 和其他庫結合使用。