From saurun
Use when implementing ASP.NET Core + EF Core features or bugfixes with xUnit + NSubstitute tests, before writing implementation code.
npx claudepluginhub fiatkongen/saurun-marketplace --plugin saurunThis skill uses the workspace's default tool permissions.
Mandatory TDD constraints for all .NET code. Violations must be fixed before committing.
Guides Next.js Cache Components and Partial Prerendering (PPR) with cacheComponents enabled. Implements 'use cache', cacheLife(), cacheTag(), revalidateTag(), static/dynamic optimization, and cache debugging.
Guides building MCP servers enabling LLMs to interact with external services via tools. Covers best practices, TypeScript/Node (MCP SDK), Python (FastMCP).
Generates original PNG/PDF visual art via design philosophy manifestos for posters, graphics, and static designs on user request.
Mandatory TDD constraints for all .NET code. Violations must be fixed before committing.
1. NO PRODUCTION CODE WITHOUT A FAILING TEST FIRST
2. NEVER add test-only methods to production classes
3. MOCK INFRASTRUCTURE ONLY — never domain
4. COMMIT after each green cycle
Write code before test? Delete it. git checkout . — not stash, not "reference."
| Step | Action | Verify |
|---|---|---|
| RED | Write one failing test | Fails for expected reason (missing feature, not typo) |
| GREEN | Write minimal code to pass | This test + all others pass |
| REFACTOR | Clean up, no new behavior | All tests still green |
| COMMIT | After each green cycle | git commit with passing tests |
Mock infrastructure with NSubstitute. NEVER mock domain.
| OK to mock (NSubstitute) | NEVER mock |
|---|---|
IRepository, DbContext | Domain entities |
IHttpClientFactory | Value objects |
ILogger<T>, TimeProvider | Pure functions |
| External API clients, message queues | Domain services with no I/O |
// OK: mocking infrastructure
var repo = Substitute.For<IOrderRepository>();
repo.GetByIdAsync(orderId).Returns(existingOrder);
// VIOLATION
var order = Substitute.For<Order>(); // Never mock domain entities
var order = Order.Create(customerId); // Use real entity
Substitute.For<>() on a domain entity/value object is always a violation. Use NSubstitute only — not Moq, not FakeItEasy.
Test behavior, not structure. Ask: "If this test didn't exist, what bug could ship?"
DO test: Public method behavior, edge cases, error paths, state transitions, integration (HTTP → response → DB state).
NEVER test: Getters/setters (CanSetName), constructor property assignment, framework behavior, private methods.
Integration test rule: Assert response body AND/OR DB state — not just HTTP status. response.EnsureSuccessStatusCode() alone is a violation.
MethodName_Scenario_ExpectedBehavior (e.g., AddItem_WithEmptyName_ReturnsFailure)[Theory] + [InlineData] for parameterized cases.CustomWebApplicationFactory shared via IClassFixture<> — never duplicate per test class.Check what the project uses, then pick the matching pattern.
Swap file-based → in-memory SQLite. Same SQL dialect, zero mismatch, no Docker needed. In-memory SQLite dies when the connection closes, so the connection MUST be kept alive.
public class CustomWebApplicationFactory : WebApplicationFactory<Program>
{
private readonly SqliteConnection _connection = new("DataSource=:memory:");
protected override void ConfigureWebHost(IWebHostBuilder builder)
{
_connection.Open();
builder.ConfigureServices(services =>
{
var descriptor = services.SingleOrDefault(
d => d.ServiceType == typeof(DbContextOptions<AppDbContext>));
if (descriptor != null) services.Remove(descriptor);
services.AddDbContext<AppDbContext>(o => o.UseSqlite(_connection));
var sp = services.BuildServiceProvider();
using var scope = sp.CreateScope();
scope.ServiceProvider.GetRequiredService<AppDbContext>().Database.EnsureCreated();
});
}
protected override void Dispose(bool disposing) { _connection.Dispose(); base.Dispose(disposing); }
}
Test against real PostgreSQL in Docker. Zero SQL dialect mismatch. Requires Testcontainers.PostgreSql NuGet.
public class CustomWebApplicationFactory : WebApplicationFactory<Program>, IAsyncLifetime
{
private readonly PostgreSqlContainer _postgres = new PostgreSqlBuilder()
.WithImage("postgres:16-alpine").Build();
protected override void ConfigureWebHost(IWebHostBuilder builder)
{
builder.ConfigureServices(services =>
{
var descriptor = services.SingleOrDefault(
d => d.ServiceType == typeof(DbContextOptions<AppDbContext>));
if (descriptor != null) services.Remove(descriptor);
services.AddDbContext<AppDbContext>(o => o.UseNpgsql(_postgres.GetConnectionString()));
var sp = services.BuildServiceProvider();
using var scope = sp.CreateScope();
scope.ServiceProvider.GetRequiredService<AppDbContext>().Database.EnsureCreated();
});
}
public async Task InitializeAsync() => await _postgres.StartAsync();
public async Task DisposeAsync() => await _postgres.DisposeAsync();
}
public class OrderTests : IClassFixture<CustomWebApplicationFactory>
{
private readonly HttpClient _client;
public OrderTests(CustomWebApplicationFactory factory) => _client = factory.CreateClient();
}
EF InMemory provider is a violation (doesn't enforce constraints, FK, or SQL behavior). Always use real database engine matching production.
tests/{Project}.Tests/
Unit/ # Domain + application service tests
Integration/ # HTTP endpoint tests via CustomWebApplicationFactory
Infrastructure/ # Shared fixtures (CustomWebApplicationFactory)
| Anti-Pattern | What it looks like | Fix |
|---|---|---|
| MockVerifyOnly | repo.Received(1).AddAsync(Arg.Any<Order>()) | Assert on response body, DB state, or domain state |
| GetterSetterTest | item.Name = "X"; Assert.Equal("X", item.Name) | Test behavior that uses the property |
| DomainMock | Substitute.For<Order>() | Use real domain entity |
| TestOnlyMethod | public void ResetState() only used in tests | Fresh instances + test helpers |
| IncompleteMock | new StoreDto { Id = id, Name = "Test" } missing fields | Include ALL fields with realistic values |
| DuplicateFactory | Copy-pasted WebApplicationFactory setup per class | Single CustomWebApplicationFactory + IClassFixture |
| StatusOnlyAssert | response.EnsureSuccessStatusCode() without body check | Assert response body AND/OR DB state |
# 1. Tests exist
ls tests/ 2>/dev/null | grep -q "." || echo "FAIL: no test project"
# 2. No Substitute.For on domain entities
grep -rn "Substitute.For<" tests/ --include="*.cs" | grep -iv "Repository\|IHttp\|ILogger\|IEmail\|IMessage\|INotif\|ICache\|IFile\|TimeProvider\|IConfiguration\|IService\|IMediator\|IDispatcher" && echo "WARN: check if mocking domain"
# 3. No getter/setter tests
grep -rn "CanSet\|CanGet\|HasDefault\|IsNullable" tests/ --include="*.cs" && echo "FAIL: getter/setter tests"
# 4. No test-only methods in production
grep -rn "ResetState\|ResetForTest\|SetForTest\|TestOnly" Domain/ Application/ --include="*.cs" 2>/dev/null && echo "FAIL: test-only methods in production"
# 5. No assertion-less tests
grep -B10 "^\s*}" tests/ -r --include="*.cs" | grep -B10 "\[Fact\]\|\[Theory\]" | grep -L "Assert\.\|\.Should\|Expect\|Verify" 2>/dev/null | head -3
# 6. Integration tests assert body/DB, not just status
grep -A5 "EnsureSuccessStatusCode\|StatusCode" tests/ -r --include="*.cs" | grep -v "Assert\.\|ReadFromJson\|Content\.\|Should" | head -3 && echo "WARN: check status-only assertions"
# 7. One shared factory
find tests/ -name "*.cs" -exec grep -l "WebApplicationFactory" {} \; 2>/dev/null | wc -l | xargs -I{} test {} -gt 1 && echo "FAIL: multiple WebApplicationFactory definitions"
# 8. All tests pass
dotnet test --no-build --verbosity quiet 2>&1 | tail -5
Substitute.For<>() on domain entities or value objectsCanSetName, HasInitializedCollections)Received(1)) without outcome assertionEnsureSuccessStatusCode() — no body/DB check[Theory]