Перейти к основному содержимому

Интеграционные тесты ASP.NET Core

Разработчику

Интеграционные тесты ASP.NET Core

Где применяют интеграционные тесты

Юнит-тест проверяет один класс: вы вызываете метод напрямую, зависимости подменяете моками. Интеграционный тест поднимает почти настоящее приложение — с Program.cs, middleware, маршрутами, JSON-сериализацией — и шлёт запросы как внешний клиент через HttpClient.

Так ловят ошибки, которые юнит не увидит:

  • забыли app.UseAuthentication();
  • опечатка в [Route];
  • неверный порядок middleware;
  • POST без Content-Type: application/json.

В ASP.NET Core для этого есть WebApplicationFactory<Program>: тестовый хост в памяти, без ручного выбора порта в браузере.

Опираемся на API из 4511. С JWT — 4515. Solution в стиле Clean Architecture — 2143, MediatR — 4518. Контекст отладки: /encyclopedia/4-code-dev/4-14-razrabotka-i-otladka/intro.


Словарь

ТерминПростыми словами
xUnitПопулярный фреймворк тестов в .NET ([Fact], dotnet test).
WebApplicationFactoryСоздаёт тестовый экземпляр вашего веб-приложения.
Test serverHTTP обрабатывается в памяти, без реального сетевого сокета.
FixtureОбщая настройка на класс тестов (IClassFixture<...>).
InMemory DBEF Core «база» в RAM для быстрых тестов (с ограничениями).

Что получится

ТестПроверка
Health_ReturnsOkGET /health → 200
PostNote_ThenGetAllPOST + GET, тело JSON
(опционально)Защищённый endpoint с Bearer после login

Подготовка решения

Если API ещё нет — создайте по 4511:

dotnet new webapi -n HelloApi -o HelloApi --use-controllers
cd HelloApi

Добавьте endpoint (если нет):

app.MapGet("/health", () => Results.Ok(new { status = "ok" }));

public partial class Program

В .NET 6+ Program.cs часто без явного класса. Тестам нужен тип Program:

app.Run();

public partial class Program { }

В конце Program.cs — стандартный приём для WebApplicationFactory<Program>.


Проект тестов

Из корня решения:

dotnet new xunit -n HelloApi.Tests -o HelloApi.Tests
dotnet add HelloApi.Tests reference HelloApi
dotnet add HelloApi.Tests package Microsoft.AspNetCore.Mvc.Testing
dotnet sln add HelloApi.Tests/HelloApi.Tests.csproj

Microsoft.AspNetCore.Mvc.Testing подтягивает test host и фабрику приложения.


Базовый тест

HelloApi.Tests/HealthEndpointTests.cs:

using System.Net;
using Microsoft.AspNetCore.Mvc.Testing;

namespace HelloApi.Tests;

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

public HealthEndpointTests(WebApplicationFactory<Program> factory)
{
_client = factory.CreateClient();
}

[Fact]
public async Task Health_ReturnsOk()
{
var response = await _client.GetAsync("/health");

Assert.Equal(HttpStatusCode.OK, response.StatusCode);
var json = await response.Content.ReadAsStringAsync();
Assert.Contains("ok", json, StringComparison.OrdinalIgnoreCase);
}
}
dotnet test

Что происходит внутри

ШагСмысл
IClassFixture<WebApplicationFactory<Program>>Одна фабрика на класс — приложение поднимается один раз.
factory.CreateClient()HttpClient с BaseAddress на test server.
GetAsync("/health")Путь от корня; ведущий / важен.
Assert.EqualПроверка статуса и тела.

Тест CRUD

При наличии NotesController из 4511:

using System.Net.Http.Json;
using Microsoft.AspNetCore.Mvc.Testing;

namespace HelloApi.Tests;

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

public NotesEndpointTests(WebApplicationFactory<Program> factory)
{
_client = factory.CreateClient();
}

