Fixturega asoslangan test ma'lumotlarini tuzish

Fixturega asoslangan test ma'lumotlarini tuzish

1. Kirish

Loyihani amalga oshirayotganda "sinov kodlarini yozish kerak"ligini bilasiz, lekin reja vaqtidan boshlab kechiktirib turadigan hollarga ko‘p duch kelinishiga tayyor bo‘lishingiz mumkin. Men ham xuddi shunday edim. Orqa tomoni xizmatlarini rivojlantirayotganda, dastlab sinov kodisiz Insomnia yoki ekran orqali qo‘lda funksiyalarni tekshirib turardim. Ammo xizmatning miqyosi oshgan va domenlar orasidagi munosabatlar murakkablashgan sari, bir funksiyani o‘zgartirishda boshqa funksiyalarga ta'sir qiladigan hollarning soni oshdi. Qo‘lda test qilish orqali bularning barchasini har safar tekshirib bo‘lmas edi va kodni o‘zgartirgandan keyin kutilmagan funktsiyalarda xatolar paydo bo‘lishi hollari bilan tez-tez duch kelardim. Ushbu tajriba orqali integratsion testlarni jiddiy qo‘shishga kirishdim va bu maqolada, ayniqsa, test ma'lumotlarini tayyorlash uchun Fixture loyihasiga e'tibor qaratmoqchiman.

2. Test muhitini ko‘rib chiqish

Integratsion test muhitini quyidagicha tuzdik. @SpringBootTest va H2 in-memory DB dan foydalangan holda barcha dastur kontekstini yuklab, tashqi DB dan foydalanmasdan testni o‘tkazdik. @MockBean dan foydalangan holda tashqi xizmatlarga bo‘ysunishni mo‘k (Mock) ga ajratamiz. Har bir testdan oldin TRUNCATE ni bajarib, barcha jadvallarni to‘kib tashlab, testlar o‘rtasida ma'lumotlar aralashuvini oldini olamiz. Test klassida ushbu bazaviy klassdan voris olish orqali bir xil test muhitini tashkil etdik.

@SpringBootTest(classes = ServiceBootApplication.class)
public abstract class FeatureH2BootTestSupport {

    @MockBean
    protected OtherServiceClient otherServiceClient;

    @Autowired
    private JdbcTemplate jdbcTemplate;

    @BeforeEach
    void clearDatabase() {
        jdbcTemplate.execute("SET REFERENTIAL_INTEGRITY FALSE");
        List<String> tableNames = jdbcTemplate.queryForList(  
                  "SELECT TABLE_NAME FROM INFORMATION_SCHEMA.TABLES
                   WHERE TABLE_SCHEMA = 'PUBLIC'", 
                  String.class        
        );
        for (String tableName : tableNames) {
            jdbcTemplate.execute("TRUNCATE TABLE " + tableName);
        }
        jdbcTemplate.execute("SET REFERENTIAL_INTEGRITY TRUE");
    }
}

3. Fixturega ehtiyoj bor edi

Integratsion testlarda eng noqulay qismi test ma'lumotlarini tayyorlash edi. Mening mas'uliyatimdagi xizmatlar ko‘p ijrochilik ma'lumotlarini boshqarish xususiyatiga ega bo‘lishi sababli, yuqori qatlam ma'lumotlari mavjud bo‘lmasa xatolar yuzaga keladi yoki ishga tushirish mumkin bo‘lmagan ko‘plab funksiyalar mavjud edi. Masalan, "Aktorga rol berish funksiyasini" test qilmoqchi bo‘lsam, quyidagi oldingi ma'lumotlar DB da mavjud bo‘lishi kerak.

Pavilion → Cineroom → Stage → StageRoleSe
     └→   Subscription → Episode → AssignedEpisode → StagedEpisode

Har bir test uchun ushbu ma'lumotlarni alohida yaratish test kodini haddan tashqari uzunlashtirib yuboradi va ma'lumotlarni yaratish logikasini takrorlaydi. Shuningdek, har bir sub'ektning ID si yuqori sub'ektning ID si asosida yaratilgan struktura bo‘lganligi sababli, ID ni qo‘lda birlashtirish ko‘p vaqt talab qiladi, shuni aytish mumkinki, hatolar ko‘payadi va o‘qish qiyinlashadi.

