Unit Testing, xUnit, Moq & Integration Tests Masterclass
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.
| Test Level | What is Tested | Speed | Tool |
|---|---|---|---|
| Unit Test | Single function/method in isolation with mocked dependencies | Very Fast (ms) | xUnit + Moq |
| Integration Test | Multiple components together (service + real DB / HTTP endpoints) | Medium (seconds) | WebApplicationFactory + xUnit |
| E2E Test | Full application flow from UI to database (user journey) | Slow (minutes) | Playwright, Selenium |
# 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
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*");
}
}
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
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.