[Fact]
public async Task PostNote_ThenGetAll_ContainsNote()
{
var create = await _client.PostAsJsonAsync(
"/api/notes",
new { Text = "integration test" });

create.EnsureSuccessStatusCode();

var notes = await _client.GetFromJsonAsync<List<NoteDto>>("/api/notes");

Assert.NotNull(notes);
Assert.Contains(notes, n => n.Text == "integration test");
}

private record NoteDto(int Id, string Text);
}

PostAsJsonAsync сериализует объект в JSON и ставит заголовок Content-Type: application/json — как настоящий клиент.


Подмена базы данных

Интеграционные тесты не должны писать в продакшен-PostgreSQL.

ВариантКогда
EF InMemoryБыстро; нет настоящего SQL
SQLite :memory:Ближе к SQL
TestcontainersРеальный PostgreSQL в Docker на CI

HelloApi.Tests/CustomWebApplicationFactory.cs:

using HelloApi.Data;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Mvc.Testing;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;

namespace HelloApi.Tests;

public class CustomWebApplicationFactory : WebApplicationFactory<Program>
{
protected override void ConfigureWebHost(IWebHostBuilder builder)
{
builder.ConfigureServices(services =>
{
var descriptor = services.SingleOrDefault(
d => d.ServiceType == typeof(DbContextOptions<AppDbContext>));

if (descriptor is not null)
services.Remove(descriptor);

services.AddDbContext<AppDbContext>(options =>
options.UseInMemoryDatabase("TestsDb"));
});

builder.UseEnvironment("Testing");
}
}

Тест:

public class NotesDbTests : IClassFixture<CustomWebApplicationFactory>
{
private readonly HttpClient _client;

public NotesDbTests(CustomWebApplicationFactory factory)
{
_client = factory.CreateClient();
}
// те же HTTP-вызовы
}

:::tip InMemory и транзакции UseInMemoryDatabase не эмулирует все ограничения SQL Server. Для raw SQL и миграций — SQLite или Testcontainers. :::

В Program.cs для внешних сервисов:

if (!app.Environment.IsEnvironment("Testing"))
{
// подключение к реальной почте, платежам и т.д.
}

Тест API с JWT

Для 4515:

var login = await _client.PostAsJsonAsync("/login",
new { email = "test@local", password = "Passw0rd!" });
login.EnsureSuccessStatusCode();

var auth = await login.Content.ReadFromJsonAsync<AuthDto>();
_client.DefaultRequestHeaders.Authorization =
new("Bearer", auth!.Token);

var secured = await _client.GetAsync("/api/notes");
secured.EnsureSuccessStatusCode();

private record AuthDto(string Token, DateTime ExpiresAt);

Альтернатива для большого сьюта: в ConfigureTestServices подставить тестового ClaimsPrincipal без реального JWT — быстрее, но не проверяет подпись токена.


Сравнение с юнит-тестом контроллера

WebApplicationFactoryМок сервиса + вызов action
ПроверяетPipeline, routing, JSON, authОдин метод
СкоростьМедленнееБыстрее
Видит Program.csДаНет

Оба подхода дополняют друг друга.


Частые ошибки

СимптомРешение
Program не найденpublic partial class Program { }
404 в тестеПуть /health, не health
Пустая БД после POSTПодмена DbContext не сработала — проверьте тип в Remove
307 redirectHTTPS redirect: AllowAutoRedirect = false или тестируйте HTTPS
Падает только на CIОбщая InMemory БД — уникальное имя на фабрику

Что попробовать

  1. Ожидайте 400 на POST с пустым телом (4517ValidationProblem).
  2. Testcontainers + PostgreSQL на GitHub Actions.
  3. Отдельный сьют только для AuthController с тестовым Jwt:Key в appsettings.Testing.json.

Дальше


См. также

Другие статьи этого же раздела в боковом меню (как на странице «О разделе»).