Problem: H2-də keçən, production-da qırılan testlər

Komanda repository testlərini PostgreSQL uyğunluq rejimində işləyən in-memory H2 bazasında yazır. Hər şey yaşıldır. Sonra release çıxır və jsonb, partial index və ya ON CONFLICT DO UPDATE işlədən sorğu xəta verir, çünki H2 nə eyni SQL dialektini, nə eyni lock davranışını, nə də eyni tip sistemini dəstəkləyir. Test dəsti production-da heç kimin işlətmədiyi bazada kodun işlədiyini sübut edib.

Adi reaksiya "integration testlər yavaş və qeyri-sabitdir" deyib repository-ni mock etməkdir. Bu, əslində qorumaq istədiyiniz riski aradan qaldırır: kodunuzla real baza arasındakı əlaqəni. Daha yaxşı yol testləri saxlamaq və onlara tələb olunduqda başladılan real bazanı verməkdir.

Əsas fikir

Testcontainers test koddan birdəfəlik Docker container-ləri başladan Java kitabxanasıdır. Spring Boot servisi üçün tipik ssenari belədir: testlər başlamazdan əvvəl postgres container-i qalxır, tətbiq ona yönəldilir, Flyway və ya Liquibase miqrasiyaları işləyir, JVM bitəndə container silinir. Testləriniz production ilə eyni mühərriki, eyni miqrasiyaları və eyni SQL-i yoxlayır. Yeganə tələb testləri işlədən maşında Docker ilə uyğun runtime-un olmasıdır.

Bu unit testləri əvəz etmir. O, səhvlərin birləşmə yerlərində gizləndiyi sahəni əhatə edir: SQL, transaction, constraint, serializasiya və miqrasiyalar.

@ServiceConnection ilə qoşulma

Spring Boot 3.1-dən etibarən JDBC URL-i properties faylına əllə köçürmək lazım deyil. @ServiceConnection annotasiyası bağlantı məlumatlarını container-dən özü yaradır.

@SpringBootTest@Testcontainersclass OrderRepositoryIT {    @Container    @ServiceConnection    static PostgreSQLContainer<?> postgres =        new PostgreSQLContainer<>("postgres:16-alpine");    @Autowired OrderRepository orders;    @Test    void savesAndFindsByCustomer() {        orders.save(new Order("c-1", "NEW"));        assertThat(orders.findByCustomerId("c-1")).hasSize(1);    }}

İmage tag-ını production-da işlətdiyiniz major versiyaya bərkidin. postgres:latest repository-də heç bir commit olmadan testlərin davranışını dəyişə bilər.

Servisiniz Flyway işlədirsə, miqrasiyalar startda avtomatik icra olunur, yəni testdəki sxem göndərdiyiniz sxemin eynisidir. Bu, ən dəyərli əlavə faydalardan biridir: qırıq miqrasiya build-i uğursuz edir.

Bir container-i bütün test sinifləri arasında paylaşmaq

Hər test sinfi üçün ayrıca container başlatmaq bu cür test dəstlərinin yavaşlamasının ən geniş yayılmış səbəbidir. Container-in qalxması real saniyələr çəkir, çox sinifə vurulduqda isə bu hiss olunur. İki praktik yol var.

Birincisi, ortaq baza konfiqurasiyadır. Container-i @TestConfiguration daxilində bean kimi elan edin və hər testdən import edin. Beləliklə Spring-in context cache-i bir context və bir container-i təkrar istifadə edir.

@TestConfiguration(proxyBeanMethods = false)class TestcontainersConfig {    @Bean    @ServiceConnection    PostgreSQLContainer<?> postgres() {        return new PostgreSQLContainer<>("postgres:16-alpine");    }}

Sonra testləri @Import(TestcontainersConfig.class) ilə işarələyin. Eyni konfiqurasiya tətbiqi lokal olaraq birdəfəlik bazayla işə salmağa da imkan verir: test main metodunda SpringApplication.from(...).with(...) istifadə olunur, bu da lokal inkişaf üçün rahatdır.

İkinci variant reuse imkanıdır: container developer maşınında JVM işə salınmaları arasında canlı qalır. O, lokal Testcontainers properties faylında açıq şəkildə aktivləşdirilməlidir və yalnız lokal sürət üçündür. CI-da buna güvənməyin, çünki hər işə salınma təmiz başlamalıdır.

Test izolyasiyası

