Skip to main content

C# Unit Testing with xUnit: TDD Tutorial for Beginners

Learn C# unit testing with xUnit and test-driven development in this complete tutorial. Master TDD, assertions, mocking, and best practices. Start testing today!

C# unit testing is one of the most valuable skills a .NET developer can master, yet it's often the most overlooked. If you've ever shipped code that broke in production, spent hours debugging a regression, or felt afraid to refactor legacy code, then learning C# unit testing with xUnit and test-driven development (TDD) will change how you build software forever. In this complete tutorial, you'll learn how to write reliable, maintainable unit tests in C# using xUnit—the most popular testing framework in the modern .NET ecosystem.

Whether you're a beginner searching for your first C# unit test example, an intermediate developer looking for best practices, or a senior engineer adopting test-driven development, this guide covers everything you need with practical, runnable code.

What Is Unit Testing in C# and Why It Matters

A unit test verifies that a small, isolated piece of your code—typically a single method—behaves exactly as expected. The word "unit" refers to the smallest testable part of your application. Instead of manually running your program and clicking through the UI to check whether something works, a unit test runs in milliseconds and tells you immediately if your logic is correct.

Here's why unit testing matters so much in .NET development:

  • Catch bugs early: Tests fail the moment you break something, long before code reaches production.
  • Refactor with confidence: A solid test suite is a safety net that lets you restructure code without fear.
  • Living documentation: Tests describe exactly how your code is meant to behave.
  • Faster feedback loops: No more manual testing cycles—run hundreds of tests in seconds.

The three most popular C# testing frameworks are xUnit, NUnit, and MSTest. We focus on xUnit because it's the modern standard, used by the .NET team itself, with clean syntax and excellent extensibility.

Setting Up xUnit: Your First C# Unit Test

Let's get hands-on. The fastest way to start C# unit testing is with the .NET CLI. Assume you have a class library project called BankApp. Create a matching test project:

// Create an xUnit test project from the terminal
dotnet new xunit -n BankApp.Tests

// Add a reference to the project you want to test
dotnet add BankApp.Tests/BankApp.Tests.csproj reference BankApp/BankApp.csproj

// Run all tests
dotnet test

The dotnet new xunit template automatically installs the three packages you need: xunit, xunit.runner.visualstudio, and Microsoft.NET.Test.Sdk. Now let's write a simple class to test:

namespace BankApp;

public class Calculator
{
    public int Add(int a, int b) => a + b;

    public int Divide(int a, int b)
    {
        if (b == 0)
            throw new DivideByZeroException("Cannot divide by zero.");
        return a / b;
    }
}

And here is your first xUnit test. In xUnit, you mark a test method with the [Fact] attribute. A "fact" is a test that is always true for a fixed set of inputs:

using Xunit;
using BankApp;

public class CalculatorTests
{
    [Fact]
    public void Add_TwoPositiveNumbers_ReturnsCorrectSum()
    {
        // Arrange
        var calculator = new Calculator();

        // Act
        int result = calculator.Add(3, 5);

        // Assert
        Assert.Equal(8, result);
    }
}

Notice the Arrange-Act-Assert (AAA) pattern. This is the gold standard structure for every unit test: set up your objects (Arrange), call the method under test (Act), then verify the outcome (Assert). Following AAA consistently makes your tests readable and predictable.

Understanding xUnit Assertions

Assertions are how you verify behavior. xUnit ships with a rich set of assertion methods. Here are the ones you'll use most often:

using Xunit;

public class AssertionExamples
{
    [Fact]
    public void CommonAssertions()
    {
        // Equality
        Assert.Equal(10, 5 + 5);
        Assert.NotEqual(10, 7);

        // Booleans
        Assert.True(1 < 2);
        Assert.False(2 < 1);

        // Null checks
        string? name = null;
        Assert.Null(name);
        Assert.NotNull("Claude");

        // Collections
        var numbers = new[] { 1, 2, 3 };
        Assert.Contains(2, numbers);
        Assert.Equal(3, numbers.Length);

        // Strings
        Assert.StartsWith("Hello", "Hello, World");
        Assert.Contains("World", "Hello, World");
    }
}

Testing for exceptions is critical. Use Assert.Throws to verify that your code fails the way it should:

[Fact]
public void Divide_ByZero_ThrowsDivideByZeroException()
{
    var calculator = new Calculator();

    var exception = Assert.Throws(
        () => calculator.Divide(10, 0));

    Assert.Equal("Cannot divide by zero.", exception.Message);
}

Data-Driven Tests with [Theory] and [InlineData]

What if you want to test the same logic with many different inputs? Copying a [Fact] ten times is wasteful. xUnit solves this with [Theory], which runs the same test once per data set. This is one of xUnit's most powerful features:

public class CalculatorTheoryTests
{
    [Theory]
    [InlineData(2, 3, 5)]
    [InlineData(-1, 1, 0)]
    [InlineData(0, 0, 0)]
    [InlineData(100, 250, 350)]
    public void Add_VariousInputs_ReturnsExpectedSum(int a, int b, int expected)
    {
        var calculator = new Calculator();
        int result = calculator.Add(a, b);
        Assert.Equal(expected, result);
    }
}

Each [InlineData] row becomes a separate test case in your test runner. For more complex data, you can use [MemberData] or [ClassData] to supply test data from a property or class. Data-driven testing dramatically reduces duplication and increases coverage.

Test-Driven Development (TDD) in C#: The Red-Green-Refactor Cycle

