Integration Test — WebApplicationFactory, HttpClient, test DB

~25 daq · .NET Backend · Test

Muammodan boshlaylik

Unit testlar hammasi yashil (76–77-darslar). Deploy — va:

  • POST /api/buyurtmalar 404 qaytaradi — route da xato
  • JWT sozlamasida UseAuthentication unutilgan — hamma 401
  • EF Core so'rovi PostgreSQL da boshqacha ishlaydi — 500
  • DI da servis ro'yxatdan o'tmagan — ilova ishga tushmaydi

Har bir detal alohida ishlaydi, lekin birgalikda — yo'q.


Integration test nima?

Bir nechta qismni birgalikda tekshirish: HTTP so'rov → routing → middleware → autentifikatsiya → controller → servis → haqiqiy baza → JSON javob.

          Test piramidasi
             /\
            /E2E\         kam, sekin, qimmat
           /------\
          /Integr. \      o'rtacha
         /----------\
        /  Unit test \    ko'p, tez, arzon
       /--------------\

Hayotiy o'xshatma

Avtomobil detallari stendda sinaldi (unit test). Endi ular yig'ilgan mashina poligonda sinaladi: motor uzatmalar qutisiga to'g'ri ulanganmi, tormoz pedali haqiqatan g'ildiraklarni to'xtatadimi. Har bir detal ideal bo'lsa ham, ular o'rtasidagi ulanishda xato bo'lishi mumkin.


WebApplicationFactory

dotnet add Dokon.Tests package Microsoft.AspNetCore.Mvc.Testing

Program klassini test loyihasiga ko'rinadigan qilish (top-level statements uchun):

// Program.cs oxirida
public partial class Program { }

[!INFO] .NET 10 da top-level statements uchun Program klassi avtomatik public qilib generatsiya qilinadi — bu qator shart emas.

public class MahsulotlarApiTests(WebApplicationFactory<Program> factory)
    : IClassFixture<WebApplicationFactory<Program>>
{
    [Fact]
    public async Task Royxat_200_qaytaradi()
    {
        var client = factory.CreateClient();

        var javob = await client.GetAsync("/api/mahsulotlar");

        javob.EnsureSuccessStatusCode();
        var royxat = await javob.Content.ReadFromJsonAsync<List<MahsulotDto>>();
        Assert.NotNull(royxat);
    }
}

WebApplicationFactory butun ilovani xotirada ishga tushiradi (haqiqiy port siz, TestServer) — Program.cs dagi barcha sozlamalar bilan. HttpClient so'rovlari tarmoqsiz unga boradi.


Haqiqiy baza — Testcontainers

In-memory provayder yoki SQLite PostgreSQL dan boshqacha ishlaydi (tranzaksiyalar, case sensitivity, JSON ustunlar). Eng ishonchlisi — Docker da haqiqiy PostgreSQL:

dotnet add Dokon.Tests package Testcontainers.PostgreSql
using Testcontainers.PostgreSql;

public class DokonApiFactory : WebApplicationFactory<Program>, IAsyncLifetime
{
    private readonly PostgreSqlContainer _db = new PostgreSqlBuilder()
        .WithImage("postgres:17")
        .Build();

    protected override void ConfigureWebHost(IWebHostBuilder builder)
    {
        builder.UseEnvironment("Testing");
        builder.ConfigureTestServices(services =>
        {
            services.RemoveAll<DbContextOptions<DokonDbContext>>();
            services.AddDbContext<DokonDbContext>(o => o.UseNpgsql(_db.GetConnectionString()));
        });
    }

    public async Task InitializeAsync()
    {
        await _db.StartAsync();
        using var scope = Services.CreateScope();
        await scope.ServiceProvider.GetRequiredService<DokonDbContext>().Database.MigrateAsync();
    }

    public new async Task DisposeAsync() => await _db.DisposeAsync();
}

Testlar boshida konteyner ko'tariladi, migratsiyalar qo'llanadi, oxirida konteyner o'chiriladi. CI da ham ishlaydi (Docker bo'lsa).

