← Kursa Dön
📄 Text · 30 min

JUnit 5 Temeller

JUnit 5 (diğer adıyla JUnit Jupiter), Java ekosisteminin standart test framework'üdür. Spring Boot 2.2+ ile varsayılan olarak gelir ve güçlü anotasyonlar, esnek assertion'lar ve modern test yazım kalıpları sunar. Bu derste JUnit 5'in tüm temel özelliklerini öğreneceğiz.

JUnit 5 Mimarisi

JUnit 5, üç modülden oluşur:

  • JUnit Platform: Test çalıştırma altyapısı (IDE ve build tool entegrasyonu)

  • JUnit Jupiter: JUnit 5'in yeni programlama modeli ve anotasyonları

  • JUnit Vintage: JUnit 3 ve JUnit 4 testlerini JUnit 5 platformunda çalıştırma desteği

Spring Boot projesinde JUnit 5 otomatik olarak gelir:

<!-- spring-boot-starter-test JUnit 5'i içerir -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-test</artifactId>
    <scope>test</scope>
</dependency>

Temel Anotasyonlar

import org.junit.jupiter.api.*;

class CalculatorTest {

    private Calculator calculator;

    @BeforeAll
    static void setupAll() {
        // Tüm testlerden ÖNCE bir kez çalışır
        // static olmalı — sınıf seviyesinde hazırlık
        System.out.println("Test sınıfı başlatılıyor...");
    }

    @AfterAll
    static void tearDownAll() {
        // Tüm testlerden SONRA bir kez çalışır
        // static olmalı — sınıf seviyesinde temizlik
        System.out.println("Test sınıfı tamamlandı.");
    }

    @BeforeEach
    void setUp() {
        // Her testten ÖNCE çalışır
        // Test izolasyonu için her seferinde yeni nesne oluşturulur
        calculator = new Calculator();
    }

    @AfterEach
    void tearDown() {
        // Her testten SONRA çalışır
        // Kaynak temizliği (dosya kapatma, bağlantı sonlandırma vb.)
        calculator = null;
    }

    @Test
    @DisplayName("İki pozitif sayı toplanmalı")
    void shouldAddTwoPositiveNumbers() {
        assertEquals(5, calculator.add(2, 3));
    }

    @Test
    @DisplayName("Sıfıra bölme exception fırlatmalı")
    void shouldThrowOnDivisionByZero() {
        assertThrows(ArithmeticException.class,
            () -> calculator.divide(10, 0));
    }

    @Test
    @Disabled("Bug #123 düzeltilene kadar devre dışı")
    void temporarilyDisabledTest() {
        // Bu test çalışmaz ama raporda görünür
    }
}

Yaşam döngüsü sırası: @BeforeAll → (@BeforeEach@Test@AfterEach) × N → @AfterAll

Assertions — Doğrulama Metotları

JUnit 5'in Assertions sınıfı zengin doğrulama metotları sunar:

import static org.junit.jupiter.api.Assertions.*;

class AssertionExamplesTest {

    @Test
    void basicAssertions() {
        // Eşitlik kontrolü
        assertEquals(4, 2 + 2);
        assertEquals("hello", "hello");
        assertEquals(3.14, Math.PI, 0.01); // delta ile double karşılaştırma

        // Boolean kontrol
        assertTrue(5 > 3);
        assertFalse(2 > 5);

        // Null kontrol
        assertNull(null);
        assertNotNull("value");

        // Aynı referans kontrolü
        String s1 = "test";
        String s2 = s1;
        assertSame(s1, s2);

        // Özel hata mesajı — test başarısız olursa gösterilir
        assertEquals(10, calculateTotal(),
            "Toplam tutar 10 olmalıydı");

        // Lambda ile lazy message — sadece başarısız olursa evaluate edilir
        assertEquals(10, calculateTotal(),
            () -> "Beklenen: 10, Gerçek: " + calculateTotal());
    }

    @Test
    void assertAllExample() {
        // assertAll: TÜMÜNÜ çalıştırır, hangilerinin başarısız olduğunu raporlar
        // Normal assert'te ilk başarısızlıkta durur
        User user = new User("Ali", "ali@test.com", 25);

        assertAll("User doğrulaması",
            () -> assertEquals("Ali", user.getName()),
            () -> assertEquals("ali@test.com", user.getEmail()),
            () -> assertTrue(user.getAge() > 0),
            () -> assertNotNull(user.getName())
        );
    }

    @Test
    void exceptionAssertions() {
        // Exception fırlatıldığını doğrula
        Exception ex = assertThrows(IllegalArgumentException.class,
            () -> new Product("", -5));

        // Exception mesajını da doğrulayabilirsiniz
        assertEquals("Fiyat negatif olamaz", ex.getMessage());
        assertTrue(ex.getMessage().contains("negatif"));

        // Exception fırlatılMADIĞINI doğrula
        assertDoesNotThrow(() -> new Product("Laptop", 999));
    }

    @Test
    @Timeout(value = 2, unit = TimeUnit.SECONDS)
    void timeoutAssertion() {
        // Belirli sürede tamamlanmalı
        assertTimeout(Duration.ofMillis(500), () -> {
            // 500ms içinde bitmeli
            Thread.sleep(100);
        });
    }
}

@Nested — İç İçe Test Sınıfları