4. Fixture loyihasi

4.1 FixtureDefaults — doimiy markaziy boshqaruv

Birinchi qilgan ishimiz testlarda foydalaniladigan barcha ID lar va asosiy qiymatlarni bir joyga to‘plab, markazda boshqarish edi.

public class FixtureDefaults {
    public static final String SQUARE_ID = "TST";
    public static final String PAVILION_ID = SQUARE_ID + ":1";
    public static final String CINEROOM_ID = PAVILION_ID + ":1";
    public static final String STAGE_ID = CINEROOM_ID + "-1";
    public static final String SUBSCRIPTION_ID = PAVILION_ID + "-S00AA";
    public static final String EPISODE_ID = PAVILION_ID + "-" + EPISODE_CODE;
    public static final String ASSIGNED_EPISODE_ID =
            AssignedEpisode.genId(CINEROOM_ID, EPISODE_ID);
    public static final String STAGED_EPISODE_ID =
            StagedEpisode.genId(STAGE_ID, ASSIGNED_EPISODE_ID);
    // ...
}

IDlarni doimiy markazda boshqarish orqali to‘g‘ri ID ma'lumotlarini oldindan tayyorlash va ko‘plab testlarda foydalanish imkoniyatiga ega bo‘lib, testdan oldin ma'lumotlarni o‘rnatishga sarflanadigan vaqtni kamaytiradi. Shuningdek, ID tizimida o‘zgarish bo‘lsa, bir joyda o‘zgartirish kerak bo‘ladi, shuning uchun test kodlarini yozish uchun yuklanish kamayadi. Boshqa tomondan, test kodini yozishda, `FixtureDefaults.PAVILION_ID` kabi murojaat qilib o‘qilishi oson boʻladi.

4.2 Domen boʻyicha Fixture klassi

Fixture klasslari to‘plamlar bo‘yicha tuzilgan. Har bir Fixture @Component sifatida ro‘yxatga olib, Spring kontekstidan in'ektsiya qilib foydalanamiz.

Fixture klassining metodlari vazifalarga ko‘ra uchta kattaroq guruhga bo‘lingan holda loyihalashtirilgan.

gen metodi FixtureDefaults konstantalariga asoslanib asosiy sub'ekt ob'ektlarini yaratadi. DB ga saqlanmaydi, faqat ob'ekt qaytaradi, shuning uchun testning Given qismida asosiy sub'ektni yaratgandan so‘ng, ma'lum maydonlarni o‘zgartirib kerakli holatga qilib berish uchun foydalanish mumkin. Static metod sifatida ta'riflangan, shuning uchun Spring kontekstidagi in'ektsiyasiz ham har joyda chaqirilishi mumkin.

genMore metodi asosiy entitidan tashqari bir xil turdagi qo'shimcha entitilar kerak bo'lsa, lekin bir-ikki maydonni o'zgartirish bilan qamrab ololmaydigan holatlar uchun tanlov metodidir. Masalan, "ikki ta Pavilion mavjud holatini" sinovdan o'tkazish kerak bo'lsa, asosiy Pavilionni genPavilion() bilan va qo'shimcha Pavilionni genMorePavilion() bilan yaratish mumkin. Parametr sifatida ajratish qiymatini (sequence, osid va boshqalar) qabul qilib, asosiy entitilar bilan to'qnashmasligini ta'minlaydi.

create metodi gen metodi yordamida yaratilgan asosiy entitini haqiqiy DB ga saqlaydi. Ichki ravishda gen metodini chaqirib, Store orqali saqlaydi, shuning uchun sinovlarda workspaceFixture.createPavilion() bir qator bilan DB da asosiy Pavilion tayyor bo'ladi. Ko'pchilik sinovlarda bu metod bilan kifoyalaniladi, ma'lumotlarni maxsuslashtirish kerak bo'lganda esa gen metodini to'g'ridan-to'g'ri ishlatadi.

Shunday qilib, rolni ajratish sababi, sinov kodining qisqa va moslashuvchanligini bir vaqtning o'zida ta'minlashdir. Oddiy sinov create metodi bir qator bilan tugaydi va murakkab ssenariylar bo'lsa, gen metod yordamida ob'ekt yaratib, kerakli holatda manipulyatsiya qilib, to'g'ridan-to'g'ri saqlash mumkin.

