Test-Driven Development in C# · Lesson 1 of 6
Red-Green-Refactor — The TDD Cycle Explained
Test-Driven Development
TDD flips the order: write a failing test first, then write just enough code to make it pass, then improve the code. The cycle is Red → Green → Refactor.
The TDD Cycle
Red → Write a test that fails (the feature doesn't exist yet)
Green → Write the minimum code to make the test pass
Refactor → Clean up the code without breaking the tests
RepeatTDD in Action — Building a ShoppingCart
Red — Write the failing test first
C#
// CartTests.cs
[Fact]
public void NewCart_IsEmpty()
{
var cart = new ShoppingCart();
cart.ItemCount.Should().Be(0);
cart.Total.Should().Be(0);
}This fails because ShoppingCart doesn't exist yet. Good.
Green — Minimum code to pass
C#
public class ShoppingCart
{
public int ItemCount => 0;
public decimal Total => 0;
}Tests pass. Don't over-engineer — just make the test green.
Next test — Adding items
C#
[Fact]
public void AddItem_IncreasesItemCount()
{
var cart = new ShoppingCart();
cart.Add("SKU-001", "Widget", 9.99m, quantity: 2);
cart.ItemCount.Should().Be(2);
}
[Fact]
public void AddItem_CalculatesTotal()
{
var cart = new ShoppingCart();
cart.Add("SKU-001", "Widget", 9.99m, quantity: 2);
cart.Total.Should().Be(19.98m);
}Green
C#
public class ShoppingCart
{
private readonly List<CartLine> _lines = [];
public int ItemCount => _lines.Sum(l => l.Quantity);
public decimal Total => _lines.Sum(l => l.Price * l.Quantity);
public void Add(string sku, string name, decimal price, int quantity)
{
_lines.Add(new CartLine(sku, name, price, quantity));
}
}
public record CartLine(string Sku, string Name, decimal Price, int Quantity);Next test — Duplicate SKU combines
C#
[Fact]
public void AddSameSku_CombinesQuantities()
{
var cart = new ShoppingCart();
cart.Add("SKU-001", "Widget", 9.99m, 1);
cart.Add("SKU-001", "Widget", 9.99m, 3);
cart.ItemCount.Should().Be(4);
cart.Lines.Should().HaveCount(1); // combined, not duplicated
}Green
C#
public void Add(string sku, string name, decimal price, int quantity)
{
var existing = _lines.FirstOrDefault(l => l.Sku == sku);
if (existing is not null)
{
_lines.Remove(existing);
_lines.Add(existing with { Quantity = existing.Quantity + quantity });
}
else
{
_lines.Add(new CartLine(sku, name, price, quantity));
}
}Next test — Remove item
C#
[Fact]
public void RemoveItem_DecreasesCount()
{
var cart = new ShoppingCart();
cart.Add("SKU-001", "Widget", 9.99m, 3);
cart.Remove("SKU-001");
cart.ItemCount.Should().Be(0);
}
[Fact]
public void RemoveMissingSku_DoesNothing()
{
var cart = new ShoppingCart();
var act = () => cart.Remove("NOT-FOUND");
act.Should().NotThrow();
}Green → Refactor
C#
// Full ShoppingCart after refactor
public class ShoppingCart
{
private readonly List<CartLine> _lines = [];
public int ItemCount => _lines.Sum(l => l.Quantity);
public decimal Total => _lines.Sum(l => l.Price * l.Quantity);
public IReadOnlyList<CartLine> Lines => _lines.AsReadOnly();
public void Add(string sku, string name, decimal price, int quantity)
{
ArgumentOutOfRangeException.ThrowIfNegativeOrZero(quantity);
var existing = _lines.FirstOrDefault(l => l.Sku == sku);
if (existing is not null)
{
_lines.Remove(existing);
_lines.Add(existing with { Quantity = existing.Quantity + quantity });
}
else
{
_lines.Add(new CartLine(sku, name, price, quantity));
}
}
public void Remove(string sku)
{
var line = _lines.FirstOrDefault(l => l.Sku == sku);
if (line is not null) _lines.Remove(line);
}
}When TDD Pays Off
✓ Complex business rules (pricing, discounts, eligibility)
✓ Core domain logic that must be correct
✓ Code that will be maintained for years
✓ New features on existing, tested codeWhen TDD Is Overkill
✗ UI layout and styling
✗ Database migrations
✗ Simple CRUD with no business logic
✗ Infrastructure configurationKey Takeaways
- Write the test first — it forces you to think about the API before the implementation
- The Red phase defines the desired behavior; the Green phase implements it minimally
- Refactor only when tests are green — tests are your safety net
- TDD produces more focused, testable code — if a test is hard to write, the code is hard to test
- TDD is not about 100% coverage — it's about designing through examples