Skip to main content

TDD in C# with xUnit: A Complete Tutorial (2026)

Learn test-driven development in C# with xUnit. Master the red-green-refactor cycle, best practices, and runnable examples. Start writing better tests today!

Test-driven development in C# (TDD) is one of the most valuable skills a .NET developer can master in 2026. Instead of writing code first and tests later, test driven development C# flips the process: you write a failing test, write just enough code to pass it, then refactor. The result is cleaner, more reliable, and easier-to-maintain software. In this tutorial, you'll learn how to do TDD in C# with xUnit, the most popular unit testing framework in the modern .NET ecosystem, with practical, runnable code examples you can try right now.

What Is Test-Driven Development in C#?

Test-driven development is a software design methodology where you write automated tests before writing the production code that makes them pass. TDD was popularized by Kent Beck and is built around a short, repeatable loop called red-green-refactor:

  • Red — Write a unit test that describes the behavior you want. It fails because the feature doesn't exist yet.
  • Green — Write the simplest code possible to make the test pass.
  • Refactor — Clean up the code (and tests) while keeping everything green.

Why bother? Because TDD forces you to think about behavior and design before implementation. You end up with a comprehensive suite of unit tests, fewer bugs in production, and code that's far easier to change without fear. For many teams, this safety net is the single biggest productivity boost they adopt.

Why Use xUnit for Unit Testing in C#?

There are three major unit testing frameworks in .NET: MSTest, NUnit, and xUnit. While all three work, xUnit has become the de facto standard for new projects, and it's what the .NET team uses internally. Here's why xUnit is a great choice for test driven development in C#:

  • Modern and lightweight — xUnit was built from lessons learned in NUnit and MSTest, removing legacy concepts like [SetUp]/[TearDown] in favor of constructor and IDisposable patterns.
  • Isolation by default — xUnit creates a new instance of the test class for every test method, preventing shared-state bugs between tests.
  • Great tooling — First-class support in Visual Studio, VS Code, Rider, and the dotnet test CLI.
  • Powerful data-driven tests — [Theory] with [InlineData] makes parameterized tests trivial.

Setting Up an xUnit Project

Let's create a solution with a class library and an xUnit test project. Open your terminal and run:

dotnet new classlib -n BankingApp
dotnet new xunit -n BankingApp.Tests
dotnet add BankingApp.Tests reference BankingApp
dotnet new sln -n Banking
dotnet sln add BankingApp BankingApp.Tests

The dotnet new xunit template automatically pulls in the xunit, xunit.runner.visualstudio, and Microsoft.NET.Test.Sdk NuGet packages, so you can run tests immediately with dotnet test.

TDD in C# Example: The Red-Green-Refactor Cycle

Let's build a simple BankAccount class using test-driven development. We'll follow the red-green-refactor loop step by step so you can see exactly how TDD C# development feels in practice.

Step 1: Red — Write a Failing Test

We start by describing the behavior we want: a new account should have a zero balance, and depositing money should increase the balance. In xUnit, a basic unit test uses the [Fact] attribute.

using Xunit;

public class BankAccountTests
{
    [Fact]
    public void NewAccount_HasZeroBalance()
    {
        // Arrange
        var account = new BankAccount();

        // Act
        decimal balance = account.Balance;

        // Assert
        Assert.Equal(0m, balance);
    }

    [Fact]
    public void Deposit_IncreasesBalance()
    {
        var account = new BankAccount();

        account.Deposit(100m);

        Assert.Equal(100m, account.Balance);
    }
}

Notice the Arrange-Act-Assert (AAA) pattern — a best practice that keeps tests readable. At this point the code won't even compile because BankAccount doesn't exist. That's the "red" state, and it's exactly where we want to be.

Step 2: Green — Write Just Enough Code to Pass

Now we write the minimum production code needed to make those tests pass. Resist the urge to add features the tests don't require — that discipline is the heart of TDD.

public class BankAccount
{
    public decimal Balance { get; private set; }

    public void Deposit(decimal amount)
    {
        Balance += amount;
    }
}

Run dotnet test and both tests turn green. We've proven the behavior works before moving on.

Step 3: Red Again — Drive Out Edge Cases

Real banking logic should reject invalid deposits. In TDD, we express that requirement as a new failing test first. This is also a perfect place to use a data-driven test with [Theory] and [InlineData], which lets one test method cover multiple inputs.

[Theory]
[InlineData(0)]
[InlineData(-1)]
[InlineData(-100)]
public void Deposit_WithNonPositiveAmount_ThrowsException(decimal amount)
{
    var account = new BankAccount();

    var ex = Assert.Throws<ArgumentException>(() => account.Deposit(amount));
    Assert.Equal("amount", ex.ParamName);
}

This test fails because our current Deposit happily accepts any number. Back to green:

public void Deposit(decimal amount)
{
    if (amount <= 0)
        throw new ArgumentException("Deposit must be positive.", nameof(amount));

    Balance += amount;
}

