
Tdd Workflow
- 7 installs
- 4 repo stars
- Updated June 18, 2026
- doubleslashse/claude-marketplace
Follow the RED-GREEN-REFACTOR test-driven development cycle with proper test design when building or fixing .NET code.
About
Guides the RED-GREEN-REFACTOR TDD cycle with test-design practices for .NET. A developer uses it when implementing features or fixing bugs test-first.
- Drives the RED-GREEN-REFACTOR cycle
- Emphasizes proper test design
Tdd Workflow by the numbers
- 7 all-time installs (skills.sh)
- Ranked #1,579 of 2,153 Testing & QA skills by installs in the Skillselion catalog
- Data as of Jul 29, 2026 (Skillselion catalog sync)
npx skills add https://github.com/doubleslashse/claude-marketplace --skill tdd-workflowAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 7 |
|---|---|
| repo stars | ★ 4 |
| Last updated | June 18, 2026 |
| Repository | doubleslashse/claude-marketplace ↗ |
What it does
Follow the RED-GREEN-REFACTOR test-driven development cycle with proper test design when building or fixing .NET code.
Files
Test-Driven Development Workflow
The TDD Cycle: RED-GREEN-REFACTOR
┌─────────────────────────────────────┐
│ │
│ ┌─────┐ ┌───────┐ ┌─────┐ │
│ │ RED │───▶│ GREEN │───▶│REFAC│──┘
│ └─────┘ └───────┘ └─────┘
│ │ │
│ │ Write failing test │
│ │ │
│ ▼ │
│ Make it pass (minimal) │
│ │
└──────────────────────────────┘
Improve designPhase 1: RED - Write a Failing Test
Rules for RED Phase
1. Write ONE test that fails 2. Test must fail for the RIGHT reason 3. Test must be meaningful and specific 4. Run the test to confirm it fails
Test Naming Convention
{MethodUnderTest}_{Scenario}_{ExpectedBehavior}Examples:
CreateOrder_WithValidItems_ReturnsOrderGetUser_WhenNotFound_ThrowsNotFoundExceptionCalculateTotal_WithDiscount_AppliesCorrectPercentage
AAA Pattern (Arrange-Act-Assert)
[Fact]
public void MethodName_Scenario_ExpectedResult()
{
// Arrange - Set up preconditions
var sut = new SystemUnderTest();
var input = CreateValidInput();
// Act - Execute the behavior
var result = sut.Execute(input);
// Assert - Verify outcome
Assert.Equal(expected, result);
}Test Categories
// Unit Test - Tests single unit in isolation
[Fact]
public void Calculator_Add_ReturnsSumOfNumbers() { }
// Integration Test - Tests component interaction
[Fact]
public void OrderService_CreateOrder_PersistsToDatabase() { }
// Acceptance Test - Tests user scenarios
[Fact]
public void User_CanCompleteCheckout_WithValidCart() { }Phase 2: GREEN - Make It Pass
Rules for GREEN Phase
1. Write MINIMAL code to pass the test 2. Do NOT add extra features 3. Do NOT optimize yet 4. It's okay to be "ugly" - we'll fix it in REFACTOR 5. Run tests to confirm they pass
The Simplest Thing That Works
// BAD - Over-engineering in GREEN phase
public decimal CalculateDiscount(Order order)
{
var strategy = _discountStrategyFactory.Create(order.CustomerType);
return strategy.Calculate(order, _configService.GetDiscountRules());
}
// GOOD - Minimal implementation for GREEN
public decimal CalculateDiscount(Order order)
{
return order.Total * 0.1m; // 10% discount
}Fake It Till You Make It
// Test expects specific value
[Fact]
public void GetGreeting_ReturnsHello()
{
var result = greeter.GetGreeting();
Assert.Equal("Hello", result);
}
// GREEN: Just return what the test expects
public string GetGreeting() => "Hello";Phase 3: REFACTOR - Improve Design
Rules for REFACTOR Phase
1. Tests MUST stay green 2. Improve structure, not behavior 3. Apply SOLID principles 4. Remove duplication (DRY) 5. Simplify (KISS) 6. Remove unused code (YAGNI)
Refactoring Checklist
- [ ] Extract methods for clarity
- [ ] Rename for intent
- [ ] Remove duplication
- [ ] Apply design patterns if needed
- [ ] Check for SOLID violations
- [ ] Run tests after each change
Common Refactorings
// Before: Long method
public void ProcessOrder(Order order)
{
// 50 lines of mixed concerns
}
// After: Single responsibility
public void ProcessOrder(Order order)
{
ValidateOrder(order);
CalculateTotals(order);
ApplyDiscounts(order);
PersistOrder(order);
NotifyCustomer(order);
}Test Doubles
Types of Test Doubles
// Dummy - Passed but never used
var dummyLogger = new Mock<ILogger>().Object;
// Stub - Provides canned answers
var stubRepo = new Mock<IUserRepository>();
stubRepo.Setup(r => r.GetById(1)).Returns(new User { Id = 1 });
// Spy - Records interactions
var spyNotifier = new SpyNotifier();
service.Execute();
Assert.True(spyNotifier.WasCalled);
// Mock - Verifies interactions
var mockNotifier = new Mock<INotifier>();
service.Execute();
mockNotifier.Verify(n => n.Send(It.IsAny<Message>()), Times.Once);
// Fake - Working implementation (in-memory)
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
// BAD: Testing implementation details
Assert.Equal(3, order.Items.Count);
// GOOD: Testing behavior
Assert.True(order.HasItems);
// BAD: Multiple assertions testing different behaviors
[Fact]
public void Order_Tests()
{
Assert.NotNull(order.Id);
Assert.Equal("Pending", order.Status);
Assert.True(order.Total > 0);
}
// GOOD: One logical assertion per test
[Fact]
public void NewOrder_HasPendingStatus()
{
Assert.Equal(OrderStatus.Pending, order.Status);
}
// BAD: Tests depending on order
[Fact]
public void Test1_CreateUser() { } // Creates user
[Fact]
public void Test2_GetUser() { } // Assumes user exists
// GOOD: Independent tests
[Fact]
public void GetUser_WhenExists_ReturnsUser()
{
var user = CreateUser(); // Arrange includes setup
var result = _sut.GetUser(user.Id);
Assert.NotNull(result);
}Quick Reference
TDD Commands
# Run all tests
dotnet test
# Run with coverage
dotnet test /p:CollectCoverage=true
# Run specific test
dotnet test --filter "FullyQualifiedName~OrderServiceTests"
# Watch mode
dotnet watch testTest Attributes
[Fact] // Single test case
[Theory] // Parameterized test
[InlineData(1, 2)] // Test data
[Trait("Category", "Unit")] // Categorization
[Skip("Reason")] // Skip testSee patterns.md for advanced testing patterns.
Advanced TDD Patterns
Builder Pattern for Test Data
public class OrderBuilder
{
private Guid _id = Guid.NewGuid();
private List<OrderItem> _items = new();
private OrderStatus _status = OrderStatus.Pending;
private decimal _discount = 0;
public OrderBuilder WithId(Guid id)
{
_id = id;
return this;
}
public OrderBuilder WithItem(string product, int quantity, decimal price)
{
_items.Add(new OrderItem(product, quantity, price));
return this;
}
public OrderBuilder WithStatus(OrderStatus status)
{
_status = status;
return this;
}
public OrderBuilder WithDiscount(decimal discount)
{
_discount = discount;
return this;
}
public Order Build()
{
return new Order(_id, _items, _status, _discount);
}
// Factory methods for common scenarios
public static OrderBuilder ValidOrder() => new OrderBuilder()
.WithItem("Product A", 2, 10.00m)
.WithItem("Product B", 1, 25.00m);
public static OrderBuilder EmptyOrder() => new OrderBuilder();
public static OrderBuilder CompletedOrder() => ValidOrder()
.WithStatus(OrderStatus.Completed);
}
// Usage in tests
[Fact]
public void CalculateTotal_WithMultipleItems_ReturnsSumOfItems()
{
var order = OrderBuilder.ValidOrder()
.WithItem("Extra", 1, 5.00m)
.Build();
var total = order.CalculateTotal();
Assert.Equal(50.00m, total);
}Object Mother Pattern
public static class TestUsers
{
public static User AdminUser() => new User
{
Id = Guid.Parse("11111111-1111-1111-1111-111111111111"),
Email = "admin@test.com",
Role = UserRole.Admin
};
public static User RegularUser() => new User
{
Id = Guid.Parse("22222222-2222-2222-2222-222222222222"),
Email = "user@test.com",
Role = UserRole.Regular
};
public static User GuestUser() => new User
{
Id = Guid.NewGuid(),
Email = "guest@test.com",
Role = UserRole.Guest
};
}
// Usage
[Fact]
public void DeleteUser_AsAdmin_Succeeds()
{
var admin = TestUsers.AdminUser();
var target = TestUsers.RegularUser();
var result = _sut.DeleteUser(admin, target.Id);
Assert.True(result.IsSuccess);
}Parameterized Tests
public class DiscountCalculatorTests
{
[Theory]
[InlineData(100, 10, 90)] // 10% off $100 = $90
[InlineData(200, 15, 170)] // 15% off $200 = $170
[InlineData(50, 0, 50)] // 0% off $50 = $50
public void ApplyDiscount_WithVariousInputs_ReturnsCorrectTotal(
decimal subtotal, decimal discountPercent, decimal expected)
{
var result = DiscountCalculator.Apply(subtotal, discountPercent);
Assert.Equal(expected, result);
}
[Theory]
[MemberData(nameof(DiscountScenarios))]
public void ApplyDiscount_WithComplexScenarios_ReturnsCorrectTotal(
DiscountTestCase testCase)
{
var result = DiscountCalculator.Apply(testCase.Subtotal, testCase.Discount);
Assert.Equal(testCase.Expected, result);
}
public static IEnumerable<object[]> DiscountScenarios()
{
yield return new object[] { new DiscountTestCase(100, 10, 90, "Standard discount") };
yield return new object[] { new DiscountTestCase(0, 50, 0, "Zero subtotal") };
yield return new object[] { new DiscountTestCase(100, 100, 0, "Full discount") };
}
}
public record DiscountTestCase(decimal Subtotal, decimal Discount, decimal Expected, string Description);Fixture Pattern (Shared Context)
public class DatabaseFixture : IDisposable
{
public DbContext Context { get; }
public DatabaseFixture()
{
var options = new DbContextOptionsBuilder<AppDbContext>()
.UseInMemoryDatabase(Guid.NewGuid().ToString())
.Options;
Context = new AppDbContext(options);
SeedTestData();
}
private void SeedTestData()
{
Context.Users.Add(TestUsers.AdminUser());
Context.Users.Add(TestUsers.RegularUser());
Context.SaveChanges();
}
public void Dispose()
{
Context.Dispose();
}
}
[CollectionDefinition("Database")]
public class DatabaseCollection : ICollectionFixture<DatabaseFixture> { }
[Collection("Database")]
public class UserRepositoryTests
{
private readonly DatabaseFixture _fixture;
public UserRepositoryTests(DatabaseFixture fixture)
{
_fixture = fixture;
}
[Fact]
public void GetById_WhenUserExists_ReturnsUser()
{
var repo = new UserRepository(_fixture.Context);
var result = repo.GetById(TestUsers.AdminUser().Id);
Assert.NotNull(result);
}
}Specification Pattern for Complex Assertions
public static class OrderAssertions
{
public static void ShouldBeValid(this Order order)
{
Assert.NotEqual(Guid.Empty, order.Id);
Assert.NotEmpty(order.Items);
Assert.True(order.Total > 0);
}
public static void ShouldHaveStatus(this Order order, OrderStatus expected)
{
Assert.Equal(expected, order.Status);
}
public static void ShouldContainItem(this Order order, string productName)
{
Assert.Contains(order.Items, i => i.ProductName == productName);
}
}
// Usage
[Fact]
public void CreateOrder_WithValidItems_CreatesValidOrder()
{
var order = _sut.CreateOrder(items);
order.ShouldBeValid();
order.ShouldHaveStatus(OrderStatus.Pending);
order.ShouldContainItem("Product A");
}Fake Repository Pattern
public class InMemoryOrderRepository : IOrderRepository
{
private readonly Dictionary<Guid, Order> _orders = new();
public Task<Order?> GetByIdAsync(Guid id)
{
_orders.TryGetValue(id, out var order);
return Task.FromResult(order);
}
public Task<IReadOnlyList<Order>> GetAllAsync()
{
return Task.FromResult<IReadOnlyList<Order>>(_orders.Values.ToList());
}
public Task AddAsync(Order order)
{
_orders[order.Id] = order;
return Task.CompletedTask;
}
public Task UpdateAsync(Order order)
{
_orders[order.Id] = order;
return Task.CompletedTask;
}
public Task DeleteAsync(Guid id)
{
_orders.Remove(id);
return Task.CompletedTask;
}
// Test helpers
public void Clear() => _orders.Clear();
public int Count => _orders.Count;
public bool Contains(Guid id) => _orders.ContainsKey(id);
}Auto-Mocking Container
public class AutoMocker<T> where T : class
{
private readonly Dictionary<Type, object> _mocks = new();
public Mock<TDependency> GetMock<TDependency>() where TDependency : class
{
var type = typeof(TDependency);
if (!_mocks.ContainsKey(type))
{
_mocks[type] = new Mock<TDependency>();
}
return (Mock<TDependency>)_mocks[type];
}
public T CreateInstance()
{
var constructor = typeof(T).GetConstructors()
.OrderByDescending(c => c.GetParameters().Length)
.First();
var parameters = constructor.GetParameters()
.Select(p => GetOrCreateMock(p.ParameterType))
.ToArray();
return (T)constructor.Invoke(parameters);
}
private object GetOrCreateMock(Type type)
{
if (!_mocks.ContainsKey(type))
{
var mockType = typeof(Mock<>).MakeGenericType(type);
var mock = Activator.CreateInstance(mockType);
_mocks[type] = mock!;
}
return ((dynamic)_mocks[type]).Object;
}
}
// Usage
public class OrderServiceTests
{
private readonly AutoMocker<OrderService> _mocker = new();
private readonly OrderService _sut;
public OrderServiceTests()
{
_sut = _mocker.CreateInstance();
}
[Fact]
public void CreateOrder_CallsRepository()
{
var order = OrderBuilder.ValidOrder().Build();
_sut.CreateOrder(order);
_mocker.GetMock<IOrderRepository>()
.Verify(r => r.AddAsync(order), Times.Once);
}
}Testing Async Code
[Fact]
public async Task GetOrderAsync_WhenExists_ReturnsOrder()
{
// Arrange
var orderId = Guid.NewGuid();
var expectedOrder = OrderBuilder.ValidOrder().WithId(orderId).Build();
_mockRepository
.Setup(r => r.GetByIdAsync(orderId))
.ReturnsAsync(expectedOrder);
// Act
var result = await _sut.GetOrderAsync(orderId);
// Assert
Assert.Equal(expectedOrder, result);
}
[Fact]
public async Task ProcessOrderAsync_WhenFails_ThrowsException()
{
_mockProcessor
.Setup(p => p.ProcessAsync(It.IsAny<Order>()))
.ThrowsAsync(new ProcessingException("Failed"));
await Assert.ThrowsAsync<ProcessingException>(
() => _sut.ProcessOrderAsync(new Order()));
}Testing Events and Callbacks
[Fact]
public void ProcessOrder_RaisesOrderProcessedEvent()
{
// Arrange
var eventRaised = false;
Order? raisedOrder = null;
_sut.OrderProcessed += (sender, args) =>
{
eventRaised = true;
raisedOrder = args.Order;
};
var order = OrderBuilder.ValidOrder().Build();
// Act
_sut.Process(order);
// Assert
Assert.True(eventRaised);
Assert.Equal(order, raisedOrder);
}
[Fact]
public void ExecuteWithCallback_InvokesCallback()
{
// Arrange
var callbackInvoked = false;
var callback = new Action<Result>(r => callbackInvoked = true);
// Act
_sut.ExecuteWithCallback(callback);
// Assert
Assert.True(callbackInvoked);
}Testing Time-Dependent Code
public interface IDateTimeProvider
{
DateTime Now { get; }
DateTime UtcNow { get; }
}
public class SystemDateTimeProvider : IDateTimeProvider
{
public DateTime Now => DateTime.Now;
public DateTime UtcNow => DateTime.UtcNow;
}
public class TestDateTimeProvider : IDateTimeProvider
{
public DateTime Now { get; set; } = new DateTime(2024, 1, 15, 10, 0, 0);
public DateTime UtcNow { get; set; } = new DateTime(2024, 1, 15, 10, 0, 0, DateTimeKind.Utc);
public void Advance(TimeSpan duration)
{
Now = Now.Add(duration);
UtcNow = UtcNow.Add(duration);
}
}
// Usage
[Fact]
public void CreateOrder_SetsCreatedDateToNow()
{
var dateProvider = new TestDateTimeProvider();
var sut = new OrderService(dateProvider);
var order = sut.CreateOrder();
Assert.Equal(dateProvider.Now, order.CreatedAt);
}
[Fact]
public void IsExpired_AfterExpiration_ReturnsTrue()
{
var dateProvider = new TestDateTimeProvider();
var order = new Order { ExpiresAt = dateProvider.Now.AddHours(1) };
dateProvider.Advance(TimeSpan.FromHours(2));
Assert.True(order.IsExpired(dateProvider));
}