Backend Systemsbeginner
Unit Testing with xUnit ā Arrange, Act, Assert
Write fast, reliable unit tests in .NET with xUnit. Learn AAA pattern, mocking with NSubstitute, test data with TheoryData, and what makes a good unit test.
Asma HafeezApril 17, 20263 min read
dotnettestingxunitunit-testsmocking
Unit Testing with xUnit
Unit tests verify individual pieces of logic in isolation. They run in milliseconds and give you confidence to refactor without breaking things.
Setup
Bash
dotnet new xunit -n MyApp.Tests
cd MyApp.Tests
dotnet add package NSubstitute # mocking
dotnet add package FluentAssertions # readable assertions
dotnet add reference ../MyApp/MyApp.csprojThe AAA Pattern
Every test follows Arrange ā Act ā Assert.
C#
[Fact]
public void Add_TwoPositiveNumbers_ReturnsCorrectSum()
{
// Arrange
var calculator = new Calculator();
// Act
var result = calculator.Add(3, 4);
// Assert
Assert.Equal(7, result);
}Test name convention: MethodName_Scenario_ExpectedBehavior
xUnit Basics
C#
public class CalculatorTests
{
private readonly Calculator _sut = new(); // sut = System Under Test
[Fact] // single test case
public void Divide_ByNonZero_ReturnsQuotient()
{
var result = _sut.Divide(10, 2);
Assert.Equal(5.0, result);
}
[Fact]
public void Divide_ByZero_ThrowsException()
{
var act = () => _sut.Divide(10, 0);
Assert.Throws<DivideByZeroException>(act);
}
[Theory] // multiple test cases
[InlineData(2, true)]
[InlineData(3, false)]
[InlineData(4, true)]
[InlineData(100, true)]
public void IsEven_ReturnsExpectedResult(int n, bool expected)
{
var result = _sut.IsEven(n);
Assert.Equal(expected, result);
}
}FluentAssertions ā Readable Assertions
C#
using FluentAssertions;
result.Should().Be(42);
result.Should().BeGreaterThan(0);
result.Should().BeNull();
result.Should().NotBeNull();
result.Should().BeOfType<Product>();
// Collections
list.Should().HaveCount(3);
list.Should().Contain(item);
list.Should().BeInAscendingOrder(x => x.Price);
list.Should().AllSatisfy(p => p.IsActive.Should().BeTrue());
// Strings
str.Should().StartWith("Hello");
str.Should().Contain("World");
// Exceptions
action.Should().Throw<ArgumentException>()
.WithMessage("*cannot be null*");Mocking with NSubstitute
C#
using NSubstitute;
public class OrderServiceTests
{
private readonly IOrderRepository _repo = Substitute.For<IOrderRepository>();
private readonly IEmailService _email = Substitute.For<IEmailService>();
private readonly OrderService _sut;
public OrderServiceTests()
{
_sut = new OrderService(_repo, _email);
}
[Fact]
public async Task PlaceOrder_ValidOrder_SavesAndSendsEmail()
{
// Arrange
var order = new Order { Id = 1, CustomerId = 42, Total = 150m };
_repo.SaveAsync(Arg.Any<Order>()).Returns(order);
// Act
await _sut.PlaceOrderAsync(order);
// Assert ā verify interactions
await _repo.Received(1).SaveAsync(Arg.Is<Order>(o => o.Total == 150m));
await _email.Received(1).SendConfirmationAsync(order.CustomerId);
}
[Fact]
public async Task PlaceOrder_RepositoryFails_ThrowsException()
{
// Arrange
_repo.SaveAsync(Arg.Any<Order>())
.Throws(new InvalidOperationException("DB error"));
// Act & Assert
var act = () => _sut.PlaceOrderAsync(new Order());
await act.Should().ThrowAsync<InvalidOperationException>();
await _email.DidNotReceive().SendConfirmationAsync(Arg.Any<int>());
}
}Testing with Theory and MemberData
C#
public class PricingTests
{
public static TheoryData<decimal, int, decimal> DiscountCases => new()
{
{ 100m, 10, 90m }, // 10% off
{ 200m, 25, 150m }, // 25% off
{ 50m, 0, 50m }, // no discount
{ 100m, 100, 0m }, // 100% off
};
[Theory]
[MemberData(nameof(DiscountCases))]
public void ApplyDiscount_ReturnsCorrectPrice(decimal price, int discount, decimal expected)
{
var result = PricingService.ApplyDiscount(price, discount);
result.Should().Be(expected);
}
}What Makes a Good Unit Test
ā Tests ONE thing ā if it fails, you know exactly what broke
ā Fast ā < 10ms per test (no database, no HTTP, no file system)
ā Isolated ā doesn't depend on other tests or external state
ā Deterministic ā same result every run
ā Readable ā test name explains what's being tested and why
ā Don't test private methods ā test the public behavior
ā Don't mock types you own ā only mock external dependencies
ā Don't use Thread.Sleep ā use fake time abstractionsRunning Tests
Bash
dotnet test # run all tests
dotnet test --filter "Category=Unit" # filter by category
dotnet test --logger "console;verbosity=detailed"
dotnet watch test # re-run on file changeKey Takeaways
- AAA pattern ā Arrange (setup), Act (call the thing), Assert (verify result)
- Mock dependencies, not the SUT ā test real logic with fake infrastructure
- One assertion concept per test ā if two things need verifying, write two tests
- NSubstitute for mocking, FluentAssertions for readable assertions
- If a unit test is hard to write, the code is probably hard to test ā simplify the design
Enjoyed this article?
Explore the Backend Systems learning path for more.
Found this helpful?
Leave a comment
Have a question, correction, or just found this helpful? Leave a note below.