@Component
@RequiredArgsConstructor
public class WorkspaceFixture {

    // workspace aggregate domain store
    private final PavilionStore pavilionStore;

    // gen
    public static Pavilion genPavilion() {
        Pavilion pavilion = new Pavilion();
        pavilion.setId(FixtureDefaults.PAVILION_ID);
        // ...
        return pavilion;
    }

    // genMore
    public static Pavilion genMorePavilion(String sequence, String osid) {
	String pavilionId = FixtureDefaults.SQUARE_CODE + “:” + sequence;
        Pavilion pavilion = new Pavilion();
        pavilion.setId(pavilionId);
        pavilion.setOsid(osid);
        // ...
        return pavilion;
    }

    // create
    public void createPavilion() {
        Pavilion pavilion = genPavilion();  // FixtureDefaults 기반 기본값
        pavilionStore.create(pavilion);
    }
}

5. Haqiqiy sinovdagi Fixture dan foydalanish

Sinov kodlari Given-When-Then patterniga asoslangan. Bu shablonda Fixture Given bosqichida, ya'ni sinov uchun zarur bo'lgan avvalgi ma'lumotlarni tayyorlash vazifasini bajaradi. Haqiqiy sinov kodidan Fixture qanday ishlatilishini ko'rib chiqamiz.

5.1 create metodidan foydalanish — asosiy ssenariy

@BeforeEach
void setUp() {
    workspaceFixture.createPavilion();
    workspaceFixture.createCineroom();
    workspaceFixture.createStage();

    subscriptionFixture.createSubscription();
    subscriptionFixture.createEpisode();
    subscriptionFixture.createAssignedEpisode();
    subscriptionFixture.createStagedEpisode();
}

@Test
@DisplayName("revokeEpisode는 assignedEpisode와 관련된 stagedEpisode를 모두 제거한다")
void revokeEpisode_removesAssignedAndStagedEpisodes() {
    // Given

    // When
    flow.revokeEpisode(FixtureDefaults.ASSIGNED_EPISODE_ID);

    // Then — Store로 DB 상태 직접 검증
    assertThat(assignedEpisodeStore.exists(FixtureDefaults.ASSIGNED_EPISODE_ID))
            .isFalse();
    assertThat(stagedEpisodeStore.exists(FixtureDefaults.STAGED_EPISODE_ID))
            .isFalse();
}

Eng keng tarqalgan holat. @BeforeEach da umumiy avvalgi ma'lumotlarni create metod yordamida tayyorlab, har bir sinovning Given qismida faqat o'sha sinovga kerakli qo'shimcha ma'lumotlarni create qiladi. Fixture joriy etilmagan bo'lsa, setUp metodida har bir entitini to'g'ridan-to'g'ri yaratib, maydonlarni sozlash kodlari o'nlab qatorlarga cho'zilgan bo'lardi. create metodidan foydalansak, bu ma'lumotlarni tayyorlash jarayoni metod chaqirishi bilan bir qatorga qisqartiriladi, shuning uchun setUp "qanday ma'lumotlar tayyorlanganini" deklarativ ko'rsatadigan ro'yxatga aylanadi. Shuningdek, FixtureDefaults ga belgilangan doimiylarga asoslangan holda entitilarni yaratganligi sababli, ID xato yozilishi yoki qatlamlar munosabatining yo'qolishi kabi xatoliklarsiz, to'g'ri ma'lumotlarni xavfsiz tarzda sozlash mumkin.

Shunday qilib, umumiy ma'lumotlar tayyorgarligi setUp da tugagach, individual sinovning Given qismida faqat o'sha sinovga xos shartlar qoldirilishi mumkin yoki butunlay bo'sh qoldirilishi mumkin. Yuqoridagi misoldagidek Given qismi bo'sh bo'lsa, "setUp ning asosiy holati bu sinovning shartidir" deb bir qarashda bilib olinish mumkin va sinovning asosiy qismi hisoblangan When-Then ga tabiiy ravishda e'tibor qaratish imkonini beradi.

5.2 gen metodidan foydalanish — ma'lumotlarni maxsuslashtirish