[!INFO] xUnit v3 da IAsyncLifetime metodlari ValueTask qaytaradi (ValueTask InitializeAsync(), ValueTask DisposeAsync()). Yuqoridagi kod xUnit v2 uchun; v3 da qaytish turlarini moslang.


To'liq oqim testi

public class BuyurtmaOqimiTests(DokonApiFactory f) : IClassFixture<DokonApiFactory>
{
    [Fact]
    public async Task Mahsulot_yaratish_va_olish()
    {
        var client = f.CreateClient();
        client.DefaultRequestHeaders.Authorization =
            new AuthenticationHeaderValue("Bearer", TestToken.Admin());

        var yaratish = await client.PostAsJsonAsync("/api/mahsulotlar",
            new { nomi = "Test telefon", narx = 1_500_000 });

        Assert.Equal(HttpStatusCode.Created, yaratish.StatusCode);
        var yaratilgan = await yaratish.Content.ReadFromJsonAsync<MahsulotDto>();

        var olish = await client.GetFromJsonAsync<MahsulotDto>(
quot;/api/mahsulotlar/{yaratilgan!.Id}"); Assert.Equal("Test telefon", olish!.Nomi); } [Fact] public async Task Tokensiz_yaratish_401() { var javob = await f.CreateClient().PostAsJsonAsync("/api/mahsulotlar", new { nomi = "X", narx = 1 }); Assert.Equal(HttpStatusCode.Unauthorized, javob.StatusCode); } [Fact] public async Task Yaroqsiz_malumot_400_problem_details() { var client = f.CreateClient(); client.DefaultRequestHeaders.Authorization = new("Bearer", TestToken.Admin()); var javob = await client.PostAsJsonAsync("/api/mahsulotlar", new { nomi = "", narx = -5 }); Assert.Equal(HttpStatusCode.BadRequest, javob.StatusCode); Assert.Equal("application/problem+json", javob.Content.Headers.ContentType?.MediaType); } }

TestToken.Admin() — test konfiguratsiyasidagi kalit bilan JWT yaratuvchi yordamchi (60-darsdagi TokenService ni qayta ishlatish mumkin).


Tashqi servislarni almashtirish

builder.ConfigureTestServices(services =>
{
    services.RemoveAll<ITolovProvayder>();
    services.AddSingleton<ITolovProvayder, SoxtaTolovProvayder>();
});

Baza — haqiqiy, to'lov va email — soxta.


Testlar orasida baza holati

Testlar bir-birining ma'lumotiga ta'sir qilmasligi kerak:

  • Har bir test o'z ma'lumotini yaratadi (noyob nomlar, ID larga tayanmaslik)
  • Yoki har test oldidan bazani tozalash — Respawn kutubxonasi jadvallarni tez tozalaydi
  • Yoki har bir testni tranzaksiyada bajarib, oxirida rollback

Tez-tez uchraydigan xatolar

UseInMemoryDatabase ga to'liq ishonish — haqiqiy SQL xulqi (constraint, tranzaksiya) tekshirilmaydi.

Testlar tartibga bog'liq — biri yaratgan ma'lumotni boshqasi kutadi; parallel ishga tushganda yiqiladi.

Development konfiguratsiyasi bilan test — haqiqiy servislarga ulanib ketish; UseEnvironment("Testing") va almashtirishlar.


Xulosa

Integration test — qismlarni birgalikda tekshirish: routing, middleware, auth, DI, baza, JSON. WebApplicationFactory<Program> — ilovani xotirada ishga tushiradi, CreateClient() bilan haqiqiy HTTP so'rovlar; IClassFixture — umumiy fixture.

Testcontainers — Docker da haqiqiy PostgreSQL, migratsiyalar bilan. ConfigureTestServices — tashqi servislarni soxtasiga almashtirish. Status kodlar, ProblemDetails va 401/403 ni tekshiring. Testlar mustaqil: o'z ma'lumoti yoki Respawn.


Keyingi dars

79-dars: Test best practices — AAA naqshi, test nomlash, coverage