Найти в Дзене

Интеграционные тесты для ASP.NET Core 8 Web API

Уточняю: тестировать я буду контроллер (см. https://dzen.ru/a/aERZVGOmEkUkVxAm) - с использованием xUnit v3 и пакета Microsoft.AspNetCore.Mvc.Testing. И без Moq. Основная статья, откуда я все это брал - вот: https://learn.microsoft.com/ru-ru/aspnet/core/test/integration-tests?view=aspnetcore-8.0&pivots=xunit. Кстати, для .NET Core 9 там код слегка поменялся, так что при переходе с .NET Core 8 на .NET Core 9 придется менять часть кода в тестовом проекте. Итак. Во-первых, в проекте с сервисами и хостингом сервисов, в точке входа (Program.cs) пришлось немного переделать код: 1. Добавить атрибут: #if DEBUG
[assembly:
InternalsVisibleTo("Foo.Server.Bar.IntegrationTests")]
#endif Foo.Server.Bar.IntegrationTests - это проект, где будут тесты. 2. Пришлось вернуться к использованию явно определенной точки входа Program / Main, причем класс Program объявить как partial. Возможно, это немного избыточно (вроде, можно обойтись без явного метода Main), ну да пусть будет как есть - более нагляд

Уточняю: тестировать я буду контроллер (см. https://dzen.ru/a/aERZVGOmEkUkVxAm) - с использованием xUnit v3 и пакета Microsoft.AspNetCore.Mvc.Testing. И без Moq. Основная статья, откуда я все это брал - вот: https://learn.microsoft.com/ru-ru/aspnet/core/test/integration-tests?view=aspnetcore-8.0&pivots=xunit. Кстати, для .NET Core 9 там код слегка поменялся, так что при переходе с .NET Core 8 на .NET Core 9 придется менять часть кода в тестовом проекте.

Итак. Во-первых, в проекте с сервисами и хостингом сервисов, в точке входа (Program.cs) пришлось немного переделать код:

1. Добавить атрибут:

#if DEBUG
[assembly:
InternalsVisibleTo("Foo.Server.Bar.IntegrationTests")]
#endif

Foo.Server.Bar.IntegrationTests - это проект, где будут тесты.

2. Пришлось вернуться к использованию явно определенной точки входа Program / Main, причем класс Program объявить как partial. Возможно, это немного избыточно (вроде, можно обойтись без явного метода Main), ну да пусть будет как есть - более наглядно, как я полагаю.

Во-вторых, был создан особый класс для настройки TestServer:

public class CustomWebApplicationFactory<TEntryPoint>: WebApplicationFactory<TEntryPoint> where TEntryPoint : class

В этом классе был переопределен единственный метод настройки:

protected override void ConfigureWebHost(IWebHostBuilder builder)
{
builder.ConfigureServices(services =>
{
var dbContextDescriptor = services.SingleOrDefault(d => d.ServiceType ==
typeof(DbContextOptions<FooDbContext>));
if (dbContextDescriptor != null)
services.Remove(dbContextDescriptor);
var dbConnectionDescriptor = services.SingleOrDefault(d => d.ServiceType ==
typeof(DbConnection));
if (dbConnectionDescriptor != null)
services.Remove(dbConnectionDescriptor);
// Create open SqliteConnection so EF won't automatically close it.
services.AddSingleton<DbConnection>(container =>
{
var connection = new SqliteConnection("DataSource=:memory:");
connection.Open();
return connection;
});
services.AddDbContext<FooDbContext>((container, options) =>
{
var connection = container.GetRequiredService<DbConnection>();
options.UseSqlite(connection);
});
});
builder.UseEnvironment("Development");
}

Для класса с тестами объявим перед его кодом (и перед ключевым словом namespace) следующий атрибут:

[assembly: CollectionBehavior(DisableTestParallelization = true)]

Да, поскольку у нас тут база данных в памяти, и, поскольку она одна, то делаем выполнение тестов последовательным.

Теперь сам класс с тестами:

public class FooBarListControllerIntegrationTest: IClassFixture<WebApplicationFactory<Program>>

Program - это класс из проекта с контроллером. Делаем для него такой конструктор:

private readonly WebApplicationFactory<Program> _factory;
private readonly HttpClient _client;
public FooBarListControllerIntegrationTest(WebApplicationFactory<Program>
factory)
{
_factory = factory;
_client = factory.CreateClient(new WebApplicationFactoryClientOptions
{
AllowAutoRedirect = false
});
}

Ну и теперь самый простой метод - тест:

[Fact]
public async Task TestGetBarListWithEmptyList()
{
// Arrange
using (IServiceScope scope = _factory.Services.CreateScope())
{
IServiceProvider scopedServices = scope.ServiceProvider;
FooDbContext db = scopedServices.GetRequiredService<FooDbContext>();
Utilities.ReinitializeDbForTests(db);
}
// Act
HttpResponseMessage response = await _client.GetAsync("/api/BarList",
TestContext.Current.CancellationToken);
// Assert
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
}

ReinitializeDbForTests проверяет базу и при необходимости создает всю ее структуру, и удаляет все данные, которые там были:

db.Database.EnsureCreated();
db.FooBars.RemoveRange(db.FooBars);
db.SaveChanges();

Заметим, что это именно интеграционные тесты. Я не дублирую функции модульных тестов - я проверяю те моменты, которые не мог проверить в них (или, в крайнем случае, что было проверять крайне неудобно из-за связей с другими компонентами).