Unit Testing, xUnit, Moq & Integration Tests Masterclass

โšก C# 12 & .NET 8 ๐ŸŸข Chapter 41 of 35 ๐Ÿ“‚ Phase 15: Testing & Professional Development ๐Ÿ“… 2026 Edition
๐Ÿ“Œ Covered in this chapter: Testing Pyramid ยท Unit Tests ยท xUnit ยท [Fact] ยท [Theory] ยท Moq ยท Setup/ReturnsAsync ยท FluentAssertions ยท Integration Tests ยท WebApplicationFactory

Welcome to Phase 15 (Chapter 41): C# Unit Testing, xUnit, Moq & Integration Tests Masterclass! Professional C# development requires a robust automated test suite. In this chapter, we master the three levels of testing (Unit, Integration, End-to-End), write unit tests with xUnit, use Moq for dependency mocking, assert expected outcomes, organize tests with fixtures, test ASP.NET Core controllers and services, and run API integration tests with WebApplicationFactory.

1Testing Pyramid โ€” Unit vs Integration vs E2E
Testing Pyramid: / /E2E โ† Few | Slow | End-to-End Browser Tests (Playwright, Selenium) /------ / Integ โ† Some | Medium | Integration Tests (DB, HTTP endpoints) /---------- / Unit Tests โ† Many | Fast | Isolated function/class tests (xUnit + Moq) /--------------
Test LevelWhat is TestedSpeedTool
Unit TestSingle function/method in isolation with mocked dependenciesVery Fast (ms)xUnit + Moq
Integration TestMultiple components together (service + real DB / HTTP endpoints)Medium (seconds)WebApplicationFactory + xUnit
E2E TestFull application flow from UI to database (user journey)Slow (minutes)Playwright, Selenium
2Writing Unit Tests with xUnit
Terminal โ€” Add xUnit & Moq Packages
# Create test project
dotnet new xunit -n ProductsAPI.Tests

# Add reference to main project
dotnet add ProductsAPI.Tests/ProductsAPI.Tests.csproj reference ProductsAPI/ProductsAPI.csproj

# Add Moq and FluentAssertions
dotnet add ProductsAPI.Tests package Moq
dotnet add ProductsAPI.Tests package FluentAssertions
C# โ€” xUnit Unit Tests โ–ถ Run in Compiler
using Xunit;
using Moq;
using FluentAssertions;

public class ProductServiceTests
{
    private readonly Mock<IProductRepository> _mockRepo;
    private readonly ProductService _service;

    public ProductServiceTests()
    {
        _mockRepo = new Mock<IProductRepository>();
        _service  = new ProductService(_mockRepo.Object); // Inject mock
    }

    [Fact]
    public async Task GetProductById_WhenExists_ReturnsProduct()
    {
        // Arrange โ€” Setup mock to return a specific product
        var expectedProduct = new Product { Id = 1, Name = "Laptop", Price = 75000m };
        _mockRepo.Setup(r => r.GetByIdAsync(1)).ReturnsAsync(expectedProduct);

        // Act โ€” Call the method under test
        var result = await _service.GetByIdAsync(1);

        // Assert โ€” Verify the result
        result.Should().NotBeNull();
        result!.Id.Should().Be(1);
        result.Name.Should().Be("Laptop");
        result.Price.Should().Be(75000m);
    }

    [Fact]
    public async Task GetProductById_WhenNotFound_ReturnsNull()
    {
        _mockRepo.Setup(r => r.GetByIdAsync(999)).ReturnsAsync((Product?)null);

        var result = await _service.GetByIdAsync(999);

        result.Should().BeNull();
    }

    [Theory]                           // Data-driven test
    [InlineData(-1)]
    [InlineData(0)]
    public async Task CreateProduct_WithInvalidPrice_ThrowsException(decimal price)
    {
        var dto = new CreateProductDto { Name = "Test", Price = price, Stock = 1 };

        Func<Task> act = async () => await _service.CreateAsync(dto);

        await act.Should().ThrowAsync<ArgumentException>()
            .WithMessage("*price*");
    }
}
3Integration Tests with WebApplicationFactory
C# โ€” API Integration Test โ–ถ Run in Compiler
using Microsoft.AspNetCore.Mvc.Testing;

public class ProductsControllerIntegrationTests : IClassFixture<WebApplicationFactory<Program>>
{
    private readonly HttpClient _client;

    public ProductsControllerIntegrationTests(WebApplicationFactory<Program> factory)
    {
        _client = factory.CreateClient(); // Creates in-memory HTTP test client
    }

    [Fact]
    public async Task GetProducts_ReturnsOkWithProductsList()
    {
        // Act โ€” Real HTTP call to in-memory API
        var response = await _client.GetAsync("/api/products");

        // Assert
        response.StatusCode.Should().Be(HttpStatusCode.OK);
        var content = await response.Content.ReadAsStringAsync();
        content.Should().Contain("Laptop");
    }
}

// Run all tests
// dotnet test
4Technical FAQs

Q1: What is the difference between [Fact] and [Theory] in xUnit?

[Fact] marks a test that runs once with no parameters. [Theory] marks a data-driven test that runs multiple times with different input values provided via [InlineData(...)] or [MemberData(...)] attributes.

Q2: Why use Moq instead of real dependencies in unit tests?

Mocks replace real dependencies (database, HTTP clients, email services) with controllable fakes, making tests: (1) Fast โ€” no actual DB I/O, (2) Isolated โ€” failures pinpoint exact code, (3) Deterministic โ€” same input always produces same output, (4) Easy to simulate error conditions.