Agent skill
dotnet-tdd
Use when implementing ASP.NET Core + EF Core features or bugfixes with xUnit + NSubstitute tests, before writing implementation code.
Install this agent skill to your Project
npx add-skill https://github.com/majiayu000/claude-skill-registry/tree/main/skills/other/other/dotnet-tdd
SKILL.md
.NET Test-Driven Development (TDD)
Overview
Write the test first. Watch it fail. Write minimal code to pass.
Core principle: If you didn't watch the test fail, you don't know if it tests the right thing.
Violating the letter of the rules is violating the spirit of the rules.
Quick 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 |
| Mock rule | Mock infrastructure only | Domain objects are real, never mocked |
| Naming | MethodName_Scenario_ExpectedBehavior |
Clear, behavioral, no CanSet* |
| Assertions | Max 3 per test | Use [Theory] for parameterized cases |
When to Use
Always: New features, bug fixes, refactoring, behavior changes.
Exceptions (ask your human partner): Throwaway prototypes, generated code, configuration files.
Thinking "skip TDD just this once"? Stop. That's rationalization.
The Iron Laws
1. NO PRODUCTION CODE WITHOUT A FAILING TEST FIRST
2. NEVER add test-only methods to production classes
3. MOCK BOUNDARY RULE (see below)
Write code before the test? Delete it. Start over.
No exceptions:
- Don't keep it as "reference"
- Don't "adapt" it while writing tests
- Don't look at it
- Delete means delete
Mock Boundary Rule
Mock infrastructure. NEVER mock domain.
OK to mock: DbContext/repositories, IHttpClientFactory, ILogger<T>, TimeProvider, external API clients, message queues, file systems.
NEVER mock: Domain entities, value objects, pure functions, domain services with no I/O.
Use NSubstitute for all mocking. Not Moq, not FakeItEasy. NSubstitute has cleaner syntax without lambda expressions for setup, and avoids Moq's SponsorLink controversy.
// OK: mocking infrastructure
var repo = Substitute.For<IShoppingListRepository>();
repo.GetByIdAsync(listId).Returns(existingList);
// NEVER: mocking domain
var item = Substitute.For<ShoppingItem>(); // NO. Use real entity.
var item = new ShoppingItem("Milk", Category.Dairy); // YES.
What to Test
Test behavior, not structure. Ask: "If this test didn't exist, what bug could ship?" If "none" -- delete it.
DO test: Public method behavior, edge cases, error paths, state transitions, integration (HTTP -> response -> DB state).
DO NOT test: Getters/setters, constructor property assignment, framework behavior, private methods.
Integration test rule: Assert response body or DB state, not just HTTP status. response.EnsureSuccessStatusCode() alone proves nothing — verify the actual data.
No assertion-less tests: A test with zero assertions is always a bug. It passes silently and catches nothing.
BAD: Testing language features
[Fact]
public void CanSetProperties()
{
var item = new ShoppingItem();
item.Name = "Milk";
Assert.Equal("Milk", item.Name);
}
Tests language feature, not behavior. Would never catch a bug.
Red-Green-Refactor
digraph tdd_cycle {
rankdir=LR;
red [label="RED\nWrite failing test", shape=box, style=filled, fillcolor="#ffcccc"];
verify_red [label="Verify fails\ncorrectly", shape=diamond];
green [label="GREEN\nMinimal code", shape=box, style=filled, fillcolor="#ccffcc"];
verify_green [label="All tests\npass?", shape=diamond];
refactor [label="REFACTOR\nClean up", shape=box, style=filled, fillcolor="#ccccff"];
next [label="Next behavior", shape=ellipse];
red -> verify_red;
verify_red -> green [label="yes"];
verify_red -> red [label="wrong\nfailure"];
green -> verify_green;
verify_green -> refactor [label="yes"];
verify_green -> green [label="no"];
refactor -> verify_green [label="re-verify"];
next -> red;
refactor -> next [label="all green\nconfirmed"];
}
RED - Write Failing Test
One minimal test showing desired behavior. Run it. Confirm it fails because the feature is missing (not typos/errors).
[Fact]
public async Task AddItem_WithValidName_AddsItemToList()
{
var list = new ShoppingList("Weekly Groceries", householdId);
list.AddItem("Milk", Category.Dairy);
Assert.Single(list.Items);
Assert.Equal("Milk", list.Items[0].Name);
}
Requirements: One behavior per test. Clear name: MethodName_Scenario_ExpectedBehavior. Real domain objects.
GREEN - Minimal Code
Write simplest code to pass. Don't add features, refactor, or "improve" beyond the test.
// Just enough to make AddItem_WithValidName_AddsItemToList pass:
public void AddItem(string name, Category category)
{
_items.Add(new ShoppingItem(name, category));
}
REFACTOR
After green only: remove duplication, improve names, extract helpers. Keep all tests green.
Naming Convention
MethodName_Scenario_ExpectedBehavior
Examples: AddItem_WithEmptyName_ThrowsArgumentException, GetNearbyStores_WithNoStoresInRange_ReturnsEmptyList
Assertion Rules
Max 3 assertions per test. More than 3? Split. Use [Theory] + [InlineData] for parameterized tests.
Single logical assertion is fine even with multiple Assert calls (e.g., checking name + category of one added item).
Test Project Structure
Unit tests: tests/{Project}.Tests/Unit/
Integration tests: tests/{Project}.Tests/Integration/
Shared fixtures: tests/{Project}.Tests/Infrastructure/
Shared Test Infrastructure
Create ONE shared CustomWebApplicationFactory for integration tests. Use SQLite in-memory with kept-alive connection, NOT the EF InMemory provider.
REFERENCE: See testing-anti-patterns.md, Anti-Pattern 6 for the full CustomWebApplicationFactory implementation and usage pattern.
Common Rationalizations
| Excuse | Reality |
|---|---|
| "Too simple to test" | Simple code breaks. Test takes 30 seconds. |
| "I'll test after" | Tests passing immediately prove nothing -- you never saw it catch the bug. |
| "Tests after achieve same goals" | Tests-after = "what does this do?" Tests-first = "what should this do?" |
| "Already manually tested" | Ad-hoc != systematic. No record, can't re-run. |
| "Deleting X hours is wasteful" | Sunk cost fallacy. Keeping unverified code is technical debt. |
| "Keep as reference, write tests first" | You'll adapt it. That's testing after. Delete means delete. |
| "Need to explore first" | Fine. Throw away exploration, start with TDD. |
| "Test hard = design unclear" | Listen to test. Hard to test = hard to use. |
| "TDD will slow me down" | TDD faster than debugging. Pragmatic = test-first. |
| "The existing codebase has no tests" | That's exactly why you start now. Every new feature gets TDD. Don't inherit technical debt as an excuse. |
Red Flags - STOP and Start Over
- Code before test / test after implementation
- Test passes immediately
- Can't explain why test failed
- Rationalizing "just this once" or "this is different because..."
- Testing getters/setters (
CanSetName,HasInitializedCollections) - Testing that a mock was called instead of testing real behavior
- More than 3 assertions / no
[Theory]for multiple inputs - Test with zero assertions (passes silently, catches nothing)
- Integration test only asserts HTTP status code, not response body or DB state
- "Already spent X hours, deleting is wasteful"
All of these mean: Delete code. Start over with TDD.
When Stuck
| Problem | Solution |
|---|---|
| Don't know how to test | Write wished-for API. Write assertion first. Ask your human partner. |
| Test too complicated | Design too complicated. Simplify interface. |
| Must mock everything | Code too coupled. Use dependency injection. |
| Test setup huge | Extract to shared helpers/factory. Still complex? Simplify design. |
Testing Anti-Patterns
REFERENCE: See testing-anti-patterns.md for the full list of anti-patterns with examples and gate functions.
Final Rule
Production code -> test exists and failed first
Otherwise -> not TDD
No exceptions without your human partner's permission.
Recommended Agent Skills
Expand your agent's capabilities with these related and highly-rated skills.
agent-ops-spec
Manage specification documents in .agent/specs/. Use when user provides requirements, acceptance criteria, or feature descriptions that need to be tracked and validated against implementation.
agent-ops-state
Maintain .agent state files. Use at session start, after meaningful steps, and before concluding: read/update constitution/memory/focus/issues/baseline consistently.
agent-ops-spec
Manage specification documents in .agent/specs/. Use when user provides requirements, acceptance criteria, or feature descriptions that need to be tracked and validated against implementation.
agent-ops-testing
Test strategy, execution, and coverage analysis. Use when designing tests, running test suites, or analyzing test results beyond baseline checks.
agent-ops-testing
Test strategy, execution, and coverage analysis. Use when designing tests, running test suites, or analyzing test results beyond baseline checks.
agent-ops-state
Maintain .agent state files. Use at session start, after meaningful steps, and before concluding: read/update constitution/memory/focus/issues/baseline consistently.
Didn't find tool you were looking for?