Интеграционные Тесты Для Своего ASP.NET Core API
Интеграционный тест для API проверяет не отдельный метод класса, а реальный HTTP-запрос к твоему приложению.
То есть тест делает почти то же, что:
- браузер
- Postman
- Swagger
- фронтенд
Но делает это программно.
Что Именно Проверяет Такой Тест
Обычно:
- маршрут находится
- контроллер вызывается
- модель запроса биндингится
- DI работает
- сервисы зарегистрированы
- middleware отрабатывает
- сериализация JSON работает
- возвращается правильный статус-код
- возвращается правильное тело ответа
Чем Он Отличается От Unit-Теста
Unit:
- вызывает метод напрямую
- не делает HTTP-запрос
- не поднимает приложение
Integration:
- поднимает тестовую версию приложения
- создаёт HttpClient
- отправляет реальный HTTP-запрос в твой API
- получает реальный HttpResponseMessage
Как Это Работает
В ASP.NET Core для этого обычно используют:
- Microsoft.AspNetCore.Mvc.Testing
- WebApplicationFactory<TEntryPoint>
Идея такая:
1. тест поднимает твоё приложение in-memory
2. создаёт HttpClient
3. делает запросы вроде GET /api/tasks/1
4. получает ответ как настоящий клиент
То есть без реального браузера и без внешнего сервера.
Как Выглядит Поток
Тест делает:
var response = await client.GetAsync("/api/tasks/1");
Дальше внутри происходит:
1. запрос попадает в pipeline ASP.NET Core
2. middleware обрабатываются
3. роутинг выбирает контроллер
4. контроллер вызывает сервис
5. формируется ActionResult
6. ASP.NET сериализует ответ в JSON
7. тест получает HttpResponseMessage
Что Нужно Подключить
Обычно в тестовый проект добавляют пакет:
dotnet add package Microsoft.AspNetCore.Mvc.Testing
Базовая Структура
Допустим, у тебя есть API-приложение MyApi.
Простейший integration-тест:
using Microsoft.AspNetCore.Mvc.Testing;
using System.Net;
using Xunit;
public class TasksApiIntegrationTests : IClassFixture<WebApplicationFactory<Program>>
{
private readonly HttpClient _client;
public TasksApiIntegrationTests(WebApplicationFactory<Program> factory)
{
_client = factory.CreateClient();
}
[Fact]
public async Task Get_Unknown_Route_Returns_NotFound()
{
var response = await _client.GetAsync("/api/unknown");
Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);
}
}
От Простого К Сложному
Уровень 1. Просто проверить статус-код
Пример: GET возвращает 404
using Microsoft.AspNetCore.Mvc.Testing;
using System.Net;
using Xunit;
public class UsersApiTests : IClassFixture<WebApplicationFactory<Program>>
{
private readonly HttpClient _client;
public UsersApiTests(WebApplicationFactory<Program> factory)
{
_client = factory.CreateClient();
}
[Fact]
public async Task GetUser_ReturnsNotFound_WhenUserDoesNotExist()
{
var response = await _client.GetAsync("/api/users/999");
Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);
}
}
Что здесь проверяется:
- маршрут существует
- приложение поднялось
- запрос реально дошёл до API
- ответ реально 404
Уровень 2. Отправить POST с JSON
Пример: создание сущности
using Microsoft.AspNetCore.Mvc.Testing;
using System.Net;
using System.Net.Http.Json;
using Xunit;
public class UsersApiTests : IClassFixture<WebApplicationFactory<Program>>
{
private readonly HttpClient _client;
public UsersApiTests(WebApplicationFactory<Program> factory)
{
_client = factory.CreateClient();
}
[Fact]
public async Task CreateUser_ReturnsCreated_WhenRequestIsValid()
{
var request = new
{
name = "Anton"
};
var response = await _client.PostAsJsonAsync("/api/users", request);
Assert.Equal(HttpStatusCode.Created, response.StatusCode);
}
}
Что важно:
- PostAsJsonAsync сам сериализует объект в JSON
- тест отправляет настоящий POST
- сервер принимает JSON как обычный HTTP body
Уровень 3. Прочитать JSON-ответ
Пример: проверить тело ответа
using Microsoft.AspNetCore.Mvc.Testing;
using System.Net;
using System.Net.Http.Json;
using Xunit;
public class UsersApiTests : IClassFixture<WebApplicationFactory<Program>>
{
private readonly HttpClient _client;
public UsersApiTests(WebApplicationFactory<Program> factory)
{
_client = factory.CreateClient();
}
[Fact]
public async Task CreateUser_ReturnsCreatedUser()
{
var request = new
{
name = "Anton"
};
var response = await _client.PostAsJsonAsync("/api/users", request);
Assert.Equal(HttpStatusCode.Created, response.StatusCode);
var body = await response.Content.ReadFromJsonAsync<UserResponse>();
Assert.NotNull(body);
Assert.Equal("Anton", body!.Name);
Assert.True(body.Id > 0);
}
}
public class UserResponse
{
public int Id { get; set; }
public string Name { get; set; } = "";
}
Что здесь проверяется:
- и статус
- и JSON
- и правильность сериализации ответа
Уровень 4. Проверить полный сценарий
Пример: создать, потом получить
using Microsoft.AspNetCore.Mvc.Testing;
using System.Net;
using System.Net.Http.Json;
using Xunit;
public class UsersApiTests : IClassFixture<WebApplicationFactory<Program>>
{
private readonly HttpClient _client;
public UsersApiTests(WebApplicationFactory<Program> factory)
{
_client = factory.CreateClient();
}
[Fact]
public async Task CreateThenGet_ReturnsSameUser()
{
var createResponse = await _client.PostAsJsonAsync("/api/users", new
{
name = "Anton"
});
Assert.Equal(HttpStatusCode.Created, createResponse.StatusCode);
var createdUser = await createResponse.Content.ReadFromJsonAsync<UserResponse>();
Assert.NotNull(createdUser);
var getResponse = await _client.GetAsync($"/api/users/{createdUser!.Id}");
Assert.Equal(HttpStatusCode.OK, getResponse.StatusCode);
var fetchedUser = await getResponse.Content.ReadFromJsonAsync<UserResponse>();
Assert.NotNull(fetchedUser);
Assert.Equal(createdUser.Id, fetchedUser!.Id);
Assert.Equal("Anton", fetchedUser.Name);
}
}
Это уже хороший интеграционный сценарий.
Уровень 5. Проверить ошибку валидации
Пример: невалидный запрос
using Microsoft.AspNetCore.Mvc.Testing;
using System.Net;
using System.Net.Http.Json;
using Xunit;
public class UsersApiTests : IClassFixture<WebApplicationFactory<Program>>
{
private readonly HttpClient _client;
public UsersApiTests(WebApplicationFactory<Program> factory)
{
_client = factory.CreateClient();
}
[Fact]
public async Task CreateUser_ReturnsBadRequest_WhenNameIsEmpty()
{
var response = await _client.PostAsJsonAsync("/api/users", new
{
name = ""
});
Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode);
}
}
Это особенно важно для API.
Уровень 6. Проверить JSON ошибки
Если у тебя есть middleware глобальной обработки ошибок, можно проверить и тело ошибки.
using Microsoft.AspNetCore.Mvc.Testing;
using System.Net;
using System.Net.Http.Json;
using Xunit;
public class UsersApiTests : IClassFixture<WebApplicationFactory<Program>>
{
private readonly HttpClient _client;
public UsersApiTests(WebApplicationFactory<Program> factory)
{
_client = factory.CreateClient();
}
[Fact]
public async Task GetUser_ReturnsErrorBody_WhenUserNotFound()
{
var response = await _client.GetAsync("/api/users/999");
Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);
var error = await response.Content.ReadFromJsonAsync<ErrorResponse>();
Assert.NotNull(error);
Assert.Equal("Ресурс не найден", error!.Message);
Assert.Equal(404, error.StatusCode);
}
}
public class ErrorResponse
{
public bool Success { get; set; }
public string Message { get; set; } = "";
public int StatusCode { get; set; }
}
Это уже integration-тест настоящего middleware.
Как Работает HttpClient В Таких Тестах
Важно: здесь HttpClient не ходит в интернет.
Он ходит в поднятое внутри теста приложение.
То есть:
- не на чужой сервер
- не в браузер
- не в localhost как внешний процесс
А в in-memory test host.
Поэтому тесты:
- быстрые
- достаточно надёжные
- близки к реальному поведению API
Что Можно Делать Через HttpClient
GET
var response = await client.GetAsync("/api/tasks");
POST
var response = await client.PostAsJsonAsync("/api/tasks", new
{
title = "Task 1"
});
PUT
var response = await client.PutAsJsonAsync("/api/tasks/1", new
{
title = "Updated"
});
DELETE
var response = await client.DeleteAsync("/api/tasks/1");
Что Проверять В HttpResponseMessage
response.StatusCode
response.Headers
response.Content
Примеры:
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
Assert.True(response.IsSuccessStatusCode);
Чтение тела:
var text = await response.Content.ReadAsStringAsync();
или
var body = await response.Content.ReadFromJsonAsync<MyDto>();
Когда Integration-Тест Особенно Полезен
Пиши его, если хочешь проверить:
- реально ли маршрут доступен
- правильно ли биндингится request body
- реально ли middleware возвращает нужный JSON
- реально ли CreatedAtAction даёт верный статус
- реально ли конфигурация приложения живая
Частые Ошибки В Integration-Тестах
1. Проверять только 200
Слабый тест.
Лучше проверять ещё:
- тело ответа
- статус
- иногда заголовки
2. Тестировать слишком много сразу
Один тест = один сценарий.
3. Смешивать unit и integration
Если ты напрямую создаёшь сервис и не делаешь HTTP-вызов, это уже не integration API test.
4. Завязываться на глобальное состояние
Если приложение использует статические словари, тесты могут влиять друг на друга.
Минимальный Реалистичный Набор Integration-Тестов Для API
Если у тебя есть CRUD API, хороший минимум:
1. POST valid request -> 201
2. POST invalid request -> 400
3. GET by id existing -> 200
4. GET by id missing -> 404
5. middleware error body -> правильный JSON
Хорошая Структура Теста
[Fact]
public async Task CreateTask_ReturnsCreated_WhenRequestIsValid()
{
// Arrange
var request = new
{
title = "Write tests",
assigneeEmail = "user@example.com"
};
// Act
var response = await _client.PostAsJsonAsync("/api/tasks", request);
// Assert
Assert.Equal(HttpStatusCode.Created, response.StatusCode);
var body = await response.Content.ReadFromJsonAsync<TaskResponse>();
Assert.NotNull(body);
Assert.Equal("Write tests", body!.Title);
}
Чем Отличается От Тестов Через Mock
Mock-тест:
- ты проверяешь сервис напрямую
- не поднимаешь приложение
- сам контролируешь зависимости
Integration API test:
- ты тестируешь весь HTTP pipeline
- не важно, как внутри устроены вызовы
- ты смотришь глазами клиента