← Kursa Dön
📄 Text · 25 min

Test Fixtures ve Builder

Test verisi hazırlamak, test yazımının en zaman alıcı kısımlarından biridir. Bir User oluşturmak için isim, email, şifre, rol, aktiflik durumu doldurmak gerekir — ve her test farklı kombinasyonlara ihtiyaç duyar. TestDataBuilder ve ObjectMother kalıpları bu problemi zarif bir şekilde çözer.

Problem: Test Verisi Karmaşıklığı

// Her testte bunları yazmak yorucu ve hata-prone:
@Test
void testActiveUserCanLogin() {
    User user = new User();
    user.setName("Ali");
    user.setEmail("ali@test.com");
    user.setPassword("encoded");
    user.setRole(Role.USER);
    user.setActive(true);
    user.setCreatedAt(LocalDateTime.now());
    // ... 10 satır daha setup
}

@Test
void testInactiveUserCannotLogin() {
    User user = new User();
    user.setName("Veli");
    user.setEmail("veli@test.com");
    user.setPassword("encoded");
    user.setRole(Role.USER);
    user.setActive(false);  // Tek fark bu satır
    user.setCreatedAt(LocalDateTime.now());
    // ... aynı 10 satır
}

Bu yaklaşımın sorunları:

  • Tekrar (DRY ihlali): Her testte aynı kodun tekrarlanması

  • Kırılganlık: Entity'ye yeni alan eklendiğinde tüm testler güncellenmeli

  • Okunabilirlik: Hangi alanın test için önemli olduğu anlaşılmaz

TestDataBuilder Pattern

Builder pattern'ı test verisi oluşturmaya uygulayalım:

public class UserTestBuilder {

    private Long id = null;
    private String name = "Test User";
    private String email = "test@example.com";
    private String password = "encodedPassword";
    private Role role = Role.USER;
    private boolean active = true;
    private LocalDateTime createdAt = LocalDateTime.now();

    // Private constructor — static factory method kullan
    private UserTestBuilder() {}

    public static UserTestBuilder aUser() {
        return new UserTestBuilder();
    }

    public static UserTestBuilder anAdmin() {
        return new UserTestBuilder()
            .withRole(Role.ADMIN)
            .withEmail("admin@example.com");
    }

    public static UserTestBuilder anInactiveUser() {
        return new UserTestBuilder()
            .withActive(false);
    }

    // Fluent setter'lar
    public UserTestBuilder withId(Long id) {
        this.id = id;
        return this;
    }

    public UserTestBuilder withName(String name) {
        this.name = name;
        return this;
    }

    public UserTestBuilder withEmail(String email) {
        this.email = email;
        return this;
    }

    public UserTestBuilder withPassword(String password) {
        this.password = password;
        return this;
    }

    public UserTestBuilder withRole(Role role) {
        this.role = role;
        return this;
    }

    public UserTestBuilder withActive(boolean active) {
        this.active = active;
        return this;
    }

    public User build() {
        User user = new User();
        user.setId(id);
        user.setName(name);
        user.setEmail(email);
        user.setPassword(password);
        user.setRole(role);
        user.setActive(active);
        user.setCreatedAt(createdAt);
        return user;
    }
}

Kullanımı:

@Test
void testActiveUserCanLogin() {
    User user = aUser().withName("Ali").withEmail("ali@test.com").build();
    assertTrue(authService.canLogin(user));
}

@Test
void testInactiveUserCannotLogin() {
    User user = anInactiveUser().build(); // Tek satır!
    assertFalse(authService.canLogin(user));
}

@Test
void testAdminCanDeleteUsers() {
    User admin = anAdmin().withName("SuperAdmin").build();
    assertTrue(authService.canDelete(admin));
}

Dikkat edin: her testte sadece test için önemli olan alanlar belirtilir. Geri kalan varsayılan değerlerle doldurulur.

ObjectMother Pattern

ObjectMother, sık kullanılan test nesnelerini merkezi bir sınıftan sağlar — builder'dan daha basit:

public class TestFixtures {

    // ─── User Fixtures ────────────────────────────────────
    public static User defaultUser() {
        return aUser().build();
    }

    public static User activeUser(String name, String email) {
        return aUser().withName(name).withEmail(email).build();
    }

