Back to blog
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
Share:š•

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.csproj

The 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 abstractions

Running 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 change

Key Takeaways

  1. AAA pattern — Arrange (setup), Act (call the thing), Assert (verify result)
  2. Mock dependencies, not the SUT — test real logic with fake infrastructure
  3. One assertion concept per test — if two things need verifying, write two tests
  4. NSubstitute for mocking, FluentAssertions for readable assertions
  5. 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?

Share:š•

Leave a comment

Have a question, correction, or just found this helpful? Leave a note below.