Backend Systemsbeginner
Red-Green-Refactor ā The TDD Cycle Explained
Learn Test-Driven Development: write a failing test first, make it pass, then refactor. See TDD in action with a real C# example and understand when it pays off.
Asma HafeezApril 17, 20264 min read
testingtddxunitdotnetclean-code
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
Enjoyed this article?
Explore the Backend Systems learning path for more.
Found this helpful?
Leave a comment
Have a question, correction, or just found this helpful? Leave a note below.