| name | tdd-workflow |
| description | Test-Driven Development workflow for .NET. Use when implementing features, fixing bugs, or writing new code. Guides RED-GREEN-REFACTOR cycle with proper test design. |
| allowed-tools | Read, Grep, Glob, Bash, Edit, Write |
Test-Driven Development Workflow
The TDD Cycle: RED-GREEN-REFACTOR
┌─────────────────────────────────────┐
│ │
│ ┌─────┐ ┌───────┐ ┌─────┐ │
│ │ RED │───▶│ GREEN │───▶│REFAC│──┘
│ └─────┘ └───────┘ └─────┘
│ │ │
│ │ Write failing test │
│ │ │
│ ▼ │
│ Make it pass (minimal) │
│ │
└──────────────────────────────┘
Improve design
Phase 1: RED - Write a Failing Test
Rules for RED Phase
- Write ONE test that fails
- Test must fail for the RIGHT reason
- Test must be meaningful and specific
- Run the test to confirm it fails
Test Naming Convention
{MethodUnderTest}_{Scenario}_{ExpectedBehavior}
Examples:
CreateOrder_WithValidItems_ReturnsOrder
GetUser_WhenNotFound_ThrowsNotFoundException
CalculateTotal_WithDiscount_AppliesCorrectPercentage
AAA Pattern (Arrange-Act-Assert)
[Fact]
public void MethodName_Scenario_ExpectedResult()
{
var sut = new SystemUnderTest();
var input = CreateValidInput();
var result = sut.Execute(input);
Assert.Equal(expected, result);
}
Test Categories
[Fact]
public void Calculator_Add_ReturnsSumOfNumbers() { }
[Fact]
public void OrderService_CreateOrder_PersistsToDatabase() { }
[Fact]
public void User_CanCompleteCheckout_WithValidCart() { }
Phase 2: GREEN - Make It Pass
Rules for GREEN Phase
- Write MINIMAL code to pass the test
- Do NOT add extra features
- Do NOT optimize yet
- It's okay to be "ugly" - we'll fix it in REFACTOR
- Run tests to confirm they pass
The Simplest Thing That Works
public decimal CalculateDiscount(Order order)
{
var strategy = _discountStrategyFactory.Create(order.CustomerType);
return strategy.Calculate(order, _configService.GetDiscountRules());
}
public decimal CalculateDiscount(Order order)
{
return order.Total * 0.1m;
}
Fake It Till You Make It
[Fact]
public void GetGreeting_ReturnsHello()
{
var result = greeter.GetGreeting();
Assert.Equal("Hello", result);
}
public string GetGreeting() => "Hello";
Phase 3: REFACTOR - Improve Design
Rules for REFACTOR Phase
- Tests MUST stay green
- Improve structure, not behavior
- Apply SOLID principles
- Remove duplication (DRY)
- Simplify (KISS)
- Remove unused code (YAGNI)
Refactoring Checklist
Common Refactorings
public void ProcessOrder(Order order)
{
}
public void ProcessOrder(Order order)
{
ValidateOrder(order);
CalculateTotals(order);
ApplyDiscounts(order);
PersistOrder(order);
NotifyCustomer(order);
}
Test Doubles
Types of Test Doubles
var dummyLogger = new Mock<ILogger>().Object;
var stubRepo = new Mock<IUserRepository>();
stubRepo.Setup(r => r.GetById(1)).Returns(new User { Id = 1 });
var spyNotifier = new SpyNotifier();
service.Execute();
Assert.True(spyNotifier.WasCalled);
var mockNotifier = new Mock<INotifier>();
service.Execute();
mockNotifier.Verify(n => n.Send(It.IsAny<Message>()), Times.Once);
var fakeRepo = new InMemoryUserRepository();
When to Use What
| Double | Use When |
|---|
| Dummy | Parameter required but unused |
| Stub | Need controlled return values |
| Spy | Need to verify calls were made |
| Mock | Need to verify specific interactions |
| Fake | Need realistic behavior without dependencies |
Test Organization
Project Structure
src/
├── MyApp.Domain/
│ └── Entities/
├── MyApp.Application/
│ └── Services/
└── MyApp.Infrastructure/
└── Repositories/
tests/
├── MyApp.Domain.Tests/
│ └── Entities/
├── MyApp.Application.Tests/
│ └── Services/
└── MyApp.Integration.Tests/
└── Repositories/
Test Class Structure
public class OrderServiceTests
{
private readonly Mock<IOrderRepository> _mockRepository;
private readonly Mock<INotificationService> _mockNotifier;
private readonly OrderService _sut;
public OrderServiceTests()
{
_mockRepository = new Mock<IOrderRepository>();
_mockNotifier = new Mock<INotificationService>();
_sut = new OrderService(_mockRepository.Object, _mockNotifier.Object);
}
[Fact]
public void CreateOrder_WithValidData_PersistsOrder() { }
[Fact]
public void CreateOrder_WithInvalidData_ThrowsValidationException() { }
}
Anti-Patterns to Avoid
Test Smells
Assert.Equal(3, order.Items.Count);
Assert.True(order.HasItems);
[Fact]
public void Order_Tests()
{
Assert.NotNull(order.Id);
Assert.Equal("Pending", order.Status);
Assert.True(order.Total > 0);
}
[Fact]
public void NewOrder_HasPendingStatus()
{
Assert.Equal(OrderStatus.Pending, order.Status);
}
[Fact]
public void Test1_CreateUser() { }
[Fact]
public void Test2_GetUser() { }
[Fact]
public void GetUser_WhenExists_ReturnsUser()
{
var user = CreateUser();
var result = _sut.GetUser(user.Id);
Assert.NotNull(result);
}
Quick Reference
TDD Commands
dotnet test
dotnet test /p:CollectCoverage=true
dotnet test --filter "FullyQualifiedName~OrderServiceTests"
dotnet watch test
Test Attributes
[Fact]
[Theory]
[InlineData(1, 2)]
[Trait("Category", "Unit")]
[Skip("Reason")]
See patterns.md for advanced testing patterns.