
Эта статья не перепечатанная документация, так что, если тебя интересует интеграционное тестирование .net rest api, то залетай.
Прежде чем перейти непосредственно к теме статьи, хотелось бы отдельно оговорить, как я пришёл к этому, и тем самым показать кому она подойдёт сейчас, кому в будущем, а кому будет мало интересна.
Предыстория
Когда я начинал свой путь в карьере разработчика, мне вынужденно пришлось начинать карьеру как full steck разработчик. И именно, это сформировало, то как я видел процесс разработки. Делал, базу, api, frontend и осуществлял тестирования с ui. Но, разработка backend части, мне всегда нравилась больше(ну и другие причины). И вот я начал разрабатывать api, для frontend.
И такой простой процесс тестирования сломался. Если я хотел протестировать через ui, он ещё мог быть не готов. Плюс, с ростом размера приложения, подготовить тестовые данные занимало довольно таки продолжительное время.
Проблема

Очевидным решением, для этой проблемы, являются тесты. Unit тесты, прекрасно подходят для тестирования какой ни будь, зубодробительной бизнес логики. Но, как бы мне не хотелось писать только такой код, 90% моего времени, занимает написания кода, api, работы с базой, работы с внешними api и так далее.
После непродолжительного времени, я осознал потребность в интеграционных тестах. Давайте же посмотрим, что нам предлагает майкрософт на этом поприще. Предлагает она нам замечательный Microsoft.AspNetCore.TestHost который поможет нам запустить наш сервер из program.cs со всеми зависимостями и предоставит нам http client к нему. А дальше…
А дальше нам предлагается, через http client вручную формировать запросы и обрабатывать ответы от них. Я попробовал такой подход на одном из своих проектов, и он дал свои плоды.
Но очевидна встало 2 проблемы, которые такой подход не решал:
Не было строгой типизации.
Процесс написания таких запросов несколько трудоемок, и создавал доп затраты на как на время разработки, так и на время поддержки. По ощущениям, поддерживать было очень сложно.
Очевидно, я решил поискать как же решить эту проблему. И не нашёл. Пришлось брать всё в свои руки.
Решение

Небольшое отступление. До этого шла речь о asp net core rest api, и далее пойдёт речь о нём же. Но у нас большинство проектов использует graphql+hotchocolate и начинал я путь именно с него. И там тоже ничего не нашёл. И я решил написать библиотеку, с названием, graph ql integration test, сокращённо GAIT. Я подумал что перевод этой аббревиатуры просто идеально подходил для описания того что она делаем. Но об этом позже.
Поскольку у меня уже была библиотека GAIT для graphql, то для rest api, я назвал библиотеку RAIT. Увы, перевод уже не так подходил к названию, но аббревиатуру решено было оставить.
Проще всего мне кажется объяснить, что она делает, приведя пример кода:
var model = new Model
{
Id = 10
};
var responseModel = await _httpClient.Rait<RaitTestController>().Call(n => n.ActionName(model));Тобиш, он позволяет вызывать любой метод контроллера через httpclient.
Это прекрасно решает все заявленные выше проблемы, а именно, тут есть строгая типизация, а также это быстро и просто.
Помимо этого, я получил 2 огромных бонуса:
В коде методы контроллера, не висят как неиспользуемые. Видно что, есть вызовы откуда то. Так наглядно видно, что покрыто тестами, а что нет.
Облегчает навигацию по коду. Вы легко можете перейти как в контроллер, так и обратно.
Немного о процессе разработки

Настраиваем проект, что бы у него были test.json конфигурации. Делаем, чтобы в тестах, база переиздавалась и накатывались миграции.
Далее, применяется tdd и обычно, процесс разработки начинается, с регистрации первого пользователя, ну или авторизации администратора:
[Test]
public async Task Register()
{
var authResult = await Context.UserOneClient.Rait<AuthController>().Call(n => n.RegisterUser(new RegisterModel
{
Email = "e1ektr0.xyz@gmail.com",
Password = "Password"
}));
var chessDbContext = Context.Services.GetRequiredService<ChessDbContext>();
Assert.That(chessDbContext.Users.Count(), Is.Not.Zero);
TestContext.UserOneClient.DefaultRequestHeaders.Authorization =
new AuthenticationHeaderValue("Bearer", authResult!.Token);
}Тут можно обратить внимание на то, что я не гнушаюсь использовать dbcontext, для того чтобы, проверить изменения в базе. А так, же, использование класса TestContext(раскрою чуть позже).
Далее напишем еще один тест, например, наш пользователь хочет создать лобби:
[Test]
public async Task CreateLobby()
{
await new AuthTests().Register();
var lobby = await TestContext.UserOneClient.Rait<LobbyController>().Call(n => n.CreateLobby());
Assert.That(lobby, Is.Not.Null);
Assert.That(Context.DbContext.Lobbies.ToList(), Is.Not.Empty);
}Как видите, тут, я вызываю предыдущий тест, с регистрацией пользователя. Тем самым выстраиваю сценарии работы пользователя на сайте.
[Test]
public async Task JoinLobby()
{
await CreateLobby();
await new AuthTests().RegisterUserTwo();
var lobbies = await Context.UserTwoClient.Rait<LobbyController>().Call(n => n.Get());
var lobby = lobbies!.First();
await TestContext.UserTwoClient.Rait<LobbyController>().Call(n => n.Join(lobby.Id));
var dbLobby = Context.DbContext.Lobbies.First();
await Context.DbContext.Entry(dbLobby).ReloadAsync();
Assert.That(dbLobby.OpponentUserId, Is.Not.Null);
}Как видите, и тут я продолжаю выстраивать сценарий. Создаю лобби, внутри созданного лобби проходит регистрация пользователя 1. TestContext я использую для передачи данных между тестами.
Собственно так выглядит мой процесс разработки. Пишу по одному тест на вызов api и выстраиваю сценарии. В текущий момент, мне очень нравиться такой подход. Это прекрасно решает мою проблему с тестированием и подготовкой тестовых данных.
Заключение
Учебный пример использования RAIT: https://github.com/e1ektr0/Chess
RAIT: https://github.com/e1ektr0/RAIT
А где же GAIT? Увы, взял деньги за разработку, и лежит в закрытом репо внутри компании. Ну и не то, чтобы он кому то был нужен, я думаю. Аудитория значительно меньше.
RAIT - не дописан полностью, по мере того, что мне требуется на проекте, дописывается. Также, он написан плохо, процесс рефакторинга отложен, до того, как я доберусь до copilot. Хочу поиграться.
В текущий момент, у меня нет, решения, для ситуации когда у вас куча микросервисов, как только снова буду работать над таким проектом - так и решу. У коллег, уже были просьбы на эту тему.
И самый скользкий момент, если кто то знает, что то лучше, что то готовое. Что то почитать на эту тему, всегда открыт к предложениям.
