
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_ExpectedResultconvention 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!
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