JSON Path 是一種 JSON 文件查詢語言,其靈感來自 XPath 為 XML 文件提供的功能。它最初由 Matt Goëssner 提出,現在已成為 IETF 規範:RFC 9535。我強大的同事提出用 JsonPath 來比對特定的欄位,測試步驟讀起來清晰,復用性也高,實作起來也是蠻簡單的,接下來就看看我們怎麼做的。
開發環境
- .NET 8
- JsonPath.Net 1.1.2
- SystemTextJson.JsonDiffPatch.Xunit 2.0.0
JsonPath的使用方法
參考
JSONPath - XPath for JSON (goessner.net)
GitHub - atifaziz/JSONPath: JSONPath (XPath-like syntax for JSON) C# implementation
範例
GitHub - atifaziz/JSONPath: JSONPath (XPath-like syntax for JSON) C# implementation
實作
在 Specflow 測試步驟裡的描述,可以使用 table 來比對多個欄位
Scenario: 用 Table 驗證資料
When 調用端發送 "Get" 請求至 "api/member"
Then 預期得到回傳 Member 結果為
| Age | Birthday |
| 18 | 1/1/2000 12:00:00 AM +00:00 |
Then 預期得到回傳 Member.FullName 結果為
| FirstName | LastName |
| John | Doe |
或是用 JsonDiff 比對 Json 物件
Scenario: 用 JsonDiff 驗證資料
When 調用端發送 "Get" 請求至 "api/member"
Then 預期回傳內容為
"""
{
"Id": 1,
"Age": 18,
"Birthday": "2000-01-01T00:00:00+00:00",
"FullName": {
"FirstName": "John",
"LastName": "Doe"
}
}
"""
模擬呼叫 API,回傳了一個 Member 物件
[When(@"調用端發送 ""(.*)"" 請求至 ""(.*)""")]
public void When調用端發送請求至(string httpMethod, string url)
{
var data = new Member
{
Id = 1,
Age = 18,
Birthday = new DateTimeOffset(2000, 1, 1, 0, 0, 0, TimeSpan.Zero),
FullName = new FullName
{
FirstName = "John",
LastName = "Doe"
}
};
this.ScenarioContext.Set(data);
}
當想要特別強調某個欄位的狀態時,必須針對不同的型別寫不同的步驟,實作雖然簡單但是步驟卻無法重複使用
Then 預期得到回傳 Member.FullName.FirstName 結果為 John
當用了JsonPath 查詢語法後,實作起來就變得簡單多了,要比對的是強型別物件只要先序列化即可
Scenario: 用 JsonPath 驗證資料
When 調用端發送 "Get" 請求至 "api/member"
Then 預期回傳內容中路徑 "$.Age" 的數值等於 "18"
Then 預期回傳內容中路徑 "$.Birthday" 的時間等於 "2000-01-01T00:00:00+00:00"
Then 預期回傳內容中路徑 "$.FullName.FirstName" 的字串等於 "John"
Then 預期回傳內容中路徑 "$.FullName.LastName" 的字串等於 "Doe"
注意:JsonPath 雖然好用但也要注意案例的可讀性,例如不要使用 Regex Expression 正則表達式,這個只要遵守團隊的規範即可,像我們講好只能使用欄位表達式,$.FullName.FirstName、$.Members[0].FullName.FirstName
[Then(@"預期回傳內容中路徑 ""(.*)"" 的(" + OperationTypes + @") ""(.*)""")]
public void Then預期回傳內容中路徑的字串等於(string selector, string operationType, string expected)
{
var data = this.ScenarioContext.Get<Member>();
var json = JsonSerializer.Serialize(data);
ContentShouldBe(json, selector, operationType, expected);
}
OperationTypes 是 Specflow 正則表達式的用法
private const string StringEquals = "字串等於";
private const string NumberEquals = "數值等於";
private const string BoolEquals = "布林值等於";
private const string JsonEquals = "Json等於";
private const string DateTimeEquals = "時間等於";
private const string OperationTypes = StringEquals
+ "|" + NumberEquals
+ "|" + BoolEquals
+ "|" + JsonEquals
+ "|" + DateTimeEquals;
private static void ContentShouldBe(string content, string selectPath, string operationType, string expected)
{
var srcInstance = JsonNode.Parse(content);
var jsonPath = JsonPath.Parse(selectPath);
switch (operationType)
{
case StringEquals:
{
var actual = jsonPath.Evaluate(srcInstance).Matches.FirstOrDefault()?.Value?.GetValue<string>();
var errorReason =
$"{nameof(operationType)}: [{operationType}], {nameof(selectPath)}: [{selectPath}], {nameof(expected)}: [{expected}], {nameof(actual)}: [{actual}]";
(actual ?? string.Empty).Should().Be(expected, errorReason);
break;
}
case NumberEquals:
{
var actual = jsonPath.Evaluate(srcInstance).Matches.FirstOrDefault()?.Value?.GetValue<int>();
var errorReason =
$"{nameof(operationType)}: [{operationType}], {nameof(selectPath)}: [{selectPath}], {nameof(expected)}: [{expected}], {nameof(actual)}: [{actual}]";
actual.Should().Be(int.Parse(expected), errorReason);
break;
}
case BoolEquals:
{
var actual = jsonPath.Evaluate(srcInstance).Matches.FirstOrDefault()?.Value?.GetValue<bool>();
var errorReason =
$"{nameof(operationType)}: [{operationType}], {nameof(selectPath)}: [{selectPath}], {nameof(expected)}: [{expected}], {nameof(actual)}: [{actual}]";
actual.Should().Be(bool.Parse(expected), errorReason);
break;
}
case DateTimeEquals:
{
var expect = DateTimeOffset.Parse(expected);
var actual = jsonPath.Evaluate(srcInstance).Matches.FirstOrDefault()
?.Value
?.GetValue<DateTimeOffset>()
;
var errorReason =
$"{nameof(operationType)}: [{operationType}], {nameof(selectPath)}: [{selectPath}], {nameof(expected)}: [{expect}], {nameof(actual)}: [{actual}]";
actual.Should().Be(expect, errorReason);
break;
}
case JsonEquals:
{
var actual = jsonPath.Evaluate(srcInstance).Matches.FirstOrDefault()?.Value;
var expect = string.IsNullOrWhiteSpace(expected) ? null : JsonNode.Parse(expected);
var diff = actual.Diff(expect);
var errorReason =
$"{nameof(operationType)}: [{operationType}], {nameof(selectPath)}: [{selectPath}], {nameof(expected)}: [{expected}], {nameof(actual)}: [{actual?.ToJsonString()}], diff: [{diff?.ToJsonString()}]";
actual.DeepEquals(expect).Should().BeTrue(errorReason);
break;
}
}
}
範例位置
若有謬誤,煩請告知,新手發帖請多包涵
Microsoft MVP Award 2010~2017 C# 第四季
Microsoft MVP Award 2018~2022 .NET