06 · Testing Advanced (Moq, integration tests)¶
Level 1/2 covered basic unit tests with xUnit. This module adds mocking with
Moq, Theory/InlineData for parameterized tests, and integration tests
against a real ASP.NET Core pipeline with WebApplicationFactory.
Project setup¶
dotnet new xunit -o OrderService.Tests
cd OrderService.Tests
dotnet add package Moq
dotnet add reference ../OrderService/OrderService.csproj
Mocking dependencies with Moq¶
public interface IOrderRepository
{
void Save(Order order);
Order? Find(Guid id);
}
public record Order(Guid Id, string Customer, decimal Total);
public class OrderService
{
private readonly IOrderRepository _repository;
public OrderService(IOrderRepository repository) => _repository = repository;
public Order Place(string customer, decimal total)
{
if (total <= 0) throw new ArgumentException("Total must be positive.", nameof(total));
var order = new Order(Guid.NewGuid(), customer, total);
_repository.Save(order);
return order;
}
}
using Moq;
using Xunit;
public class OrderServiceTests
{
[Fact]
public void Place_SavesOrderToRepository()
{
var repositoryMock = new Mock<IOrderRepository>();
var service = new OrderService(repositoryMock.Object);
var order = service.Place("Alice", 49.99m);
repositoryMock.Verify(r => r.Save(It.Is<Order>(o => o.Customer == "Alice" && o.Total == 49.99m)), Times.Once);
Assert.Equal("Alice", order.Customer);
}
[Fact]
public void Place_ZeroTotal_Throws()
{
var repositoryMock = new Mock<IOrderRepository>();
var service = new OrderService(repositoryMock.Object);
Assert.Throws<ArgumentException>(() => service.Place("Bob", 0m));
repositoryMock.Verify(r => r.Save(It.IsAny<Order>()), Times.Never);
}
}
Mock<IOrderRepository>() creates a fake implementation with no real
storage. .Object hands the fake to the code under test. Verify(...,
Times.Once) asserts the mocked method was called exactly once with an
argument matching the It.Is<Order>(...) predicate — this checks
behavior (was Save called correctly) rather than state.
Stubbing return values with Setup¶
[Fact]
public void Find_ReturnsRepositoryResult()
{
var existing = new Order(Guid.NewGuid(), "Carol", 10m);
var repositoryMock = new Mock<IOrderRepository>();
repositoryMock.Setup(r => r.Find(existing.Id)).Returns(existing);
var found = repositoryMock.Object.Find(existing.Id);
Assert.Equal(existing, found);
}
Setup(...).Returns(...) tells the mock what to return for a given call
shape; any call not matching a Setup returns the type's default (null
here) instead of throwing, unless MockBehavior.Strict is used.
Theory and InlineData for parameterized tests¶
public class DiscountCalculatorTests
{
[Theory]
[InlineData(100, 0, 100)]
[InlineData(100, 10, 90)]
[InlineData(50, 50, 25)]
public void ApplyPercentage_ComputesExpectedTotal(decimal subtotal, decimal percent, decimal expected)
{
decimal result = subtotal * (1 - percent / 100m);
Assert.Equal(expected, result);
}
[Theory]
[MemberData(nameof(InvalidPercentages))]
public void ApplyPercentage_RejectsOutOfRange(decimal percent)
{
Assert.Throws<ArgumentOutOfRangeException>(() =>
{
if (percent is < 0 or > 100) throw new ArgumentOutOfRangeException(nameof(percent));
});
}
public static IEnumerable<object[]> InvalidPercentages =>
new List<object[]> { new object[] { -1m }, new object[] { 101m } };
}
One [Theory] method with several [InlineData] rows runs as several
distinct test cases in the test explorer/CLI output — far less duplication
than copy-pasting a [Fact] per case. [MemberData] pulls cases from a
static property when they're too complex for inline literals.
Integration testing an ASP.NET Core app¶
// In the API project's Program.cs, add at the very end so the test project can see it:
// public partial class Program { }
using Microsoft.AspNetCore.Mvc.Testing;
using System.Net;
using Xunit;
public class ProductsApiTests : IClassFixture<WebApplicationFactory<Program>>
{
private readonly WebApplicationFactory<Program> _factory;
public ProductsApiTests(WebApplicationFactory<Program> factory) => _factory = factory;
[Fact]
public async Task GetProducts_ReturnsOk()
{
var client = _factory.CreateClient();
var response = await client.GetAsync("/products");
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
}
[Fact]
public async Task GetProduct_UnknownId_ReturnsNotFound()
{
var client = _factory.CreateClient();
var response = await client.GetAsync("/products/999999");
Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);
}
}
WebApplicationFactory<Program> spins up the whole app — routing,
middleware, DI container — in memory, backed by an in-process TestServer.
IClassFixture<T> shares one factory instance across all tests in the class
(the app starts once, not per test), which keeps the suite fast.
Swapping real services for test doubles in integration tests¶
public class ProductsApiWithFakeDbTests : IClassFixture<WebApplicationFactory<Program>>
{
private readonly WebApplicationFactory<Program> _factory;
public ProductsApiWithFakeDbTests(WebApplicationFactory<Program> factory)
{
_factory = factory.WithWebHostBuilder(builder =>
{
builder.ConfigureServices(services =>
{
services.RemoveAll<IProductRepository>();
services.AddSingleton<IProductRepository, InMemoryProductRepository>();
});
});
}
[Fact]
public async Task GetProducts_UsesFakeRepository()
{
var client = _factory.CreateClient();
var response = await client.GetAsync("/products");
response.EnsureSuccessStatusCode();
}
}
WithWebHostBuilder lets a test replace real registrations (a SQL-backed
repository, an external email sender) with in-memory fakes before the app
boots, so integration tests stay hermetic — no real database or network
calls needed.
How It Actually Works¶
Mock<IOrderRepository>()generates a real class implementingIOrderRepositoryat run time, usingSystem.Reflection.Emit(orDispatchProxy-style dynamic type generation) — Moq is not simulating the interface, it is compiling one on the fly. The first time you mock a given interface, Moq'sCastle.DynamicProxydependency emits IL for a new type into a dynamically generated assembly loaded into your process, implementing every interface member by forwarding into Moq's own interception logic;.Objecthands you an actual instance of that generated type. This is exactly why Moq can only mock interfaces and non-sealed classes with virtual members — it needs a real vtable slot (Module 1's dispatch mechanism) to intercept, andsealed/non-virtual members give it nothing to override.Setup(...)andVerify(...)both work against a recorded call log, matched by the same expression-tree machinery EF Core'sIQueryablerelies on.r => r.Find(existing.Id)is captured as anExpression<Func<...>>, not executed directly — Moq walks that expression to identify which method and argument pattern you're describing, then configures the dynamically generated proxy to recognize matching calls at invocation time and return the stubbed value.Verifyreplays the same matching logic against Moq's internal invocation history (every call the proxy actually received, recorded as it happened) to check the count matchesTimes.Once/Times.Never.WebApplicationFactory<Program>builds and hosts a genuine, in-memory ASP.NET CoreTestServer— the same middleware pipeline construction from Module 1, just fed requests through an in-memory transport instead of a real Kestrel socket.CreateClient()returns anHttpClientwired to a customHttpMessageHandlerthat hands requests directly toTestServer's in-process pipeline, bypassing TCP/sockets entirely — every route match, DI resolution, and middleware execution genuinely happens, just without the network layer, which is why integration tests through it exercise real bugs (a broken route constraint, a misconfigured DI registration) that a pure unit test mocking everything out could never catch.IClassFixture<T>triggers the factory's construction exactly once for the whole test class, unlike the per-test-method constructor behavior covered in Module 06 of Level 2 — xUnit recognizes theIClassFixture<T>interface specifically and special-cases fixture lifetime to span the class, callingDispose()on the fixture only after every test in the class has run, which is the real reason "the app starts once, not per test" and is measurably faster for a suite with many integration tests sharing one hosted app.RemoveAll<IProductRepository>()mutates theIServiceCollectionbeforeBuild()runs, replacing the registration descriptor entirely — this works because DI registrations are just data (a list of service-type → implementation/factory/lifetime descriptors) untilBuild()compiles them into the resolvable container Module 03 described; swapping a descriptor before that point changes what the finished container will ever construct for that interface.
Exercise¶
Given an IPaymentGateway interface with Task<bool> ChargeAsync(decimal
amount), write unit tests for a CheckoutService that: (1) mocks a
successful charge and asserts an order is marked Paid; (2) mocks a failed
charge (Returns(false) — or ThrowsAsync for a gateway exception) and
asserts the order is marked Failed and no confirmation email is sent
(verify a mocked IEmailSender.Send was Times.Never called). Then add one
WebApplicationFactory integration test hitting a POST /checkout endpoint
with the real gateway swapped for a fake that always succeeds.