A unit test is a small program that runs part of your code and checks the answer. In C# that means xUnit, NUnit or MSTest — xUnit is the common default for new projects. This guide covers writing tests, the arrange-act-assert shape, testing code that depends on a database or a clock, and how to decide what deserves a test at all.
Setting up#
dotnet new xunit -n MyApp.Tests
cd MyApp.Tests
dotnet add reference ../MyApp/MyApp.csproj
dotnet test
The convention is one test project per production project, named after it with .Tests appended.
A first test#
public class Calculator
{
public decimal ApplyDiscount(decimal price, int percent)
{
if (percent < 0 || percent > 100)
throw new ArgumentOutOfRangeException(nameof(percent));
return Math.Round(price * (100 - percent) / 100m, 2);
}
}
using Xunit;
public class CalculatorTests
{
[Fact]
public void ApplyDiscount_ReducesPriceByPercentage()
{
// Arrange
var calculator = new Calculator();
// Act
decimal result = calculator.ApplyDiscount(100m, 20);
// Assert
Assert.Equal(80m, result);
}
[Fact]
public void ApplyDiscount_RejectsNegativePercentage()
{
var calculator = new Calculator();
Assert.Throws<ArgumentOutOfRangeException>(
() => calculator.ApplyDiscount(100m, -5));
}
}
Every test has the same three parts: set up the situation, do the thing, check the result. Keeping them visually separate makes tests scannable, which matters when one fails a year from now.
The naming convention MethodName_ExpectedBehaviour means a failing test tells you what broke without opening the file.
Parameterised tests#
[Theory]
[InlineData(100, 0, 100)]
[InlineData(100, 50, 50)]
[InlineData(100, 100, 0)]
[InlineData(19.99, 10, 17.99)]
public void ApplyDiscount_CalculatesCorrectly(decimal price, int percent, decimal expected)
{
var calculator = new Calculator();
Assert.Equal(expected, calculator.ApplyDiscount(price, percent));
}
Four cases, one method, and each appears separately in the results so you can see exactly which combination failed.
For values that cannot be compile-time constants, use a member data source:
public static IEnumerable<object[]> DiscountCases => new List<object[]>
{
new object[] { 100m, 20, 80m },
new object[] { 250m, 15, 212.50m },
};
[Theory]
[MemberData(nameof(DiscountCases))]
public void ApplyDiscount_HandlesCases(decimal price, int percent, decimal expected)
{
Assert.Equal(expected, new Calculator().ApplyDiscount(price, percent));
}
The assertions you will use#
Assert.Equal(expected, actual);
Assert.NotEqual(a, b);
Assert.True(condition);
Assert.Null(value);
Assert.NotNull(value);
Assert.Empty(collection);
Assert.Contains("text", message);
Assert.Single(collection);
Assert.Throws<InvalidOperationException>(() => DoThing());
await Assert.ThrowsAsync<HttpRequestException>(() => FetchAsync());
// Floating point needs a tolerance
Assert.Equal(0.3, 0.1 + 0.2, precision: 10);
Making code testable#
This class cannot be tested reliably:
public class OrderService
{
public bool IsExpired(Order order)
{
return order.CreatedAt.AddDays(30) < DateTime.Now; // reads the clock
}
}
The test result depends on when it runs. Inject the dependency instead:
public interface IClock
{
DateTime Now { get; }
}
public class SystemClock : IClock
{
public DateTime Now => DateTime.Now;
}
public class OrderService
{
private readonly IClock _clock;
public OrderService(IClock clock) => _clock = clock;
public bool IsExpired(Order order) => order.CreatedAt.AddDays(30) < _clock.Now;
}
public class FixedClock : IClock
{
public FixedClock(DateTime now) => Now = now;
public DateTime Now { get; }
}
[Fact]
public void IsExpired_ReturnsTrueAfterThirtyDays()
{
var order = new Order { CreatedAt = new DateTime(2026, 1, 1) };
var service = new OrderService(new FixedClock(new DateTime(2026, 2, 15)));
Assert.True(service.IsExpired(order));
}
The same pattern applies to databases, HTTP clients, file systems and random numbers — anything the test cannot control. Code that is hard to test is usually code with hidden dependencies, so the difficulty is telling you something about the design.
Mocking#
dotnet add package Moq
using Moq;
[Fact]
public void PlaceOrder_SavesAndNotifies()
{
var repository = new Mock<IOrderRepository>();
var notifier = new Mock<INotifier>();
repository.Setup(r => r.Save(It.IsAny<Order>())).Returns(42);
var service = new OrderService(repository.Object, notifier.Object);
service.PlaceOrder(new Order());
repository.Verify(r => r.Save(It.IsAny<Order>()), Times.Once);
notifier.Verify(n => n.Send(It.IsAny<string>()), Times.Once);
}
Use mocks for things you cannot call in a test — sending real emails, charging real cards. Do not mock your own simple classes; a real instance is clearer and tests more.
Setup and cleanup#
public class DatabaseTests : IDisposable
{
private readonly TestDatabase _db;
public DatabaseTests() // runs before each test
{
_db = new TestDatabase();
}
public void Dispose() // runs after each test
{
_db.Dispose();
}
}
xUnit creates a new instance of the class for every test, so the constructor is your setup and there is no shared state to leak between tests. That is a deliberate design difference from NUnit and MSTest.
What to test#
Worth testing: business rules, calculations, validation, edge cases, boundaries, anything you have already had to fix once, and any bug you fix — write the failing test first, then make it pass.
Not worth testing: property getters and setters, framework behaviour, third-party libraries, and code that only wires other code together.
Running them#
dotnet test
dotnet test --filter "FullyQualifiedName~Calculator"
dotnet test --logger "console;verbosity=detailed"
dotnet test --collect:"XPlat Code Coverage"
Questions people ask#
xUnit, NUnit or MSTest?
All three are capable. xUnit is the common choice for new .NET projects and has the cleanest isolation model. Use whichever your team already uses — the differences are small.
What is the difference between a unit and an integration test?
A unit test exercises one piece in isolation and runs in milliseconds. An integration test exercises several pieces together, often including a real database. You want many of the first and a few of the second.
Should I write tests before the code?
Test-driven development works well for algorithms and business rules where you can state the expected result up front. It is more awkward for exploratory work. Writing the test first for every bug fix is valuable regardless of your general approach.
How do I test private methods?
Usually you do not — test them through the public method that calls them. If a private method is complex enough to need its own tests, that is a hint it wants to be its own class.
Where to go next#
- C# basics — the language underneath.
- C# advanced language features — interfaces, generics and async in tests.
- C# deployment — running tests before you ship.