From zenbu-powers
當在 Gherkin 測試中驗證「Query 回傳結果」時,「只能」使用此指令。 讀取 _ctx["LastResponse"] 驗證 HTTP Response Body。
npx claudepluginhub zenbuapps/zenbu-powers --plugin zenbu-powersThis skill uses the workspace's default tool permissions.
讀取 When 階段儲存的 `HttpResponseMessage`,驗證 Query API 回傳的 Response Body 內容。
Searches, retrieves, and installs Agent Skills from prompts.chat registry using MCP tools like search_skills and get_skill. Activates for finding skills, browsing catalogs, or extending Claude.
Checks Next.js compilation errors using a running Turbopack dev server after code edits. Fixes actionable issues before reporting complete. Replaces `next build`.
Guides code writing, review, and refactoring with Karpathy-inspired rules to avoid overcomplication, ensure simplicity, surgical changes, and verifiable success criteria.
Share bugs, ideas, or general feedback.
讀取 When 階段儲存的 HttpResponseMessage,驗證 Query API 回傳的 Response Body 內容。
| 項目 | 技術 |
|---|---|
| Language | C# 12 / .NET 8+ |
| BDD | SpecFlow 3.9+ |
| HTTP | WebApplicationFactory + HttpClient |
| JSON | System.Text.Json |
| Assertion | FluentAssertions 6+ |
Then 語句緊跟在 Query When 之後,驗證 API 回傳的資料內容。
識別規則:
@query / @readmodel 場景綁定| readmodel-then | aggregate-then | |
|---|---|---|
| 驗證對象 | HTTP Response Body | DB 持久化狀態 |
| 資料來源 | _ctx["LastResponse"] | AppDbContext 查詢 |
| 對應的 When | query handler | command handler |
_ctx 取出儲存的 HttpResponseMessage(key: "LastResponse")await response.Content.ReadAsStringAsync() 取得 body 字串JsonSerializer.Deserialize<JsonElement>(body) 解析 JSONdata / items 欄位下)api.yml response schema)using System.Text.Json;
using FluentAssertions;
using TechTalk.SpecFlow;
namespace ProjectName.IntegrationTests.Steps.Lesson.ReadModelThen;
[Binding]
public class LessonProgressReadModelSteps
{
private readonly ScenarioContext _ctx;
public LessonProgressReadModelSteps(ScenarioContext ctx) => _ctx = ctx;
}
[Then(@"查詢結果應包含進度 (.*),狀態為 ""(.*)""")]
public async Task ThenResultContains(int progress, string status)
{
var response = _ctx.Get<HttpResponseMessage>("LastResponse");
var body = await response.Content.ReadAsStringAsync();
var data = JsonSerializer.Deserialize<JsonElement>(body);
var statusMap = new Dictionary<string, string>
{
["進行中"] = "IN_PROGRESS",
["已完成"] = "COMPLETED",
["未開始"] = "NOT_STARTED"
};
var expectedStatus = statusMap.GetValueOrDefault(status, status);
data.GetProperty("progress").GetInt32().Should().Be(progress,
"預期進度 {0}", progress);
data.GetProperty("status").GetString().Should().Be(expectedStatus,
"預期狀態 {0}", expectedStatus);
}
[Then(@"查詢結果應包含 (.*) 筆記錄")]
public async Task ThenResultCount(int expectedCount)
{
var response = _ctx.Get<HttpResponseMessage>("LastResponse");
var body = await response.Content.ReadAsStringAsync();
var data = JsonSerializer.Deserialize<JsonElement>(body);
// 處理常見 envelope:{"data": [...]} 或 {"items": [...]}
var items = ExtractArray(data);
items.GetArrayLength().Should().Be(expectedCount);
}
private static JsonElement ExtractArray(JsonElement root)
{
if (root.ValueKind == JsonValueKind.Array) return root;
if (root.TryGetProperty("data", out var d) && d.ValueKind == JsonValueKind.Array) return d;
if (root.TryGetProperty("items", out var i) && i.ValueKind == JsonValueKind.Array) return i;
if (root.TryGetProperty("results", out var r) && r.ValueKind == JsonValueKind.Array) return r;
throw new InvalidOperationException("Response 不含可識別的陣列欄位(data / items / results)");
}
[Then(@"查詢結果應包含以下記錄:")]
public async Task ThenResultContainsRecords(Table table)
{
var response = _ctx.Get<HttpResponseMessage>("LastResponse");
var body = await response.Content.ReadAsStringAsync();
var data = JsonSerializer.Deserialize<JsonElement>(body);
var items = ExtractArray(data);
items.GetArrayLength().Should().BeGreaterThanOrEqualTo(table.RowCount,
"Response 陣列長度 {0},Gherkin 預期至少 {1}", items.GetArrayLength(), table.RowCount);
foreach (var row in table.Rows)
{
var matched = items.EnumerateArray().Any(item =>
table.Header.All(header =>
item.TryGetProperty(header, out var prop) &&
prop.ToString() == row[header]
));
matched.Should().BeTrue(
"找不到符合 Gherkin row 的項目: {0}",
string.Join(", ", table.Header.Select(h => $"{h}={row[h]}")));
}
}
[Then(@"用戶 ""(.*)"" 的購物車總金額應為 (.*)")]
public async Task ThenCartTotalShouldBe(string userName, decimal expectedTotal)
{
var response = _ctx.Get<HttpResponseMessage>("LastResponse");
var body = await response.Content.ReadAsStringAsync();
var data = JsonSerializer.Deserialize<JsonElement>(body);
// 假設 response shape: { "summary": { "total": 1234.5 } }
var total = data.GetProperty("summary").GetProperty("total").GetDecimal();
total.Should().Be(expectedTotal);
}
[Then(@"查詢結果不應包含 ""(.*)"" 欄位")]
public async Task ThenResultNotContainField(string fieldName)
{
var response = _ctx.Get<HttpResponseMessage>("LastResponse");
var body = await response.Content.ReadAsStringAsync();
var data = JsonSerializer.Deserialize<JsonElement>(body);
data.TryGetProperty(fieldName, out _).Should().BeFalse(
"Response 不應含有 {0} 欄位(敏感欄位過濾)", fieldName);
}
// 整數
var id = data.GetProperty("id").GetInt32();
var count = data.GetProperty("count").GetInt64();
// 字串
var name = data.GetProperty("name").GetString();
// 小數
var price = data.GetProperty("price").GetDecimal();
// 布林
var isActive = data.GetProperty("isActive").GetBoolean();
// 可選欄位(Safe)
var description = data.TryGetProperty("description", out var desc)
? desc.GetString() : null;
C# / ASP.NET Core 預設 JSON 序列化為 camelCase(lessonId, newLeadsThisMonth)。api.yml 中的 schema 欄位名應保持一致。
// ✅ camelCase(與 api.yml 一致)
data.GetProperty("lessonId").GetInt32()
// ❌ snake_case(.NET 預設不會這樣序列化)
data.GetProperty("lesson_id").GetInt32()
若後端明確使用 snake_case(透過 JsonNamingPolicy.SnakeCaseLower),測試中也要對應。
_ctx["LastResponse"],不重新發請求api.yml 為 SSOT{"data": [...]},需先解包GetInt32() / GetString() 等,不要用字串 parseKeyNotFoundExceptionapi.yml response schemas 完全一致