Skip to main content

TDD in C# with xUnit and Moq: A Practical Guide

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.

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

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...

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...

Send an Email via SMTP with MailKit Using .NET 6

How to Send an Email in .NET Core This tutorial show you how to send an email in .NET 6.0 using the MailKit email client library. Install MailKit via NuGet Visual Studio Package Manager Console: Install-Package MailKit How to Send an HTML Email in .NET 6.0 This code sends a simple HTML email using the Gmail SMTP service. There are instructions further below on how to use a few other popular SMTP providers - Gmail, Hotmail, Office 365. // create email message var email = new MimeMessage(); email.From.Add(MailboxAddress.Parse("from_address@example.com")); email.To.Add(MailboxAddress.Parse("to_address@example.com")); email.Subject = "Email Subject"; email.Body = new TextPart(TextFormat.Html) { Text = "<h1>Test HTML Message Body</h1>" }; // send email using var smtp = new SmtpClient(); smtp.Connect("smtp.gmail.com", 587, SecureSocketOptions.StartTls); smtp.Authenticate("[Username]", "[Password]"); smtp.Se...