Step 4: Refactor — Add Withdrawal With Confidence

Let's drive out a Withdraw method the same way. Write the test first:

[Fact]
public void Withdraw_WithInsufficientFunds_ThrowsException()
{
    var account = new BankAccount();
    account.Deposit(50m);

    Assert.Throws<InvalidOperationException>(() => account.Withdraw(100m));
}

[Fact]
public void Withdraw_ReducesBalance()
{
    var account = new BankAccount();
    account.Deposit(100m);

    account.Withdraw(40m);

    Assert.Equal(60m, account.Balance);
}

Then implement just enough to pass:

public void Withdraw(decimal amount)
{
    if (amount <= 0)
        throw new ArgumentException("Withdrawal must be positive.", nameof(amount));
    if (amount > Balance)
        throw new InvalidOperationException("Insufficient funds.");

    Balance -= amount;
}

Because every behavior is covered by tests, you can now refactor the internals — extract validation, rename fields, change data structures — and instantly know if you broke anything. That confidence is why teams adopt test-driven development in C# in the first place.

xUnit Features Every C# Developer Should Know

Fact vs. Theory

Use [Fact] for a test with no parameters and a single scenario. Use [Theory] together with [InlineData], [MemberData], or [ClassData] when you want to run the same logic against many inputs. Theories dramatically reduce duplication in your unit tests.

Setup and Teardown the xUnit Way

Unlike NUnit's [SetUp], xUnit uses the constructor for per-test setup and IDisposable.Dispose() (or IAsyncLifetime) for cleanup. For shared, expensive setup across multiple tests, use a fixture with IClassFixture<T>.

public class BankAccountTests : IDisposable
{
    private readonly BankAccount _account;

    public BankAccountTests()  // runs before every test
    {
        _account = new BankAccount();
    }

    public void Dispose()      // runs after every test
    {
        // clean up resources here
    }

    [Fact]
    public void Deposit_IncreasesBalance()
    {
        _account.Deposit(100m);
        Assert.Equal(100m, _account.Balance);
    }
}

Useful xUnit Assertions

  • Assert.Equal(expected, actual) — value equality, the most common assertion.
  • Assert.True / Assert.False — boolean conditions.
  • Assert.Throws<T>() — verify the correct exception type is thrown.
  • Assert.Contains / Assert.Empty — assertions for collections and strings.
  • Assert.NotNull — guard against null returns.

TDD Best Practices in C#

Once you understand the mechanics, these best practices will keep your test suite fast, trustworthy, and maintainable:

  • Follow Arrange-Act-Assert. A clear AAA structure makes every unit test self-documenting.
  • Test behavior, not implementation. Assert on outcomes (the public API) rather than private internals so refactoring doesn't break your tests.
  • One logical assertion per test. When a test fails you should know exactly what broke without debugging.
  • Name tests descriptively. A pattern like MethodName_Scenario_ExpectedResult turns your test list into living documentation.
  • Keep tests fast and isolated. Unit tests shouldn't hit databases, networks, or the file system. Use mocking libraries like Moq or NSubstitute to isolate dependencies.
  • Write the smallest failing test. Don't over-engineer; let tests drive the design incrementally.

Common TDD Pitfalls to Avoid

  • Writing tests after the code. That's not TDD — it's test-after development, and you lose the design feedback that makes TDD valuable.
  • Testing the framework. Don't write unit tests for getters/setters or third-party libraries. Test your logic.
  • Brittle tests coupled to internals. Tests that break on every refactor discourage refactoring and defeat the purpose.
  • Skipping the refactor step. Green is not the finish line. The refactor phase is where good design emerges.
  • Slow test suites. If dotnet test takes minutes, developers stop running it. Keep unit tests in-memory and fast.

Running Your Tests

From the command line, run the entire suite with:

dotnet test

You can filter to specific tests and collect code coverage too:

dotnet test --filter "FullyQualifiedName~BankAccountTests"
dotnet test --collect:"XPlat Code Coverage"

Integrate dotnet test into your CI/CD pipeline (GitHub Actions, Azure DevOps) so every pull request runs the full suite automatically — a cornerstone of professional .NET development.

Conclusion: Key Takeaways

Test-driven development in C# with xUnit is a skill that pays dividends across your entire career. By writing tests first and following the red-green-refactor cycle, you produce code that's better designed, well-documented by its tests, and safe to change. Here are the key takeaways from this xUnit tutorial:

  • TDD = red-green-refactor. Write a failing test, make it pass simply, then improve the design.
  • xUnit is the modern standard for unit testing in C#, with per-test isolation and powerful [Theory] data-driven tests.
  • Use AAA and descriptive names to keep tests readable and maintainable.
  • Test behavior, keep tests fast, and never skip the refactor step.

Start small: pick one class in your current project and drive its next feature with a failing xUnit test. Once you experience the confidence that comprehensive test coverage gives you, you'll never want to write C# any other way. Ready to level up? Try applying test driven development to your next feature today and watch your bug count drop.

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