Интеграционные Тесты

Скачать

Интеграционные Тесты Для Своего 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
- не важно, как внутри устроены вызовы
- ты смотришь глазами клиента