@Test
@DisplayName("Dormant 상태의 subscription에 subscribed 이벤트가 오면 Active로 변경한다")
void subscribed_reactivatesSubscription_whenDormantState() {
    // Given — gen으로 만들고 상태를 커스터마이징
    Subscription subscription = SubscriptionFixture.genSubscription();
    subscription.setState(SubscriptionState.Dormant); // 기본값(Active)을 변경
    subscriptionStore.create(subscription);
    
    // When
    flow.subscribed(createSubscribeRequestSdo(FixtureDefaults.PAVILION_ID));
    
    // Then
    assertThat(subscriptionStore.retrieve(FixtureDefaults.SUBSCRIPTION_ID).isActive())
            .isTrue();
}

Asosiy qiymatdan farqli holatdagi ma'lumot kerak bo'lsa, gen metod yordamida ob'ekt yaratib, kerakli qiymatlarni sozlab to'g'ridan-to'g'ri saqlaymiz. Shunday qilib, gen metod "asosiy skeletni Fixture taqdim etsa, sinov ssenariysiga mos nozik sozlashlarni sinov kodida bevosita amalga oshirish" moslashuvchanligini taqdim etadi.

5.3 genMore metodidan foydalanish — ko'p ma'lumot ssenariysi

Bir xil turdagi entitilar ko'p talab qilinadigan sinovlarda genMore metodidan foydalanamiz.

@Test
void stageEpisode_stagesNewAndStashesRemovedStagedEpisodes() {
     subscriptionFixture.createAssignedEpisode();
     subscriptionFixture.createStagedEpisode();

     // Given — 추가 Episode 생성
     Episode otherEpisode = SubscriptionFixture.genEpisodeMore("E00AB");
     episodeStore.create(otherEpisode);
     AssignedEpisode otherAssigned =
              SubscriptionFixture.genAssignedEpisodeMore(
                      FixtureDefaults.CINEROOM_ID, otherEpisode);
     assignedEpisodeStore.create(otherAssigned);

     // When — 새 Episode로 교체
     flow.stageEpisode(FixtureDefaults.STAGE_ID,
               List.of(otherAssigned.getId()));
   
     // Then — 기존 것은 제거되고 새로운 것만 존재
     assertThat(stagedEpisodeStore.exists(FixtureDefaults.STAGED_EPISODE_ID))
              .isFalse();
     assertThat(stagedEpisodeStore.exists(
              StagedEpisode.genId(FixtureDefaults.STAGE_ID, otherAssigned.getId())))
             .isTrue();
}

genMore parametr sifatida ajratish qiymatini qabul qilib, asosiy Fixture va ID bilan to'qnashmaydigan entitini yaratadi. Episode domeni ID o'zgarishi ko'p bo'lgan maydonlarga ta'sir qilgani uchun, avvalgi 5.2 usuli o'rniga genMore metodidan foydalanishga qaror qilindi. Bu orqali "mavjud ma'lumotlar va yangi ma'lumotlar birgalikda mavjud bo'lgan holat" ni qulay yaratish mumkin.

6. Yakun

Boshida Fixture va doimiy ma'lumotlarni tashkil etish ishini og'ir topishingiz mumkin. Biroq, bir marta sozlangandan so'ng, sinov kodini yozishda katta yordam beradi. Dastlab FixtureDefaults va domen bo'yicha Fixture tizimini o'rnatmagunimizcha, sinov kodini yozayotganda har doim ma'lumotlarni tayyorlash mantiqini ko'chirib olib, ma'lumotlarni moslashtirish uchun vaqtni sarflardik. Ammo Fixture tizimini o'rnatgandan keyin, avvalgi ma'lumotlarni tayyorlashga vaqt sarflashim shart emas edi.

Soʻnggi paytlarda tuzilgan Fixturedan foydalangan holda AI vositalarining yordamida asosiy Flow/Seekga oid test kodlarini bir yoʻla yozib chiqdik va asta-sekin yanada nozikroq qilib toʻldirib bormoqdamiz. Kelajakda ham yangi funksiyalarni ishlab chiqishda Fixture asosidagi test tuzilmasini doimiy ravishda kengaytirishni rejalashtirmoqdamiz.

Rosalyn

Site footer