
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 andIDisposablepatterns. - 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 testCLI. - 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_ExpectedResultturns 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 testtakes 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.
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