Paylaşılan baza o deməkdir ki, testlər bir-birinə təsir edə bilər. Strategiyanı şüurlu seçin.

  1. Transaction rollback. Testi @Transactional ilə işarələyin ki, hər test geri qaytarılsın. Bu ucuzdur və repository və service testləri üçün işləyir. Test olunan kod REQUIRES_NEW ilə yeni transaction açırsa və ya tətbiqi HTTP üzərindən çağırırsınızsa, işləmir, çünki server thread-inin öz transaction-ı var.
  2. Açıq təmizləmə. @BeforeEach içində bir TRUNCATE ... RESTART IDENTITY CASCADE ilə cədvəlləri təmizləyin. Sadədir, hər cür test üçün işləyir, hər testə kiçik xərc əlavə edir.
  3. Unikal data. Hər test üçün unikal identifikatorlar yaradın (məsələn, təsadüfi customer id) və yalnız öz datanız üzərində assert edin. Paralel işləyən testlər üçün ən yaxşı seçimdir.

Bunları qaydasız qarışdırmaq yalnız CI serverində görünən, ardıcıllıqdan asılı xətalara gətirir. Layihə üçün bir default seçin və test base sinfində yazın.

Sürət üçün məsləhətlər

  • Spring context-i təkrar istifadə edin. @MockBean, property və profile-ların hər fərqli kombinasiyası yeni context yaradır. Eyni quraşdırma tələb edən testləri qruplaşdırın.
  • Sxemi kiçik, seed datanı minimal saxlayın. Yalnız testin ehtiyac duyduğunu yükləyin.
  • Rəsmi olaraq mövcud olduqda alpine kimi yüngül image-lərdən istifadə edin və image-in CI runner-lərində əvvəlcədən keşdə olduğuna əmin olun.
  • Hər testdə Kafka, Redis və PostgreSQL-i birlikdə qaldırmayın. Yalnız testin toxunduğunu qaldırın, ağır çox-container ssenarilərini ayrıca, kiçik dəstə çıxarın.
  • Unit və integration testləri ayrı task kimi işlədin, məsələn Maven Failsafe plugin və *IT adlandırması ilə, ki, developer-lər sürətli hissəni tez-tez işlədə bilsin.

CI məsələləri

CI agent-inin Docker daemon-a çıxışı olmalıdır. Hosted runner-lərdə bu adətən mövcuddur. Job-ların özünün container içində işlədiyi CI-da ya Docker socket-i mount etmək, ya da sidecar daemon lazımdır; hər ikisinin təhlükəsizlik nəticələri var: Docker socket-ə çıxış faktiki olaraq host-da root deməkdir. Bunu CI platformasına cavabdeh şəxslə birlikdə həll edin.

CI-nızda registry mirror və ya keş varsa, image-ləri onun üzərindən çəkin ki, registry-nin dayanması və ya limit build-i yıxmasın. Ağlabatan startup timeout təyin edin və uğursuzluqda container log-larını çap edin: log-suz readiness xətasını araşdırmaq çətindir.

Tez-tez edilən səhvlər

  • latest tag-ından istifadə edib gözlənilməz yenilənmələrlə qarşılaşmaq.
  • Test olunan kod öz transaction-ını açdığı halda @Transactional rollback-ə güvənmək.
  • Test datasını geridə qoymaq və "cədvəldə düz 3 sətir var" kimi qlobal saylara assert etmək.
  • Container ilə @DataJpaTest slice-ı daha tez qalxıb eyni şeyi yoxladığı halda hər testi @SpringBootTest etmək.
  • Portu sərt yazmaq. Container-in mapped portundan istifadə edin; Testcontainers təsadüfi boş portu qəsdən seçir.
  • hibernate.ddl-auto=create ilə miqrasiyaları keçmək. Onda test sxemi real sxemdən fərqlənir.

Güzəştlər və nə vaxt istifadə etməməli

Container əsaslı testlər sırf unit testlərdən yavaşdır və Docker tələb edir. I/O olmayan domen məntiqi üçün adi unit testlər daha yaxşıdır. Yeganə asılılığı üçüncü tərəf HTTP API olan kod üçün container yox, stub server uyğundur. Komandanız developer noutbuklarında Docker işlədə bilmirsə, əvvəlcə bunu həll edin, çünki yalnız bəzi insanların yavaş işlədə bildiyi test dəsti tezliklə istifadədən çıxır.

Yoxlama siyahısı

  • Container image tag-ı production-un major versiyasına uyğundur.
  • Sxem avtomatik generasiyadan yox, real miqrasiyalardan gəlir.
  • Mümkün olan yerdə bir container bütün dəst arasında paylaşılır.
  • Sənədləşmiş bir izolyasiya strategiyası var.
  • Integration testlər unit testlərdən ayrı task kimi işləyir.
  • CI-nin Docker çıxışı var və uğursuzluqda container log-ları çap olunur.

Növbəti addım