Skip to content

Unit Testing in C#

The .NET ecosystem has three mature test frameworks. xUnit is the most popular for new projects.

FrameworkNuGet PackageAttribute
xUnitxunit[Fact], [Theory]
NUnitNUnit[Test], [TestCase]
MSTestMSTest.TestFramework[TestMethod], [DataRow]
Terminal window
dotnet new classlib -n MyApp
dotnet new xunit -n MyApp.Tests
cd MyApp.Tests
dotnet add reference ../MyApp/MyApp.csproj
dotnet add package Moq
dotnet add package FluentAssertions
using Xunit;
public class CalculatorTests
{
private readonly Calculator _sut; // system under test
public CalculatorTests()
{
_sut = new Calculator();
}
[Fact]
public void Add_TwoPositiveNumbers_ReturnsCorrectSum()
{
// Arrange
var a = 5;
var b = 3;
// Act
var result = _sut.Add(a, b);
// Assert
Assert.Equal(8, result);
}
[Theory]
[InlineData(0, 5, 5)]
[InlineData(-1, 1, 0)]
[InlineData(100, -50, 50)]
public void Add_VariousInputs_ReturnsExpectedResult(int a, int b, int expected)
{
var result = _sut.Add(a, b);
Assert.Equal(expected, result);
}
}

More readable assertions:

using FluentAssertions;
result.Should().Be(8);
result.Should().BeGreaterThan(0);
items.Should().HaveCount(3);
items.Should().Contain("apple");
items.Should().BeInAscendingOrder();
action.Should().Throw<ArgumentException>()
.WithMessage("*cannot be null*");

Mock dependencies so unit tests don’t touch databases or external APIs:

using Moq;
public class OrderServiceTests
{
[Fact]
public async Task PlaceOrder_WhenProductExists_ReturnsOrderId()
{
// Arrange
var productRepo = new Mock<IProductRepository>();
var orderRepo = new Mock<IOrderRepository>();
productRepo
.Setup(r => r.GetByIdAsync(42))
.ReturnsAsync(new Product { Id = 42, Name = "Widget", Price = 9.99m });
orderRepo
.Setup(r => r.SaveAsync(It.IsAny<Order>()))
.ReturnsAsync(Guid.NewGuid());
var service = new OrderService(productRepo.Object, orderRepo.Object);
// Act
var orderId = await service.PlaceOrderAsync(userId: 1, productId: 42, quantity: 2);
// Assert
orderId.Should().NotBeEmpty();
orderRepo.Verify(r => r.SaveAsync(It.IsAny<Order>()), Times.Once);
}
[Fact]
public async Task PlaceOrder_WhenProductNotFound_ThrowsNotFoundException()
{
var productRepo = new Mock<IProductRepository>();
productRepo
.Setup(r => r.GetByIdAsync(It.IsAny<int>()))
.ReturnsAsync((Product?)null);
var service = new OrderService(productRepo.Object, new Mock<IOrderRepository>().Object);
var act = () => service.PlaceOrderAsync(1, 999, 1);
await act.Should().ThrowAsync<NotFoundException>()
.WithMessage("*Product 999 not found*");
}
}
// xUnit
Assert.Throws<ArgumentNullException>(() => new UserService(null));
// FluentAssertions
Action act = () => new UserService(null);
act.Should().Throw<ArgumentNullException>()
.WithParameterName("repository");
// Async exceptions
await act.Should().ThrowAsync<HttpRequestException>();
public class DatabaseTests : IAsyncLifetime
{
private TestDatabase _db;
public async Task InitializeAsync()
{
_db = await TestDatabase.CreateAsync();
}
public async Task DisposeAsync()
{
await _db.DisposeAsync();
}
[Fact]
public async Task CanInsertUser()
{
await _db.Users.AddAsync(new User { Name = "Alice" });
var count = await _db.Users.CountAsync();
count.Should().Be(1);
}
}
Terminal window
dotnet test
dotnet test --filter "FullyQualifiedName~OrderService" # filter by name
dotnet test --logger "console;verbosity=detailed"
dotnet test /p:CollectCoverage=true # with coverage