Test-Driven Development in Real Projects
Write the test before the code. Let failing tests guide your design, producing cleaner, more reliable software with every iteration.
What You'll Learn
- • The Red-Green-Refactor cycle in practice
- • Build features incrementally with iterative TDD
- • Outside-In TDD: start from controllers, mock inward
- • When TDD helps most and when it adds overhead
🏗️ Real-World Analogy
TDD is like building with LEGO instructions. Each step (Red) shows you the piece you need. You find it and snap it in (Green). Then you tidy up the build surface (Refactor). You never build blindly — every piece has a purpose defined before you place it.
Red-Green-Refactor: The TDD Cycle
Red: Write a test that fails. Green: Write the minimum code to pass it. Refactor: Clean up while keeping tests green. Repeat for each new behaviour.
Red-Green-Refactor with ShoppingCart
Build a shopping cart feature step by step using the TDD cycle.
using Xunit;
// Step 1: RED — Write a failing test first
// We need a ShoppingCart that calculates totals with discounts
public class ShoppingCartTests
{
// RED: This test FAILS because ShoppingCart doesn't exist yet
[Fact]
public void NewCart_HasZeroTotal()
{
var cart = new ShoppingCart();
Assert.Equal(0m, cart.Total);
}
[Fact]
public void AddItem_SingleItem_UpdatesTotal()
{
var cart = new ShoppingCart();
cart.AddItem("Widget",
...Iterative TDD: Growing Features
Real TDD is iterative. Start with the simplest requirement, make it pass, then add the next. Each iteration extends your implementation without breaking previous tests.
Iterative TDD — Password Validator
Build a password validator one requirement at a time with TDD.
using Xunit;
using System;
// TDD Workflow: Build a PasswordValidator step by step
// ── Iteration 1: Basic length check ──
public class PasswordValidatorTests_V1
{
[Theory]
[InlineData("abc", false)] // Too short
[InlineData("abcdefgh", true)] // 8 chars — valid
[InlineData("", false)] // Empty
public void Validate_ChecksMinimumLength(string password, bool expected)
{
var validator = new PasswordValidator();
var result = validator.Va
...Outside-In TDD
Start from the outermost layer (API controller), mock dependencies, and work inward. Interfaces emerge naturally from what your tests need — producing clean architecture by design.
Outside-In TDD — Order System
Start from the controller, mock services, and let interfaces emerge from tests.
using Moq;
using Xunit;
// Outside-In TDD: Start from the API controller, mock inward
// ── Test the controller first ──
public class OrderControllerTests
{
[Fact]
public async Task CreateOrder_ValidRequest_Returns201()
{
// Arrange — mock the service layer
var mockService = new Mock<IOrderService>();
mockService.Setup(s => s.CreateOrderAsync(It.IsAny<CreateOrderDto>()))
.ReturnsAsync(new Order { Id = 1, Status = "Created" });
var contro
...Pro Tip
TDD works best for business logic and domain rules. For UI code, integration tests, or exploratory prototyping, a "test-after" approach is often more pragmatic. Don't force TDD everywhere — use it where it gives the most value.
Common Mistakes
- • Writing too many tests at once — TDD means one failing test at a time
- • Skipping the refactor step — code gets messy even when tests pass
- • Testing implementation details — test behaviour, not internal structure
Lesson Complete!
You've mastered Test-Driven Development. Next, learn how to structure large applications with Clean Architecture.
Sign up for free to track which lessons you've completed and get learning reminders.