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.
- Transaction rollback. Testi
@Transactionalilə 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 kodREQUIRES_NEWilə 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. - Açıq təmizləmə.
@BeforeEachiçində birTRUNCATE ... RESTART IDENTITY CASCADEilə 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. - 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
alpinekimi 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ə
*ITadlandı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
latesttag-ından istifadə edib gözlənilməz yenilənmələrlə qarşılaşmaq.- Test olunan kod öz transaction-ını açdığı halda
@Transactionalrollback-ə 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ə
@DataJpaTestslice-ı daha tez qalxıb eyni şeyi yoxladığı halda hər testi@SpringBootTestetmə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=createilə 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
- Bu testlərin böyük mənzərədəki yerini Backend test strategiyası məqaləsində görün.
- Sorğular barədə nəyi yoxlamaq lazım olduğunu SQL index-lərin izahı məqaləsində öyrənin.
- Dəstin pipeline-da necə işlədiyini Java servisləri üçün CI/CD məqaləsində baxın.