Learn to test ASP.NET Core: unit tests, integration tests with WebApplicationFactory, dependency replacement, and outbound HTTP mocking.
Testing ASP.NET Core applications requires both unit tests for isolated components and integration tests that exercise the full HTTP pipeline. WebApplicationFactory creates an in-memory test server, enabling realistic integration tests without deploying to a real server.
Unit test controllers by injecting mocked services:
1// ProductsController.cs
2[ApiController]
3[Route("api/[controller]")]
4public class ProductsController : ControllerBase
5{
6 private readonly IProductService _productService;
7 private readonly ILogger<ProductsController> _logger;
8
9 public ProductsController(IProductService productService, ILogger<ProductsController> logger)
10 {
11 _productService = productService;
12 _logger = logger;
13 }
14
15 [HttpGet("{id}")]
16 public async Task<ActionResult<ProductDto>> GetById(int id)
17 {
18 var product = await _productService.GetByIdAsync(id);
19 if (product == null) return NotFound();
20 return Ok(product);
21 }
22
23 [HttpPost]
24 public async Task<ActionResult<ProductDto>> Create(CreateProductDto dto)
25 {
26 var product = await _productService.CreateAsync(dto);
27 return CreatedAtAction(nameof(GetById), new { id = product.Id }, product);
28 }
29}
30
31// Unit tests with xUnit and Moq
32public class ProductsControllerTests
33{
34 private readonly Mock<IProductService> _mockService;
35 private readonly Mock<ILogger<ProductsController>> _mockLogger;
36 private readonly ProductsController _controller;
37
38 public ProductsControllerTests()
39 {
40 _mockService = new Mock<IProductService>();
41 _mockLogger = new Mock<ILogger<ProductsController>>();
42 _controller = new ProductsController(_mockService.Object, _mockLogger.Object);
43 }
44
45 [Fact]
46 public async Task GetById_ExistingProduct_ReturnsOkWithProduct()
47 {
48 // Arrange
49 var product = new ProductDto { Id = 1, Name = "Test Product", Price = 99.99m };
50 _mockService.Setup(s => s.GetByIdAsync(1)).ReturnsAsync(product);
51
52 // Act
53 var result = await _controller.GetById(1);
54
55 // Assert
56 var okResult = Assert.IsType<OkObjectResult>(result.Result);
57 var returnedProduct = Assert.IsType<ProductDto>(okResult.Value);
58 Assert.Equal(1, returnedProduct.Id);
59 Assert.Equal("Test Product", returnedProduct.Name);
60 }
61
62 [Fact]
63 public async Task GetById_NonExistingProduct_ReturnsNotFound()
64 {
65 // Arrange
66 _mockService.Setup(s => s.GetByIdAsync(999)).ReturnsAsync((ProductDto?)null);
67
68 // Act
69 var result = await _controller.GetById(999);
70
71 // Assert
72 Assert.IsType<NotFoundResult>(result.Result);
73 }
74
75 [Fact]
76 public async Task Create_ValidProduct_ReturnsCreatedAtAction()
77 {
78 // Arrange
79 var createDto = new CreateProductDto { Name = "New Product", Price = 49.99m };
80 var createdProduct = new ProductDto { Id = 1, Name = "New Product", Price = 49.99m };
81 _mockService.Setup(s => s.CreateAsync(createDto)).ReturnsAsync(createdProduct);
82
83 // Act
84 var result = await _controller.Create(createDto);
85
86 // Assert
87 var createdResult = Assert.IsType<CreatedAtActionResult>(result.Result);
88 Assert.Equal(nameof(ProductsController.GetById), createdResult.ActionName);
89 Assert.Equal(1, createdResult.RouteValues!["id"]);
90 }
91}Spin up the full ASP.NET Core pipeline in-memory:
1// Install: Microsoft.AspNetCore.Mvc.Testing
2
3public class ProductsApiTests : IClassFixture<WebApplicationFactory<Program>>
4{
5 private readonly WebApplicationFactory<Program> _factory;
6 private readonly HttpClient _client;
7
8 public ProductsApiTests(WebApplicationFactory<Program> factory)
9 {
10 _factory = factory;
11 _client = factory.CreateClient();
12 }
13
14 [Fact]
15 public async Task GetProducts_ReturnsSuccessAndList()
16 {
17 // Act
18 var response = await _client.GetAsync("/api/products");
19
20 // Assert
21 response.EnsureSuccessStatusCode();
22 var content = await response.Content.ReadAsStringAsync();
23 var products = JsonSerializer.Deserialize<List<ProductDto>>(content);
24 Assert.NotNull(products);
25 }
26
27 [Fact]
28 public async Task GetProduct_NotFound_Returns404()
29 {
30 // Act
31 var response = await _client.GetAsync("/api/products/999999");
32
33 // Assert
34 Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);
35 }
36
37 [Fact]
38 public async Task CreateProduct_ValidData_Returns201()
39 {
40 // Arrange
41 var product = new CreateProductDto { Name = "Test", Price = 99.99m };
42 var content = new StringContent(
43 JsonSerializer.Serialize(product),
44 Encoding.UTF8,
45 "application/json");
46
47 // Act
48 var response = await _client.PostAsync("/api/products", content);
49
50 // Assert
51 Assert.Equal(HttpStatusCode.Created, response.StatusCode);
52 Assert.NotNull(response.Headers.Location);
53 }
54
55 [Fact]
56 public async Task CreateProduct_InvalidData_Returns400()
57 {
58 // Arrange
59 var product = new { Name = "", Price = -10 }; // Invalid
60 var content = new StringContent(
61 JsonSerializer.Serialize(product),
62 Encoding.UTF8,
63 "application/json");
64
65 // Act
66 var response = await _client.PostAsync("/api/products", content);
67
68 // Assert
69 Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode);
70 }
71
72 [Fact]
73 public async Task UpdateProduct_Returns204()
74 {
75 // Arrange - first create a product
76 var createContent = new StringContent(
77 JsonSerializer.Serialize(new { Name = "Original", Price = 50m }),
78 Encoding.UTF8, "application/json");
79 var createResponse = await _client.PostAsync("/api/products", createContent);
80 var created = JsonSerializer.Deserialize<ProductDto>(
81 await createResponse.Content.ReadAsStringAsync());
82
83 // Act - update it
84 var updateContent = new StringContent(
85 JsonSerializer.Serialize(new { Name = "Updated", Price = 75m }),
86 Encoding.UTF8, "application/json");
87 var response = await _client.PutAsync($"/api/products/{created!.Id}", updateContent);
88
89 // Assert
90 Assert.Equal(HttpStatusCode.NoContent, response.StatusCode);
91 }
92}Override service registrations to use in-memory databases:
1// Custom test factory with in-memory database
2public class CustomWebApplicationFactory<TProgram> : WebApplicationFactory<TProgram>
3 where TProgram : class
4{
5 protected override void ConfigureWebHost(IWebHostBuilder builder)
6 {
7 builder.ConfigureServices(services =>
8 {
9 // Remove the real database context
10 var descriptor = services.SingleOrDefault(
11 d => d.ServiceType == typeof(DbContextOptions<AppDbContext>));
12 if (descriptor != null)
13 services.Remove(descriptor);
14
15 // Add in-memory database
16 services.AddDbContext<AppDbContext>(options =>
17 {
18 options.UseInMemoryDatabase("TestDb_" + Guid.NewGuid());
19 });
20
21 // Replace external services with fakes
22 services.RemoveAll<IEmailService>();
23 services.AddScoped<IEmailService, FakeEmailService>();
24
25 services.RemoveAll<IPaymentService>();
26 services.AddScoped<IPaymentService, FakePaymentService>();
27
28 // Build service provider and seed data
29 var sp = services.BuildServiceProvider();
30 using var scope = sp.CreateScope();
31 var db = scope.ServiceProvider.GetRequiredService<AppDbContext>();
32 db.Database.EnsureCreated();
33 SeedTestData(db);
34 });
35
36 builder.UseEnvironment("Testing");
37 }
38
39 private void SeedTestData(AppDbContext context)
40 {
41 context.Products.AddRange(
42 new Product { Id = 1, Name = "Test Product 1", Price = 10.00m },
43 new Product { Id = 2, Name = "Test Product 2", Price = 20.00m }
44 );
45 context.SaveChanges();
46 }
47}
48
49// Fake services for testing
50public class FakeEmailService : IEmailService
51{
52 public List<EmailMessage> SentEmails { get; } = new();
53
54 public Task SendAsync(string to, string subject, string body)
55 {
56 SentEmails.Add(new EmailMessage(to, subject, body));
57 return Task.CompletedTask;
58 }
59}
60
61public class FakePaymentService : IPaymentService
62{
63 public bool ShouldSucceed { get; set; } = true;
64
65 public Task<PaymentResult> ProcessAsync(decimal amount)
66 {
67 return Task.FromResult(ShouldSucceed
68 ? PaymentResult.Success("tx_test_123")
69 : PaymentResult.Failed("Payment declined"));
70 }
71}
72
73// Use custom factory in tests
74public class OrdersApiTests : IClassFixture<CustomWebApplicationFactory<Program>>
75{
76 private readonly HttpClient _client;
77 private readonly CustomWebApplicationFactory<Program> _factory;
78
79 public OrdersApiTests(CustomWebApplicationFactory<Program> factory)
80 {
81 _factory = factory;
82 _client = factory.CreateClient();
83 }
84
85 [Fact]
86 public async Task CreateOrder_SendsConfirmationEmail()
87 {
88 // Arrange
89 var order = new { ProductId = 1, Quantity = 2 };
90 var content = new StringContent(JsonSerializer.Serialize(order), Encoding.UTF8, "application/json");
91
92 // Act
93 var response = await _client.PostAsync("/api/orders", content);
94
95 // Assert
96 response.EnsureSuccessStatusCode();
97
98 using var scope = _factory.Services.CreateScope();
99 var emailService = scope.ServiceProvider.GetRequiredService<IEmailService>() as FakeEmailService;
100 Assert.Single(emailService!.SentEmails);
101 Assert.Contains("Order Confirmation", emailService.SentEmails[0].Subject);
102 }
103}Substitute a custom HttpMessageHandler to mock HTTP dependencies:
1// Testing services that make HTTP calls
2public class ExternalApiService
3{
4 private readonly HttpClient _httpClient;
5
6 public ExternalApiService(HttpClient httpClient)
7 {
8 _httpClient = httpClient;
9 }
10
11 public async Task<WeatherData?> GetWeatherAsync(string city)
12 {
13 var response = await _httpClient.GetAsync($"/weather?city={city}");
14 if (!response.IsSuccessStatusCode) return null;
15
16 return await response.Content.ReadFromJsonAsync<WeatherData>();
17 }
18}
19
20// Mock HTTP handler for testing
21public class MockHttpMessageHandler : HttpMessageHandler
22{
23 private readonly Dictionary<string, HttpResponseMessage> _responses = new();
24
25 public void SetupResponse(string url, HttpResponseMessage response)
26 {
27 _responses[url] = response;
28 }
29
30 protected override Task<HttpResponseMessage> SendAsync(
31 HttpRequestMessage request,
32 CancellationToken cancellationToken)
33 {
34 var url = request.RequestUri!.PathAndQuery;
35
36 if (_responses.TryGetValue(url, out var response))
37 {
38 return Task.FromResult(response);
39 }
40
41 return Task.FromResult(new HttpResponseMessage(HttpStatusCode.NotFound));
42 }
43}
44
45// Tests
46public class ExternalApiServiceTests
47{
48 [Fact]
49 public async Task GetWeather_Success_ReturnsWeatherData()
50 {
51 // Arrange
52 var mockHandler = new MockHttpMessageHandler();
53 mockHandler.SetupResponse("/weather?city=London", new HttpResponseMessage
54 {
55 StatusCode = HttpStatusCode.OK,
56 Content = JsonContent.Create(new WeatherData
57 {
58 City = "London",
59 Temperature = 15.5,
60 Condition = "Cloudy"
61 })
62 });
63
64 var httpClient = new HttpClient(mockHandler) { BaseAddress = new Uri("https://api.weather.com") };
65 var service = new ExternalApiService(httpClient);
66
67 // Act
68 var result = await service.GetWeatherAsync("London");
69
70 // Assert
71 Assert.NotNull(result);
72 Assert.Equal("London", result.City);
73 Assert.Equal(15.5, result.Temperature);
74 }
75
76 [Fact]
77 public async Task GetWeather_ApiError_ReturnsNull()
78 {
79 // Arrange
80 var mockHandler = new MockHttpMessageHandler();
81 mockHandler.SetupResponse("/weather?city=InvalidCity", new HttpResponseMessage
82 {
83 StatusCode = HttpStatusCode.BadRequest
84 });
85
86 var httpClient = new HttpClient(mockHandler) { BaseAddress = new Uri("https://api.weather.com") };
87 var service = new ExternalApiService(httpClient);
88
89 // Act
90 var result = await service.GetWeatherAsync("InvalidCity");
91
92 // Assert
93 Assert.Null(result);
94 }
95}
96
97// Using Moq.Contrib.HttpClient (NuGet package)
98public class WeatherServiceTests
99{
100 [Fact]
101 public async Task GetWeather_WithMoqContrib()
102 {
103 // Arrange
104 var handler = new Mock<HttpMessageHandler>();
105 handler.SetupRequest(HttpMethod.Get, "https://api.weather.com/weather?city=Paris")
106 .ReturnsResponse(JsonSerializer.Serialize(new WeatherData
107 {
108 City = "Paris",
109 Temperature = 22.0,
110 Condition = "Sunny"
111 }), "application/json");
112
113 var httpClient = handler.CreateClient();
114 httpClient.BaseAddress = new Uri("https://api.weather.com");
115 var service = new ExternalApiService(httpClient);
116
117 // Act
118 var result = await service.GetWeatherAsync("Paris");
119
120 // Assert
121 Assert.NotNull(result);
122 Assert.Equal("Paris", result.City);
123 }
124}Your progress is saved in this browser, on this device only.