Learnixo

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
Repeat

TDD 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 code

When TDD Is Overkill

✗ UI layout and styling
✗ Database migrations
✗ Simple CRUD with no business logic
✗ Infrastructure configuration

Key Takeaways

  1. Write the test first — it forces you to think about the API before the implementation
  2. The Red phase defines the desired behavior; the Green phase implements it minimally
  3. Refactor only when tests are green — tests are your safety net
  4. TDD produces more focused, testable code — if a test is hard to write, the code is hard to test
  5. TDD is not about 100% coverage — it's about designing through examples