Testing with xUnit

A complete xUnit test class with [Fact] and [Theory], and mocking dependencies with Moq.

Why xUnit

xUnit.net is the de facto standard unit testing framework for modern .NET — MSTest and NUnit are both still valid, actively maintained choices, but dotnet new xunit and the overwhelming majority of new open-source .NET projects default to xUnit, so it's the one worth learning first. The core ideas map directly onto JUnit in Java or Jest in JavaScript: test classes containing test methods, an assertion library, and a runner that discovers and executes them.

Setting up a test project

Test code lives in its own project, separate from the code it tests, referencing that project like any other dependency:

Bash
dotnet new xunit -o OrderService.Tests
cd OrderService.Tests
dotnet add reference ../OrderService/OrderService.csproj
dotnet add package Moq

The generated .csproj already wires up everything needed to run tests: the xUnit framework itself, xunit.runner.visualstudio (so tests show up in IDEs and CI), and Microsoft.NET.Test.Sdk. Moq (added above) is the mocking library covered later on this page.

The service under test

This page tests the OrderService built up on the dependency injection page in this track — a class depending on two abstractions, IClock and IOrderRepository, injected through its constructor:

C#
public record Order(string Product, int Quantity, DateTime PlacedAt);

public interface IClock
{
    DateTime UtcNow { get; }
}

public interface IOrderRepository
{
    void Save(Order order);
}

public class OrderService
{
    private readonly IClock _clock;
    private readonly IOrderRepository _repository;

    public OrderService(IClock clock, IOrderRepository repository)
    {
        _clock = clock;
        _repository = repository;
    }

    public Order PlaceOrder(string product, int quantity)
    {
        if (quantity <= 0)
        {
            throw new ArgumentOutOfRangeException(nameof(quantity), "Quantity must be positive");
        }

        var order = new Order(product, quantity, _clock.UtcNow);
        _repository.Save(order);
        return order;
    }
}

This is exactly the shape of class DI is meant for: because OrderService only knows about interfaces, a test can supply fakes for both dependencies instead of a real clock or a real database.

Fact tests: verifying one specific behavior

A [Fact] is a test that's always run the same way, with no parameters — used to verify one concrete, unconditional behavior:

C#
using Moq;
using Xunit;

public class OrderServiceTests
{
    [Fact]
    public void PlaceOrder_ReturnsOrderWithGivenProductAndQuantity()
    {
        // Arrange
        var clock = new Mock<IClock>();
        clock.Setup(c => c.UtcNow).Returns(new DateTime(2026, 1, 1));
        var repository = new Mock<IOrderRepository>();
        var sut = new OrderService(clock.Object, repository.Object);

        // Act
        var order = sut.PlaceOrder("Keyboard", 2);

        // Assert
        Assert.Equal("Keyboard", order.Product);
        Assert.Equal(2, order.Quantity);
        Assert.Equal(new DateTime(2026, 1, 1), order.PlacedAt);
    }

    [Fact]
    public void PlaceOrder_SavesTheOrderExactlyOnce()
    {
        var clock = new Mock<IClock>();
        clock.Setup(c => c.UtcNow).Returns(DateTime.UtcNow);
        var repository = new Mock<IOrderRepository>();
        var sut = new OrderService(clock.Object, repository.Object);

        sut.PlaceOrder("Monitor", 1);

        repository.Verify(r => r.Save(It.IsAny<Order>()), Times.Once);
    }
}

sut (short for "system under test") is a common naming convention for the object a test is actually exercising, keeping it visually distinct from its mocked collaborators. The Arrange / Act / Assert structure in the first test — set up the world, do the one thing being tested, check the outcome — is the standard shape of a well-written unit test in any language.

Theory tests: the same logic, many inputs

A [Theory] runs the same test method once per row of supplied data — ideal when a rule should hold across a whole range of inputs rather than just one example:

C#
public class OrderServiceValidationTests
{
    [Theory]
    [InlineData(0)]
    [InlineData(-1)]
    [InlineData(-100)]
    public void PlaceOrder_RejectsNonPositiveQuantity(int invalidQuantity)
    {
        var clock = new Mock<IClock>();
        var repository = new Mock<IOrderRepository>();
        var sut = new OrderService(clock.Object, repository.Object);

        Assert.Throws<ArgumentOutOfRangeException>(() => sut.PlaceOrder("Keyboard", invalidQuantity));
    }
}

Each [InlineData(...)] attribute supplies one row of arguments; the test runner reports each row as its own separate test result (PlaceOrder_RejectsNonPositiveQuantity(invalidQuantity: 0), and so on), so a failure on one input doesn't hide whether the others passed.

Mocking with Moq

Moq creates fake implementations of an interface (or a virtual member of a class) at test time, without hand-writing a stub class for every dependency. Two pieces come up constantly:

  • .Setup(...).Returns(...) — tells the mock what to return when a specific member is called, as with clock.Setup(c => c.UtcNow).Returns(...) above. Without a matching Setup, a mocked member returns a harmless default (null, 0, false) rather than throwing.
  • .Verify(...) — asserts, after the fact, that a specific call actually happened (and optionally, how many times) — used above to confirm Save was called exactly once, something a plain return-value assertion can't check on its own.
  • It.IsAny<Order>() — a Moq matcher meaning "any Order value satisfies this," used when the exact argument isn't what's being tested.

Common mistakes

  • Writing a test that calls the method under test but never actually asserts anything — it "passes" as long as nothing throws, without checking any real behavior.
  • Mocking a concrete class with no interface and no virtual members — Moq generates its fake by overriding members at runtime, so it needs either an interface or virtual methods to intercept; a sealed class or non-virtual methods can't be mocked this way.
  • Reaching for Verify() on every single interaction out of habit, even when a plain state-based assertion (checking a return value) would test the same behavior more simply and with less coupling to how the method happens to be implemented internally.

Interview questions

Q: What's the difference between [Fact] and [Theory] in xUnit? A [Fact] is a single test with no parameters, verifying one specific behavior unconditionally. A [Theory], paired with a data source like [InlineData], runs the same test method once per supplied row of arguments — useful for confirming a rule holds across many inputs without duplicating the test method itself.

Q: What's the purpose of a mocking library like Moq in a unit test? It creates a lightweight fake implementation of a dependency's interface so a test can isolate the class under test from real collaborators — a database, a clock, a network call — without needing them to actually run. Setup controls what the fake returns; Verify confirms a specific interaction with it actually happened, which a plain assertion on a return value can't check.