@Nested, ilişkili testleri gruplamak için kullanılır. BDD (Behavior-Driven Development) tarzında okunabilir test hiyerarşisi oluşturur:

@DisplayName("UserService Testleri")
class UserServiceTest {

    private UserService userService;
    private UserRepository mockRepo;

    @BeforeEach
    void setUp() {
        mockRepo = mock(UserRepository.class);
        userService = new UserService(mockRepo);
    }

    @Nested
    @DisplayName("createUser metodu")
    class CreateUser {

        @Test
        @DisplayName("geçerli kullanıcı oluşturmalı")
        void shouldCreateValidUser() {
            User user = new User("Ali", "ali@test.com");
            when(mockRepo.save(any())).thenReturn(user);

            User result = userService.createUser("Ali", "ali@test.com");
            assertNotNull(result);
            assertEquals("Ali", result.getName());
        }

        @Test
        @DisplayName("boş isim verilince hata fırlatmalı")
        void shouldThrowWhenNameIsEmpty() {
            assertThrows(IllegalArgumentException.class,
                () -> userService.createUser("", "ali@test.com"));
        }

        @Nested
        @DisplayName("email doğrulama")
        class EmailValidation {

            @Test
            @DisplayName("geçersiz email formatta hata fırlatmalı")
            void shouldThrowForInvalidEmail() {
                assertThrows(IllegalArgumentException.class,
                    () -> userService.createUser("Ali", "invalid"));
            }
        }
    }

    @Nested
    @DisplayName("findUser metodu")
    class FindUser {

        @Test
        @DisplayName("var olan kullanıcıyı bulmalı")
        void shouldFindExistingUser() {
            when(mockRepo.findById(1L))
                .thenReturn(Optional.of(new User("Ali", "ali@test.com")));

            Optional<User> result = userService.findById(1L);
            assertTrue(result.isPresent());
        }
    }
}

Test raporu çıktısı:

UserService Testleri
  ├─ createUser metodu
  │   ├─ ✓ geçerli kullanıcı oluşturmalı
  │   ├─ ✓ boş isim verilince hata fırlatmalı
  │   └─ email doğrulama
  │       └─ ✓ geçersiz email formatta hata fırlatmalı
  └─ findUser metodu
      └─ ✓ var olan kullanıcıyı bulmalı

@ParameterizedTest — Parametreli Testler

Aynı test mantığını farklı verilerle tekrar tekrar çalıştırmak için kullanılır:

import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.*;

class ParameterizedExamplesTest {

    // @ValueSource — basit değer listesi
    @ParameterizedTest(name = "{0} pozitif olmalı")
    @ValueSource(ints = {1, 5, 100, Integer.MAX_VALUE})
    void shouldBePositive(int number) {
        assertTrue(number > 0);
    }

    // @NullAndEmptySource — null ve boş string testi
    @ParameterizedTest
    @NullAndEmptySource
    @ValueSource(strings = {"  ", "\t", "\n"})
    void shouldRejectBlankStrings(String input) {
        assertThrows(IllegalArgumentException.class,
            () -> validator.validateName(input));
    }

    // @CsvSource — virgülle ayrılmış çoklu parametre
    @ParameterizedTest(name = "{0} + {1} = {2}")
    @CsvSource({
        "1, 1, 2",
        "2, 3, 5",
        "10, -5, 5",
        "0, 0, 0",
        "-1, -1, -2"
    })
    void shouldAdd(int a, int b, int expected) {
        assertEquals(expected, calculator.add(a, b));
    }

    // @EnumSource — enum değerleri ile test
    @ParameterizedTest
    @EnumSource(value = DayOfWeek.class, names = {"SATURDAY", "SUNDAY"})
    void shouldBeWeekend(DayOfWeek day) {
        assertTrue(calendarService.isWeekend(day));
    }

    // @MethodSource — metottan veri sağlama
    @ParameterizedTest
    @MethodSource("provideUsersForValidation")
    void shouldValidateUser(String name, String email, boolean expected) {
        assertEquals(expected, validator.isValid(name, email));
    }

    static Stream<Arguments> provideUsersForValidation() {
        return Stream.of(
            Arguments.of("Ali", "ali@test.com", true),
            Arguments.of("", "ali@test.com", false),
            Arguments.of("Ali", "invalid", false),
            Arguments.of(null, "ali@test.com", false)
        );
    }
}

Test Lifecycle ve Instance Per Method

JUnit 5 varsayılan olarak her test metodu için yeni bir test sınıfı instance'ı oluşturur. Bu, testler arasında state paylaşımını önler:

class InstancePerMethodTest {
    private int counter = 0;

    @Test
    void firstTest() {
        counter++;
        assertEquals(1, counter); // her zaman 1 — yeni instance
    }

    @Test
    void secondTest() {
        counter++;
        assertEquals(1, counter); // yine 1 — farklı instance
    }
}

// İsterseniz davranışı değiştirebilirsiniz (önerilmez):
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
class SharedInstanceTest {
    // Tek instance — testler arasında state paylaşılır
    // @BeforeAll ve @AfterAll static olmak zorunda değil
}

JUnit 5, modern Java test yazımının temelidir. @DisplayName ile okunabilir isimler, @Nested ile hiyerarşik yapı, @ParameterizedTest ile veri-güdümlü testler ve zengin assertion kütüphanesi sayesinde kapsamlı ve bakımı kolay test suitleri oluşturabilirsiniz.