From zenbu-powers
當在 Gherkin 測試中驗證「Aggregate 最終狀態」,務必參考此規範。 使用 EF Core DbContext 查詢並驗證資料庫狀態。
npx claudepluginhub zenbuapps/zenbu-powers --plugin zenbu-powersThis skill uses the workspace's default tool permissions.
透過 EF Core DbContext 查詢資料庫,驗證 Aggregate 的最終狀態是否符合 Gherkin 描述。
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.
透過 EF Core DbContext 查詢資料庫,驗證 Aggregate 的最終狀態是否符合 Gherkin 描述。
| 項目 | 技術 |
|---|---|
| Language | C# 12 / .NET 8+ |
| BDD | SpecFlow 3.9+ |
| ORM | Entity Framework Core 8+ |
| Database | PostgreSQL 16(Testcontainers) |
| Assertion | FluentAssertions 6+ |
| Test Command | dotnet test --filter "Category!=Ignore" |
Then 語句驗證 Aggregate 的持久化狀態(DB 中的資料),而非 API 回傳結果。
識別規則:
| aggregate-then | readmodel-then | |
|---|---|---|
| 驗證對象 | 資料庫(透過 DbContext) | HTTP Response(_ctx["LastResponse"]) |
| 資料來源 | 重新查詢 DB | 已儲存的 HTTP Response |
| 適用情境 | Command 的副作用驗證 | Query 的回傳結果驗證 |
| 重新發 API | 否(直接查 DB) | 否(使用 When 儲存的 response) |
_ctx 取得 AppDbContextNotBeNull,再驗證屬性值using FluentAssertions;
using TechTalk.SpecFlow;
using ProjectName.Data;
using ProjectName.Models;
namespace ProjectName.IntegrationTests.Steps.Lesson.AggregateThen;
[Binding]
public class LessonProgressThenSteps
{
private readonly ScenarioContext _ctx;
public LessonProgressThenSteps(ScenarioContext ctx) => _ctx = ctx;
private Dictionary<string, object> Ids => _ctx.Get<Dictionary<string, object>>("Ids");
}
[Then(@"用戶 ""(.*)"" 在課程 (.*) 的進度應為 (.*)%")]
public void ThenProgressShouldBe(string userName, int lessonId, int expectedProgress)
{
var userId = Ids[userName].ToString()!;
var dbContext = _ctx.Get<AppDbContext>("DbContext");
// 清除 EF 快取,確保讀到資料庫最新值
dbContext.ChangeTracker.Clear();
var entity = dbContext.LessonProgresses
.FirstOrDefault(e => e.UserId == userId && e.LessonId == lessonId);
entity.Should().NotBeNull("找不到用戶 {0} 在課程 {1} 的進度紀錄", userName, lessonId);
entity!.Progress.Should().Be(expectedProgress,
"預期進度 {0}%,實際 {1}%", expectedProgress, entity.Progress);
}
[Then(@"用戶 ""(.*)"" 在課程 (.*) 的狀態應為 ""(.*)""")]
public void ThenStatusShouldBe(string userName, int lessonId, string expectedStatus)
{
var userId = Ids[userName].ToString()!;
var dbContext = _ctx.Get<AppDbContext>("DbContext");
dbContext.ChangeTracker.Clear();
var statusMap = new Dictionary<string, string>
{
["進行中"] = "IN_PROGRESS",
["已完成"] = "COMPLETED",
["未開始"] = "NOT_STARTED"
};
var mappedStatus = statusMap.GetValueOrDefault(expectedStatus, expectedStatus);
var entity = dbContext.LessonProgresses
.FirstOrDefault(e => e.UserId == userId && e.LessonId == lessonId);
entity.Should().NotBeNull("找不到課程進度");
entity!.Status.ToString().Should().Be(mappedStatus);
}
[Then(@"系統中應有以下購物車項目:")]
public void ThenCartItemsShouldBe(Table table)
{
var dbContext = _ctx.Get<AppDbContext>("DbContext");
dbContext.ChangeTracker.Clear();
foreach (var row in table.Rows)
{
var userId = Ids[row["userName"]].ToString()!;
var productId = row["productId"];
var expectedQuantity = int.Parse(row["quantity"]);
var entity = dbContext.CartItems
.FirstOrDefault(e => e.UserId == userId && e.ProductId == productId);
entity.Should().NotBeNull($"找不到用戶 {row["userName"]} 的商品 {productId}");
entity!.Quantity.Should().Be(expectedQuantity);
}
}
[Then(@"用戶 ""(.*)"" 的購物車中不應存在商品 ""(.*)""")]
public void ThenCartItemShouldNotExist(string userName, string productId)
{
var userId = Ids[userName].ToString()!;
var dbContext = _ctx.Get<AppDbContext>("DbContext");
dbContext.ChangeTracker.Clear();
var entity = dbContext.CartItems
.FirstOrDefault(e => e.UserId == userId && e.ProductId == productId);
entity.Should().BeNull($"預期商品 {productId} 已被刪除,但仍存在於資料庫中");
}
[Then(@"用戶 ""(.*)"" 的購物車中應有 (.*) 項商品")]
public void ThenCartShouldHaveItems(string userName, int expectedCount)
{
var userId = Ids[userName].ToString()!;
var dbContext = _ctx.Get<AppDbContext>("DbContext");
dbContext.ChangeTracker.Clear();
var count = dbContext.CartItems.Count(e => e.UserId == userId);
count.Should().Be(expectedCount);
}
// 單一 Key
entity = dbContext.Users.FirstOrDefault(e => e.Id == userId);
// 複合 Key(FirstOrDefault 彈性較高)
entity = dbContext.LessonProgresses
.FirstOrDefault(e => e.UserId == userId && e.LessonId == lessonId);
// 複合 Key via Find(需依 HasKey 順序傳入)
entity = dbContext.LessonProgresses.Find(userId, lessonId);
EF Core 會快取實體。若在同一 DbContext 中先寫入再查詢,可能讀到記憶體快取而非 DB 實際值。ChangeTracker.Clear() 強制後續查詢走 DB。
// ❌ 可能讀到過期快取
var entity = dbContext.LessonProgresses.FirstOrDefault(...);
// ✅ 確保讀到 DB 最新值
dbContext.ChangeTracker.Clear();
var entity = dbContext.LessonProgresses.FirstOrDefault(...);
另一個選擇是使用 AsNoTracking():
var entity = dbContext.LessonProgresses
.AsNoTracking()
.FirstOrDefault(e => e.UserId == userId && e.LessonId == lessonId);
NullReferenceException.Should().Be(..., "失敗時的訊息") 提升診斷清晰度.Should().Be() / .NotBeNull()ChangeTracker.Clear() 或使用 AsNoTracking()