Now for the heart of this tutorial: test-driven development in C#. TDD flips the usual workflow on its head. Instead of writing code first and testing later, you write the test before the implementation. The TDD cycle has three steps:

  • Red: Write a failing test for behavior that doesn't exist yet.
  • Green: Write the minimum code needed to make the test pass.
  • Refactor: Clean up the code while keeping all tests green.

Let's walk through a real example. Suppose we need a ShoppingCart that calculates a total with a discount. Step 1 — Red: write the test first:

public class ShoppingCartTests
{
    [Fact]
    public void Total_WithTenPercentDiscount_ReturnsDiscountedPrice()
    {
        var cart = new ShoppingCart();
        cart.AddItem(price: 100m, quantity: 2);   // 200 total

        decimal total = cart.GetTotal(discountPercent: 10);

        Assert.Equal(180m, total);   // 200 - 10%
    }
}

This won't even compile because ShoppingCart doesn't exist yet. That's expected—we're in the Red phase. Step 2 — Green: write just enough code to pass:

public class ShoppingCart
{
    private decimal _subtotal;

    public void AddItem(decimal price, int quantity)
    {
        _subtotal += price * quantity;
    }

    public decimal GetTotal(decimal discountPercent)
    {
        decimal discount = _subtotal * (discountPercent / 100m);
        return _subtotal - discount;
    }
}

Run dotnet test—the test passes. Step 3 — Refactor: improve the design while keeping tests green. Maybe you extract the discount calculation into its own method or add validation. Because you have a test, you can refactor fearlessly. This Red-Green-Refactor loop is what makes TDD so powerful: every line of production code is justified by a test that demanded it.

Mocking Dependencies with Moq

Real-world classes rarely live in isolation. They depend on databases, web APIs, or other services. To unit test such a class, you isolate it by replacing its dependencies with mocks—fake objects you control. The most popular mocking library in C# is Moq. Install it with dotnet add package Moq.

Consider an OrderService that depends on an IEmailService interface:

public interface IEmailService
{
    bool SendConfirmation(string email);
}

public class OrderService
{
    private readonly IEmailService _emailService;

    public OrderService(IEmailService emailService)
    {
        _emailService = emailService;
    }

    public bool PlaceOrder(string customerEmail)
    {
        // ... order processing logic ...
        return _emailService.SendConfirmation(customerEmail);
    }
}

We don't want to send a real email in a test. Instead, we mock IEmailService and verify the interaction:

using Moq;
using Xunit;

public class OrderServiceTests
{
    [Fact]
    public void PlaceOrder_SendsConfirmationEmail()
    {
        // Arrange
        var mockEmail = new Mock();
        mockEmail
            .Setup(e => e.SendConfirmation(It.IsAny()))
            .Returns(true);

        var service = new OrderService(mockEmail.Object);

        // Act
        bool result = service.PlaceOrder("user@example.com");

        // Assert
        Assert.True(result);
        mockEmail.Verify(
            e => e.SendConfirmation("user@example.com"),
            Times.Once);
    }
}

The Setup method configures the mock's behavior, and Verify confirms that our code actually called the dependency exactly once. This is how you write fast, deterministic unit tests for code with external dependencies—a cornerstone of professional .NET testing.

C# Unit Testing Best Practices

Writing tests is easy; writing good tests takes discipline. Follow these best practices to keep your test suite valuable:

  • One logical assertion per test: Each test should verify a single behavior so failures point to one cause.
  • Use descriptive test names: Follow the MethodName_Scenario_ExpectedResult convention so a failing test name explains itself.
  • Keep tests independent: Tests must not depend on execution order or shared state. xUnit creates a new test class instance for each test, which helps enforce isolation.
  • Test behavior, not implementation: Assert on outcomes, not private internals, so tests survive refactoring.
  • Aim for fast tests: Unit tests should run in milliseconds. Anything touching a real database or network is an integration test, not a unit test.
  • Use the AAA pattern: Arrange, Act, Assert keeps every test readable.

Common Unit Testing Pitfalls to Avoid

Even experienced developers fall into these traps. Watch out for them:

  • Testing trivial code: Don't test auto-properties or framework code. Focus on your business logic.
  • Over-mocking: Mocking everything makes tests brittle and meaningless. Mock external dependencies, not the system under test.
  • Fragile tests: Tests that break on every minor refactor usually assert on implementation details rather than behavior.
  • Ignoring failing tests: A skipped or commented-out test is worse than no test—it gives false confidence.
  • No edge cases: Test boundaries—nulls, empty collections, zero, negatives, and maximum values—where bugs love to hide.

Conclusion: Key Takeaways

Mastering C# unit testing with xUnit and test-driven development is one of the highest-return investments you can make in your career as a .NET developer. You'll ship fewer bugs, refactor without fear, and build software you can actually trust.

Let's recap the key takeaways from this tutorial:

  • Use xUnit with the [Fact] and [Theory] attributes to write clean, data-driven tests.
  • Structure every test with the Arrange-Act-Assert pattern.
  • Adopt test-driven development with the Red-Green-Refactor cycle to drive better design.
  • Isolate dependencies using Moq so your unit tests stay fast and deterministic.
  • Follow best practices—descriptive names, independent tests, and edge-case coverage—while avoiding common pitfalls.

The best way to learn C# unit testing is to start writing tests today. Open your current project, pick one method, and write a single [Fact]. Then write another. Before long, testing will become second nature—and your future self will thank you. Happy testing!

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