
Learn TDD in C# using xUnit and Moq. Write tests first, code second with step-by-step examples and best practices. Start building better software today.
TDD in C#: Why Writing Tests First Changes Everything
TDD in C# is a development practice where you write a failing test before writing the production code that makes it pass. It sounds backwards, but test driven development in C# produces cleaner designs, fewer bugs, and code you can refactor with confidence. In this guide, you will learn how to practice TDD using xUnit and Moq — the two most popular testing libraries in the .NET ecosystem.
If you have ever spent hours debugging a regression that slipped through manual testing, TDD is the answer. Instead of treating tests as an afterthought, you make them the driving force behind your design decisions. By the end of this article, you will have a complete, working C# TDD example you can apply to your own projects.
What Is Test Driven Development in C#?
Test driven development follows a strict cycle known as Red-Green-Refactor:
- Red: Write a test that describes a behavior you want. Run it — it fails because the code does not exist yet.
- Green: Write the minimum amount of production code to make the test pass.
- Refactor: Clean up the code while keeping all tests green.
This cycle typically takes one to five minutes. You repeat it dozens of times per feature. The result is a codebase where every line of production code exists because a test demanded it.
Why Developers Choose TDD Over Test-After
Writing tests after the code is written often leads to tests that mirror the implementation rather than verifying behavior. TDD flips this: your tests describe what the system should do, not how it does it. This distinction matters because implementation details change — behavior should not.
Teams practicing TDD in C# report measurable benefits:
- Fewer production defects (studies show a 40–80% reduction in bug density)
- Simpler, more modular designs driven by testability requirements
- Faster debugging — when a test fails, you know exactly what broke
- Living documentation — tests describe the system's intended behavior
Setting Up Your C# TDD Environment with xUnit and Moq
Before writing your first test, you need a project structure. The standard convention in .NET is to keep your production code and test code in separate projects within the same solution.
// Create the solution and projects from your terminal
// dotnet new sln -n OrderSystem
// dotnet new classlib -n OrderSystem.Core
// dotnet new xunit -n OrderSystem.Tests
// dotnet sln add OrderSystem.Core OrderSystem.Tests
// cd OrderSystem.Tests
// dotnet add reference ../OrderSystem.Core
// dotnet add package Moq
This gives you an xUnit test project with a reference to your production code and Moq installed for mocking dependencies. xUnit is the most widely used unit testing framework in modern .NET because of its clean syntax, parallel test execution, and strong community support.
Your First C# TDD Example: Building an Order Service
Let us build an order processing service using strict TDD. We will not write a single line of production code until a test asks for it.
Step 1 (Red): Write the First Failing Test
We want an OrderService that can place an order. Start with the simplest possible behavior: placing an order should save it to a repository.
using Moq;
using Xunit;
namespace OrderSystem.Tests
{
public class OrderServiceTests
{
[Fact]
public void PlaceOrder_WithValidItem_SavesOrderToRepository()
{
// Arrange
var mockRepository = new Mock<IOrderRepository>();
var service = new OrderService(mockRepository.Object);
var item = new OrderItem("Keyboard", 2, 49.99m);
// Act
service.PlaceOrder(item);
// Assert
mockRepository.Verify(
r => r.Save(It.Is<Order>(o => o.Items.Contains(item))),
Times.Once);
}
}
}
This test will not compile. That is the point. The compiler errors tell you exactly what to create: IOrderRepository, OrderService, OrderItem, and Order. In TDD, compilation errors are your first red signal.
Step 2 (Green): Write Just Enough Code to Pass
Now create the minimum production code. Do not add anything the test does not require.
namespace OrderSystem.Core
{
public record OrderItem(string Name, int Quantity, decimal UnitPrice);
public class Order
{
public List<OrderItem> Items { get; } = new();
public decimal Total => Items.Sum(i => i.Quantity * i.UnitPrice);
}
public interface IOrderRepository
{
void Save(Order order);
}
public class OrderService
{
private readonly IOrderRepository _repository;
public OrderService(IOrderRepository repository)
{
_repository = repository;
}
public void PlaceOrder(OrderItem item)
{
var order = new Order();
order.Items.Add(item);
_repository.Save(order);
}
}
}
Run the test. It passes. You are green. Notice the code is deliberately simple — no validation, no error handling. TDD says: do not build what no test demands.
Step 3 (Red Again): Add Validation Logic
Now let us add a rule: orders with a quantity of zero or less should throw an exception. Write the test first.
[Theory]
[InlineData(0)]
[InlineData(-1)]
public void PlaceOrder_WithInvalidQuantity_ThrowsArgumentException(int quantity)
{
// Arrange
var mockRepository = new Mock<IOrderRepository>();
var service = new OrderService(mockRepository.Object);
var item = new OrderItem("Mouse", quantity, 25.00m);
// Act & Assert
var exception = Assert.Throws<ArgumentException>(
() => service.PlaceOrder(item));
Assert.Contains("quantity", exception.Message, StringComparison.OrdinalIgnoreCase);
mockRepository.Verify(r => r.Save(It.IsAny<Order>()), Times.Never);
}
This test uses xUnit's [Theory] attribute with [InlineData] to run the same test with different invalid inputs. It also verifies the repository was never called — a failed order should not be persisted.
Step 4 (Green): Add the Validation
public void PlaceOrder(OrderItem item)
{
if (item.Quantity <= 0)
throw new ArgumentException("Quantity must be greater than zero.");
var order = new Order();
order.Items.Add(item);
_repository.Save(order);
}
Both tests pass. The design is emerging from the tests, not from upfront planning.
Using Moq for Dependency Isolation in Unit Testing C#
Real services have dependencies: databases, APIs, email services. Moq lets you isolate the code under test by replacing dependencies with controlled fakes. Here is a more advanced Moq C# example that tests discount logic with a mocked pricing service.
public interface IPricingService
{
decimal GetDiscount(string customerTier);
}
// Test: premium customers get a 15% discount
[Fact]
public void PlaceOrder_ForPremiumCustomer_AppliesDiscount()
{
// Arrange
var mockRepo = new Mock<IOrderRepository>();
var mockPricing = new Mock<IPricingService>();
mockPricing
.Setup(p => p.GetDiscount("Premium"))
.Returns(0.15m);
var service = new OrderService(mockRepo.Object, mockPricing.Object);
var item = new OrderItem("Monitor", 1, 400.00m);
// Act
service.PlaceOrder(item, customerTier: "Premium");
// Assert
mockRepo.Verify(r => r.Save(
It.Is<Order>(o => o.Total == 340.00m)), Times.Once);
}
// Updated production code
public void PlaceOrder(OrderItem item, string customerTier = "Standard")
{
if (item.Quantity <= 0)
throw new ArgumentException("Quantity must be greater than zero.");
var order = new Order();
order.Items.Add(item);
var discount = _pricingService.GetDiscount(customerTier);
order.ApplyDiscount(discount);
_repository.Save(order);
}
Moq's Setup and Verify methods are the two pillars of mocking. Setup defines what the mock returns when called. Verify confirms that expected interactions occurred. Together, they let you test complex workflows without touching real databases or services.
TDD Best Practices Every .NET Developer Should Follow
After practicing TDD in C# on multiple projects, certain patterns emerge as essential:
1. One Assertion Per Logical Concept
Each test should verify one behavior. If you are testing that an order calculates totals correctly and validates input in the same test, split it into two. This makes failures precise and debugging fast.
2. Follow the Arrange-Act-Assert Pattern
Every test should have three clearly separated sections. The Arrange block sets up the test data and mocks. The Act block calls the method under test. The Assert block verifies the outcome. This structure makes tests readable even to someone unfamiliar with the codebase.
3. Name Tests to Describe Behavior
Use the format MethodName_Scenario_ExpectedResult. When a test fails at 2 AM, a name like PlaceOrder_WithExpiredCoupon_ThrowsInvalidOperationException tells you exactly what broke without reading the test body.
4. Do Not Mock What You Own Unless Necessary
Only mock interfaces that represent external boundaries — database access, HTTP calls, file systems. If you find yourself mocking half the classes in your project, your design has too much coupling. Let TDD push you toward better interfaces.
5. Refactor Ruthlessly While Green
The refactor step is where TDD pays off. You can restructure code with confidence because the tests will catch regressions instantly. If you skip refactoring, you accumulate technical debt that erases TDD's benefits.
Common TDD Pitfalls and How to Avoid Them
Even experienced developers fall into these traps when starting with test driven development in C#:
- Writing too many tests at once: TDD works in small, fast cycles. Write one test, make it pass, then write the next. Batch-writing tests defeats the purpose because you are designing without feedback.
- Testing implementation details: If your test breaks every time you refactor, it is testing how the code works rather than what it does. Verify outputs and side effects, not internal method calls.
- Skipping the refactor step: Green is not done. The refactor step is where you eliminate duplication, improve naming, and simplify logic. Without it, TDD produces working but messy code.
- Over-mocking: If a test requires five mocks to set up, the class under test has too many dependencies. Use this as a design signal to break the class into smaller, focused components.
- Abandoning TDD for "simple" code: Simple code has a way of becoming complex code. The tests you write today protect you from the bugs that tomorrow's feature changes introduce.
xUnit Features That Make TDD Productive
xUnit provides several features that fit naturally into the TDD workflow:
// [Theory] with [MemberData] for complex test cases
public static IEnumerable<object[]> BulkOrderData => new[]
{
new object[] { 10, 29.99m, 299.90m },
new object[] { 50, 29.99m, 1499.50m },
new object[] { 100, 29.99m, 2999.00m },
};
[Theory]
[MemberData(nameof(BulkOrderData))]
public void Order_CalculatesTotal_ForBulkQuantities(
int quantity, decimal unitPrice, decimal expectedTotal)
{
var item = new OrderItem("Cable", quantity, unitPrice);
var order = new Order();
order.Items.Add(item);
Assert.Equal(expectedTotal, order.Total);
}
// IAsyncLifetime for async setup/teardown
public class IntegrationTests : IAsyncLifetime
{
private OrderService _service;
public async Task InitializeAsync()
{
_service = await TestSetup.CreateServiceAsync();
}
public Task DisposeAsync() => Task.CompletedTask;
[Fact]
public async Task PlaceOrderAsync_PersistsToDatabase()
{
var item = new OrderItem("Desk", 1, 299.99m);
await _service.PlaceOrderAsync(item);
// Assert against real database in integration tests
}
}
[Theory] with [MemberData] is particularly useful in TDD because it lets you express a wide range of scenarios without duplicating test methods. When you discover a new edge case during development, you add a row to the data — not a new test.
When TDD Is Worth the Investment
TDD adds overhead. Each feature takes longer to build initially because you are writing tests alongside the code. The return comes later: fewer bugs in production, faster debugging, safer refactoring, and code that other developers can understand through its tests.
TDD is especially valuable for:
- Business logic: Discount calculations, workflow rules, validation — anywhere a mistake costs money.
- Long-lived codebases: The test suite becomes a safety net for years of changes.
- Team projects: Tests document behavior more reliably than comments or wikis.
Key Takeaways
TDD in C# with xUnit and Moq is not just a testing technique — it is a design discipline. By writing tests first, you force your code into small, testable, well-defined units. The Red-Green-Refactor cycle keeps you focused on behavior rather than implementation. Moq isolates your code from external dependencies so every unit test runs in milliseconds.
Start with a single class in your current project. Write one failing test, make it pass, refactor. Do this for a week and you will notice your designs becoming simpler, your bugs becoming rarer, and your confidence in the code becoming stronger. The best time to start practicing test driven development in C# is today.
Your go-to resource for C#, .NET, and modern software development. Follow along for daily tutorials, tips, and real-world examples.
Comments
Post a Comment