
Learn TDD in C# with xUnit and Moq. Write tests first, code second with practical examples, best practices, and common pitfalls. Start writing better C# today.
Test-Driven Development (TDD) in C# flips the way most developers write code: instead of writing a feature and hoping your tests catch the bugs later, you write a failing test first, then write just enough code to make it pass. In this hands-on tutorial you'll learn TDD in C# using xUnit for testing and Moq for mocking dependencies. By the end you'll understand not just how to write tests first and code second, but why this workflow produces cleaner, more maintainable .NET applications.
Whether you're a beginner searching for "how to write unit tests in C#", an intermediate developer chasing testing best practices, or a senior engineer refining your mocking strategy, this guide covers the full red-green-refactor cycle with runnable code examples.
What Is TDD in C# and Why It Matters
Test-Driven Development is a discipline where every new behavior begins as a test. The core loop, known as red-green-refactor, has three steps:
- Red — Write a test that describes the behavior you want. It fails because the code doesn't exist yet.
- Green — Write the simplest code that makes the test pass. No gold-plating.
- Refactor — Clean up the implementation and the test while keeping everything green.
Why bother writing tests first? Because TDD in C# forces you to think about the public API and expected behavior before you're distracted by implementation details. It gives you a fast feedback loop, a safety net of regression tests, and a design pressure that naturally pushes toward small, decoupled classes. Teams that adopt TDD typically report fewer production bugs and far less fear when refactoring legacy code.
Setting Up xUnit and Moq in Your .NET Project
xUnit is the most popular unit testing framework in the modern .NET ecosystem — it's the framework the .NET team itself uses. Moq is the go-to mocking library for creating fake implementations of interfaces so you can test a class in isolation.
Start by creating a solution with a class library and a test project:
// From the terminal
dotnet new sln -n BankingApp
dotnet new classlib -n BankingApp.Core
dotnet new xunit -n BankingApp.Tests
dotnet sln add BankingApp.Core BankingApp.Tests
dotnet add BankingApp.Tests reference BankingApp.Core
// Add Moq to the test project
dotnet add BankingApp.Tests package Moq
Your test project already references xUnit and the test SDK. The Moq package gives you the mocking API. That's everything you need to start practicing TDD.
Writing Your First Test First (Red)
Let's build a simple AccountService that transfers money between accounts. The rule of TDD is simple: no production code until a failing test demands it. So we begin with the test, even though AccountService doesn't exist yet.
using Xunit;
public class AccountServiceTests
{
[Fact]
public void Withdraw_WithSufficientFunds_ReducesBalance()
{
// Arrange
var account = new Account(balance: 100m);
var service = new AccountService();
// Act
service.Withdraw(account, 40m);
// Assert
Assert.Equal(60m, account.Balance);
}
}
This won't even compile — Account and AccountService don't exist. That's the red phase. The failing test is your specification. It says, in executable form, "withdrawing 40 from a balance of 100 leaves 60."
Making the Test Pass (Green)
Now write the minimum code to satisfy the test. Resist the urge to add features nobody asked for.
public class Account
{
public decimal Balance { get; private set; }
public Account(decimal balance) => Balance = balance;
public void Debit(decimal amount) => Balance -= amount;
}
public class AccountService
{
public void Withdraw(Account account, decimal amount)
{
account.Debit(amount);
}
}
Run dotnet test. The test passes — you're green. Notice we didn't add overdraft protection or logging yet, because no test requires it. In TDD, untested code is code you shouldn't be writing.
Driving Edge Cases With More Tests
Good behavior emerges one test at a time. What happens if someone withdraws more than they have? Write the test first.
[Fact]
public void Withdraw_WithInsufficientFunds_ThrowsInvalidOperationException()
{
var account = new Account(balance: 50m);
var service = new AccountService();
var ex = Assert.Throws<InvalidOperationException>(
() => service.Withdraw(account, 100m));
Assert.Equal("Insufficient funds.", ex.Message);
}
This fails (red), so we add just enough logic to make it green:
public void Withdraw(Account account, decimal amount)
{
if (amount > account.Balance)
throw new InvalidOperationException("Insufficient funds.");
account.Debit(amount);
}
Use [Theory] with [InlineData] to test many inputs without duplicating code — a key xUnit best practice for keeping test suites concise:
[Theory]
[InlineData(100, 30, 70)]
[InlineData(100, 100, 0)]
[InlineData(250, 50, 200)]
public void Withdraw_ValidAmounts_LeavesCorrectBalance(
decimal start, decimal amount, decimal expected)
{
var account = new Account(start);
new AccountService().Withdraw(account, amount);
Assert.Equal(expected, account.Balance);
}
Using Moq to Mock Dependencies in C#
Real services rarely live in isolation — they talk to databases, email gateways, and payment APIs. You don't want your unit tests hitting a real database. This is where Moq in C# shines: it lets you supply fake implementations of an interface so you can test your logic without external systems.
Suppose AccountService must send a notification after a large withdrawal. First, define the dependency as an interface — a design decision that TDD encourages naturally:
public interface INotificationService
{
void Send(string message);
}
Now write a test that verifies the notification is sent. With Moq you create a mock, inject it, and then verify the interaction:
using Moq;
using Xunit;
public class AccountServiceNotificationTests
{
[Fact]
public void Withdraw_LargeAmount_SendsNotification()
{
// Arrange
var mockNotifier = new Mock<INotificationService>();
var service = new AccountService(mockNotifier.Object);
var account = new Account(balance: 5000m);
// Act
service.Withdraw(account, 1000m);
// Assert — verify the dependency was called exactly once
mockNotifier.Verify(
n => n.Send(It.IsAny<string>()),
Times.Once);
}
}
The test drives a new constructor. Update AccountService to accept the dependency and act on it:
public class AccountService
{
private readonly INotificationService _notifier;
public AccountService(INotificationService notifier)
{
_notifier = notifier;
}
public void Withdraw(Account account, decimal amount)
{
if (amount > account.Balance)
throw new InvalidOperationException("Insufficient funds.");
account.Debit(amount);
if (amount >= 1000m)
_notifier.Send($"Large withdrawal of {amount:C} processed.");
}
}
Setting Up Return Values With Moq
Beyond verifying calls, Moq can make a fake method return canned data using Setup and Returns. This is essential when your code depends on the result of a call:
var mockRepo = new Mock<IAccountRepository>();
mockRepo
.Setup(r => r.GetById(42))
.Returns(new Account(balance: 300m));
var service = new AccountService(mockRepo.Object);
var account = service.Load(42);
Assert.Equal(300m, account.Balance);
mockRepo.Verify(r => r.GetById(42), Times.Once);
Use It.IsAny<T>() to match any argument, or pass specific values like 42 to assert exact arguments were used. This precision is what makes Moq so powerful for isolating the unit under test.
TDD Best Practices in C#
Following the mechanics of xUnit and Moq is only half the battle. These best practices separate durable test suites from brittle ones:
- Follow Arrange-Act-Assert. Structure every test in three clear sections. It makes intent obvious to the next reader.
- Test behavior, not implementation. Assert on observable outcomes (return values, state, interactions) rather than private internals. Otherwise refactoring breaks tests unnecessarily.
- One logical assertion per test. A test that checks one behavior gives a precise failure message when it goes red.
- Name tests descriptively. The
MethodName_Scenario_ExpectedResultconvention turns your test list into living documentation. - Mock only what you own. Wrap third-party SDKs behind your own interfaces, then mock those interfaces — don't mock types you can't control.
- Keep the cycle tight. Write a small failing test, make it pass, refactor, repeat. Don't write ten tests before any production code.
Common TDD and Mocking Pitfalls to Avoid
Even experienced .NET developers stumble on these traps when doing TDD in C#:
- Over-mocking. If a test has more mock setup than actual assertions, your class probably has too many dependencies. Let that pain drive a better design.
- Verifying everything. Only verify interactions that matter to the behavior. Excessive
Verifycalls make tests fragile. - Testing the mock instead of your code. If a test only asserts that a mock returns what you told it to return, it proves nothing.
- Skipping the refactor step. Green is not the finish line. Duplicated, messy code that passes tests still rots over time.
- Writing tests after the fact and calling it TDD. Tests written last tend to confirm existing bugs rather than prevent them.
Conclusion: Key Takeaways for TDD in C#
TDD in C# with xUnit and Moq is less about tooling and more about a mindset: write the test first, let it fail, then write code second to make it pass. That discipline gives you a fast feedback loop, a comprehensive regression safety net, and a design that stays clean because testability is baked in from the start.
Here are the key takeaways to remember:
- Follow the red-green-refactor cycle religiously — one small test at a time.
- Use xUnit's
[Fact]and[Theory]attributes to keep tests expressive and DRY. - Use Moq to isolate the unit under test by mocking interfaces, then
Verifyonly the interactions that matter. - Test behavior over implementation so your suite survives refactoring.
- Avoid over-mocking and never skip the refactor step.
Start small: pick one class in your current project, write a failing test before your next change, and experience the confidence that comes from writing tests first and code second. Once TDD clicks, you'll never want to go back to writing untested C# again.
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