    public static User adminUser() {
        return anAdmin().withName("Admin").build();
    }

    // ─── Product Fixtures ─────────────────────────────────
    public static Product laptop() {
        return ProductTestBuilder.aProduct()
            .withName("Laptop").withPrice(999.99)
            .withCategory("Electronics").build();
    }

    public static Product cheapProduct() {
        return ProductTestBuilder.aProduct()
            .withName("Pen").withPrice(1.99).build();
    }

    // ─── Order Fixtures ───────────────────────────────────
    public static Order pendingOrder() {
        return OrderTestBuilder.anOrder()
            .withStatus(OrderStatus.PENDING)
            .withProduct(laptop())
            .withUser(defaultUser())
            .build();
    }

    // ─── Request Fixtures ─────────────────────────────────
    public static CreateUserRequest validCreateRequest() {
        return new CreateUserRequest("Ali", "ali@test.com", "pass123");
    }
}

@TestFactory — Dinamik Testler

JUnit 5'in @TestFactory, test metotlarını çalışma zamanında oluşturmanızı sağlar:

class DynamicTestExamples {

    @TestFactory
    @DisplayName("Farklı kullanıcı rolleri ile yetki kontrolü")
    Collection<DynamicTest> testPermissionsByRole() {
        Map<Role, Boolean> expected = Map.of(
            Role.ADMIN, true,
            Role.MANAGER, true,
            Role.USER, false,
            Role.GUEST, false
        );

        return expected.entrySet().stream()
            .map(entry -> dynamicTest(
                entry.getKey() + " → canDeleteUser: " + entry.getValue(),
                () -> {
                    User user = aUser().withRole(entry.getKey()).build();
                    assertEquals(entry.getValue(),
                        authService.canDeleteUser(user));
                }
            ))
            .toList();
    }

    @TestFactory
    @DisplayName("Email validation senaryoları")
    Stream<DynamicTest> testEmailValidation() {
        record TestCase(String email, boolean valid) {}

        return Stream.of(
            new TestCase("ali@test.com", true),
            new TestCase("admin@company.org", true),
            new TestCase("invalid", false),
            new TestCase("@no-local.com", false),
            new TestCase("spaces in@email.com", false),
            new TestCase("", false)
        ).map(tc -> dynamicTest(
            "'" + tc.email() + "' → " + (tc.valid() ? "geçerli" : "geçersiz"),
            () -> assertEquals(tc.valid(), validator.isValidEmail(tc.email()))
        ));
    }
}

Random Data Generation

Deterministic random test verisi oluşturmak, edge case'leri yakalamaya yardımcı olur:

public class RandomTestData {

    private static final Random RANDOM = new Random(42); // Seed — tekrarlanabilir
    private static final String CHARS = "abcdefghijklmnopqrstuvwxyz";

    public static String randomName() {
        int len = 3 + RANDOM.nextInt(10);
        StringBuilder sb = new StringBuilder(len);
        sb.append(Character.toUpperCase(CHARS.charAt(RANDOM.nextInt(26))));
        for (int i = 1; i < len; i++) {
            sb.append(CHARS.charAt(RANDOM.nextInt(26)));
        }
        return sb.toString();
    }

    public static String randomEmail() {
        return randomName().toLowerCase() + "@test.com";
    }

    public static double randomPrice() {
        return Math.round(RANDOM.nextDouble() * 10000) / 100.0;
    }

    public static User randomUser() {
        return aUser()
            .withName(randomName())
            .withEmail(randomEmail())
            .build();
    }

    public static List<User> randomUsers(int count) {
        return IntStream.range(0, count)
            .mapToObj(i -> randomUser())
            .toList();
    }
}

Seed kullanımı önemlidir: new Random(42) her çalıştırıldığında aynı "rastgele" değerleri üretir. Test başarısız olursa aynı verilerle tekrar çalıştırılabilir (tekrarlanabilirlik prensibi).

Test fixtures ve builder kalıpları, test kodunuzun bakımını kolaylaştırır, okunabilirliği artırır ve entity değişikliklerinde güncelleme maliyetini minimuma indirir. Her projede src/test/java altında bir fixtures veya testdata paketi oluşturmak iyi bir pratiktir.