Skip to main content

TDD in C# with xUnit and Moq: A Complete Tutorial

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_ExpectedResult convention 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 Verify calls 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 Verify only 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.

About csharp-coder.com
Your go-to resource for C#, .NET, and modern software development. Follow along for daily tutorials, tips, and real-world examples.

Comments

Popular posts from this blog

.NET MAUI Tutorial 2026: Build Cross-Platform Apps in C#

Learn .NET MAUI in 2026 to build iOS, Android, Windows & Mac apps from one C# codebase. Start this cross-platform tutorial with code examples today. .NET MAUI (Multi-platform App UI) is Microsoft's framework for building native iOS, Android, Windows, and macOS apps from a single C# codebase . If you've ever wanted to ship a mobile app without learning Swift, Kotlin, and Win32 separately, this .NET MAUI tutorial for 2026 is your starting point. In this guide you'll learn what .NET MAUI is, why it matters for cross-platform app development in C#, and how to build your first working app — with runnable code examples and the best practices senior engineers actually use in production. What Is .NET MAUI and Why Use It in 2026? .NET MAUI is the evolution of Xamarin.Forms, fully integrated into the modern .NET runtime. With one project and one language — C# — you target four platforms. The framework compiles to native UI controls on each device, so a button on iOS...

Angular 14 : 404 error during refresh page after deployment

In this article, We will learn how to solve 404 file or directory not found angular error in production.  Refresh browser angular 404 file or directory not found error You have built an Angular app and created a production build with ng build --prod You deploy it to a production server. Everything works fine until you refresh the page. The app throws The requested URL was not found on this server message (Status code 404 not found). It appears that angular routing not working on the production server when you refresh the page. The error appears on the following scenarios When you type the URL directly in the address bar. When you refresh the page The error appears on all the pages except the root page.   Reason for the requested URL was not found on this server error In a Multi-page web application, every time the application needs to display a page it has to send a request to the web server. You can do that by either typing the URL in the address bar, clicking on the Me...

Angular 14 CRUD Operation with Web API .Net 6.0

How to Perform CRUD Operation Using Angular 14 In this article, we will learn the angular crud (create, read, update, delete) tutorial with ASP.NET Core 6 web API. We will use the SQL Server database and responsive user interface for our Web app, we will use the Bootstrap 5. Let's start step by step. Step 1 - Create Database and Web API First we need to create Employee database in SQL Server and web API to communicate with database. so you can use my previous article CRUD operations in web API using net 6.0 to create web API step by step. As you can see, after creating all the required API and database, our API creation part is completed. Now we have to do the angular part like installing angular CLI, creating angular 14 project, command for building and running angular application...etc. Step 2 - Install Angular CLI Now we have to install angular CLI into our system. If you have already installed angular CLI into your system then skip this step.  To install angular CLI ope...