Articles › C#
Unit Testing in .NET with xUnit and Moq: A Practical Guide
Write tests that catch real bugs: xUnit facts and theories, the Arrange-Act-Assert pattern, mocking with Moq, testing async code, integration tests with WebApplicationFactory and good test design.
Tests are the safety net that lets you change code without fear. A good suite catches regressions before users do, documents how the code is supposed to behave, and makes refactoring fast. This guide shows how to write effective tests in .NET with xUnit and Moq, from your first test to integration tests of a whole ASP.NET Core API.
Kinds of tests
| Type | Scope | Speed | Example |
|---|---|---|---|
| Unit | one class/method, dependencies replaced | milliseconds | discount calculation |
| Integration | several real components together | seconds | API endpoint + database |
| End-to-end | the whole system through the UI | slow | browser test of checkout |
The usual advice is a "pyramid": many fast unit tests, fewer integration tests, a handful of end-to-end tests.
Setting up
dotnet new xunit -n Shop.Tests
dotnet add Shop.Tests reference src/Shop.Application
dotnet add Shop.Tests package Moq
dotnet test
Your first test: Arrange, Act, Assert
public class PriceCalculator
{
public decimal Total(decimal price, int quantity, decimal discountPercent)
{
ArgumentOutOfRangeException.ThrowIfNegative(price);
ArgumentOutOfRangeException.ThrowIfNegativeOrZero(quantity);
var gross = price * quantity;
return Math.Round(gross - gross * discountPercent / 100, 2);
}
}
public class PriceCalculatorTests
{
[Fact]
public void Total_applies_discount_to_gross_amount()
{
// Arrange
var calculator = new PriceCalculator();
// Act
var total = calculator.Total(price: 200, quantity: 3, discountPercent: 10);
// Assert
Assert.Equal(540m, total);
}
}
The AAA structure keeps tests readable: set up, perform one action, verify the outcome.
Theories: one test, many inputs
[Theory]
[InlineData(100, 1, 0, 100)]
[InlineData(100, 2, 50, 100)]
[InlineData(19.99, 3, 0, 59.97)]
public void Total_calculates_correctly(decimal price, int qty, decimal discount, decimal expected)
{
var total = new PriceCalculator().Total(price, qty, discount);
Assert.Equal(expected, total);
}
[Fact]
public void Total_rejects_zero_quantity()
{
var calculator = new PriceCalculator();
Assert.Throws<ArgumentOutOfRangeException>(() => calculator.Total(10, 0, 0));
}
For complex inputs use [MemberData] or [ClassData].
Naming and structure
A good test name says what is tested, under which condition, and what is expected:
PlaceOrder_with_empty_cart_returns_failureGetBySlug_when_article_is_unpublished_returns_null
One logical assertion per test makes failures easy to diagnose.
Mocking dependencies with Moq
Unit tests replace dependencies (database, email, payment gateway) with test doubles, so tests are fast and deterministic:
public class OrderService(IOrderRepository orders, IPaymentGateway payments, IEmailSender email)
{
public async Task<Result> PlaceAsync(Order order, CancellationToken ct)
{
var payment = await payments.ChargeAsync(order.Total, ct);
if (!payment.Succeeded) return Result.Failure("Payment declined.");
orders.Add(order);
await orders.SaveAsync(ct);
await email.SendAsync(order.CustomerEmail, "Order confirmed", ct);
return Result.Success();
}
}
public class OrderServiceTests
{
private readonly Mock<IOrderRepository> _orders = new();
private readonly Mock<IPaymentGateway> _payments = new();
private readonly Mock<IEmailSender> _email = new();
private OrderService CreateSut() => new(_orders.Object, _payments.Object, _email.Object);
[Fact]
public async Task PlaceAsync_declined_payment_does_not_save_or_email()
{
_payments.Setup(p => p.ChargeAsync(It.IsAny<decimal>(), It.IsAny<CancellationToken>()))
.ReturnsAsync(PaymentResult.Declined());
var result = await CreateSut().PlaceAsync(new Order { Total = 100 }, default);
Assert.False(result.IsSuccess);
_orders.Verify(o => o.Add(It.IsAny<Order>()), Times.Never);
_email.Verify(e => e.SendAsync(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<CancellationToken>()), Times.Never);
}
[Fact]
public async Task PlaceAsync_successful_payment_saves_and_sends_confirmation()
{
_payments.Setup(p => p.ChargeAsync(250, It.IsAny<CancellationToken>()))
.ReturnsAsync(PaymentResult.Success("tx-1"));
var order = new Order { Total = 250, CustomerEmail = "sara@example.com" };
var result = await CreateSut().PlaceAsync(order, default);
Assert.True(result.IsSuccess);
_orders.Verify(o => o.SaveAsync(It.IsAny<CancellationToken>()), Times.Once);
_email.Verify(e => e.SendAsync("sara@example.com", "Order confirmed", It.IsAny<CancellationToken>()), Times.Once);
}
}
Key Moq features:
Setup(...).Returns/ReturnsAsync/Throws— define behavior.It.IsAny<T>(),It.Is<T>(predicate)— argument matchers.Verify(..., Times.Once)— assert an interaction happened.Callback(...)— capture arguments for detailed assertions.
Tip: Mock what you don't own through your own interfaces (payment gateway, clock, email). Don't mock everything: simple value objects and pure logic should be used for real.
Test doubles without a library
For simple dependencies a hand-written fake is often clearer than a mock:
public class FakeClock(DateTimeOffset now) : TimeProvider
{
public override DateTimeOffset GetUtcNow() => now;
}
.NET 8's TimeProvider (and FakeTimeProvider in Microsoft.Extensions.TimeProvider.Testing) makes time-dependent code testable without static DateTime.Now calls.
Testing async code
xUnit supports async Task tests natively:
[Fact]
public async Task GetAsync_unknown_id_throws()
{
var sut = new ArticleService(new InMemoryArticleStore());
await Assert.ThrowsAsync<NotFoundException>(() => sut.GetAsync("missing", default));
}
Never use async void in tests — failures would be lost.
Setup, teardown and shared fixtures
- The constructor runs before each test;
IDisposable.Disposeruns after — xUnit creates a new instance per test, so tests are isolated. IClassFixture<T>shares an expensive object (e.g. a database container) across tests in one class.ICollectionFixture<T>shares it across several classes.
Integration tests for ASP.NET Core
WebApplicationFactory boots your real application in memory and gives you an HttpClient:
dotnet add package Microsoft.AspNetCore.Mvc.Testing
public class ArticlesApiTests(WebApplicationFactory<Program> factory)
: IClassFixture<WebApplicationFactory<Program>>
{
[Fact]
public async Task Get_unknown_article_returns_404()
{
var client = factory.CreateClient();
var response = await client.GetAsync("/api/articles/does-not-exist");
Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);
}
}
Replace services for tests with WithWebHostBuilder(b => b.ConfigureServices(...)) — for example, point the DbContext at a test database. For realistic database behavior, use a real SQL Server in Docker via Testcontainers rather than the EF Core in-memory provider.
(Add public partial class Program { } at the end of Program.cs if the test project can't see the Program class.)
What makes a good test
- Fast — the whole unit suite runs in seconds, so developers run it often.
- Deterministic — no dependence on time, randomness, network or test order.
- Independent — each test sets up its own data.
- Readable — a failing test should explain the problem without debugging.
- Tests behavior, not implementation — assert on outcomes, not private details, so refactoring doesn't break tests.
Common mistakes
- Testing trivial getters/setters instead of business rules.
- Over-mocking — tests that mirror the implementation line by line break on every refactor.
- Shared mutable state between tests.
- Ignoring flaky tests instead of fixing them.
- Chasing 100% coverage instead of covering important behavior and edge cases.
Summary
Use xUnit facts and theories with the Arrange-Act-Assert pattern, replace external dependencies with Moq or simple fakes, test async code with async Task, and add integration tests with WebApplicationFactory for your API's critical paths. Keep tests fast, deterministic and focused on behavior, and they'll give you the confidence to change your code every day.
FAQ
xUnit, NUnit or MSTest?
All three are mature. xUnit is the most common in modern .NET and is used by the .NET team itself; pick one and stay consistent.
What code coverage should I aim for?
Coverage is a hint, not a goal. Aim to cover business rules and risky paths thoroughly; 70–80% overall is a common healthy range.
Should I use the EF Core in-memory provider for tests?
It's fine for very simple cases, but it doesn't behave like a relational database (no constraints, different query translation). Prefer SQLite in-memory or a real database container.