Takip et

JUnit5 Tutorial: Kapsamlı Bir Rehber

JUnit5 Tutorial: Kapsamlı Bir Rehber Yazılım geliştirme sürecinde kalitenin ve güvenilirliğin sağlanması, modern uygulamaların vazgeçilm

JUnit5 Tutorial: Kapsamlı Bir Rehber

Yazılım geliştirme sürecinde kalitenin ve güvenilirliğin sağlanması, modern uygulamaların vazgeçilmez bir parçasıdır. Bu bağlamda, birim testleri (unit tests) yazılımın temel yapı taşlarının doğru çalıştığından emin olmanın en etkili yollarından biridir. Java ekosisteminde birim testleri için en yaygın ve güçlü araçlardan biri de şüphesiz JUnit’tir. Bu rehberde, JUnit’in en güncel sürümü olan JUnit5’i (resmi adıyla JUnit Jupiter) tüm detaylarıyla inceleyecek, temelden ileri seviyeye kadar özelliklerini kod örnekleriyle açıklayacağız.

JUnit5, önceki sürümlerine göre daha modüler, esnek ve güçlü bir yapı sunar. Geliştiricilere test yazma ve çalıştırma konusunda daha fazla kontrol ve olanak tanır. Uygulamalarınızın kalitesini artırmak, hataları erken aşamada tespit etmek ve kodunuzun sürdürülebilirliğini sağlamak için JUnit5’i etkin bir şekilde kullanmak kritik öneme sahiptir.

JUnit5 Kurulumu ve Proje Yapılandırması

JUnit5, modüler yapısı sayesinde farklı bileşenlerden oluşur: Jupiter (test API’si ve test motoru), Vintage (JUnit3 ve JUnit4 testlerini çalıştırma motoru) ve Platform (test motorlarının çalıştırıldığı temel API). Modern bir Java projesinde JUnit5’i kullanmaya başlamak için proje bağımlılıklarını eklemeniz gerekir. En yaygın kullanılan build araçları Maven ve Gradle için örnek yapılandırmaları aşağıda bulabilirsiniz.

Maven Projeleri İçin (pom.xml)

Maven projenizde JUnit5 kullanmak için pom.xml dosyanıza aşağıdaki bağımlılıkları ekleyin. Genellikle sadece junit-jupiter-api ve junit-jupiter-engine yeterli olacaktır. Testlerinizi çalıştırabilmek için Maven Surefire Plugin’in de doğru yapılandırıldığından emin olun.


    
    
        org.junit.jupiter
        junit-jupiter-api
        5.10.0
        test
    
    
        org.junit.jupiter
        junit-jupiter-engine
        5.10.0
        test
    



    
        
            org.apache.maven.plugins
            maven-surefire-plugin
            3.2.3
            
                17 
                17 
            
        
    

Gradle Projeleri İçin (build.gradle)

Gradle projenizde ise build.gradle dosyanıza aşağıdaki bağımlılıkları ekleyin. useJUnitPlatform() çağrısı, Gradle’ın JUnit Platform’u kullanarak testlerinizi çalıştırmasını sağlar.

plugins {
    id 'java'
}

repositories {
    mavenCentral()
}

dependencies {
    testImplementation 'org.junit.jupiter:junit-jupiter-api:5.10.0' // Güncel sürümü kontrol edin
    testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.10.0' // Güncel sürümü kontrol edin
}

test {
    useJUnitPlatform()
}

Bağımlılıkları ekledikten sonra projenizi güncelleyin (Maven için mvn clean install, Gradle için gradle build). Artık JUnit5 testleri yazmaya hazırsınız.

Temel JUnit5 Annotasyonları ve Assertions

JUnit5, test senaryolarınızı tanımlamak ve yönetmek için zengin bir annotasyon seti sunar. Ayrıca, test sonuçlarını doğrulamak için çeşitli assertion metotları mevcuttur.

Temel Annotasyonlar

  • @Test: Bir metodun bir test metodu olduğunu belirtir. En temel annotasyondur.
  • @DisplayName("Açıklayıcı Test Adı"): Test metodlarına veya sınıflarına okunabilir, açıklayıcı isimler vermenizi sağlar. Test raporlarında bu isimler görünür.
  • @BeforeEach: Her test metodundan önce çalışacak kodu tanımlar. Kaynakları başlatma veya test ortamını hazırlama gibi işlemler için idealdir.
  • @AfterEach: Her test metodundan sonra çalışacak kodu tanımlar. Kaynakları serbest bırakma veya test ortamını temizleme gibi işlemler için kullanılır.
  • @BeforeAll: Test sınıfındaki tüm test metotlarından önce bir kez çalışacak kodu tanımlar. Genellikle pahalı kaynakları (veritabanı bağlantısı gibi) başlatmak için kullanılır. Bu metodun static olması gerekir.
  • @AfterAll: Test sınıfındaki tüm test metotları bittikten sonra bir kez çalışacak kodu tanımlar. @BeforeAll ile başlatılan kaynakları serbest bırakmak için kullanılır. Bu metodun da static olması gerekir.

Assertions (Onaylamalar)

JUnit5’in org.junit.jupiter.api.Assertions sınıfı, test sonuçlarını doğrulamak için bir dizi statik metot sunar:

  • assertEquals(expected, actual): İki değerin eşit olup olmadığını kontrol eder.
  • assertTrue(condition): Bir koşulun doğru olup olmadığını kontrol eder.
  • assertFalse(condition): Bir koşulun yanlış olup olmadığını kontrol eder.
  • assertNull(object): Bir nesnenin null olup olmadığını kontrol eder.
  • assertNotNull(object): Bir nesnenin null olmadığını kontrol eder.
  • assertThrows(expectedType, executable): Belirli bir kod bloğunun beklenen bir istisnayı (exception) fırlatıp fırlatmadığını kontrol eder.
  • assertAll(executables...): Birden fazla assertion’ı aynı anda çalıştırır. Eğer bir assertion başarısız olursa diğerleri de çalışmaya devam eder ve tüm başarısızlıklar raporlanır.

Örnek Bir Test Sınıfı

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

class HesapMakinesi {
    public int topla(int a, int b) {
        return a + b;
    }

    public int cikar(int a, int b) {
        return a - b;
    }

    public int carp(int a, int b) {
        return a * b;
    }

    public double bol(int a, int b) {
        if (b == 0) {
            throw new IllegalArgumentException("Sıfıra bölme hatası!");
        }
        return (double) a / b;
    }
}

@DisplayName("Hesap Makinesi Testleri")
class HesapMakinesiTest {

    private HesapMakinesi hesapMakinesi;

    @BeforeAll
    static void setupAll() {
        System.out.println("Tüm testler başlamadan önce bir kez çalışır.");
        // Örneğin, veritabanı bağlantısı burada kurulabilir.
    }

    @BeforeEach
    void setup() {
        System.out.println("Her testten önce çalışır: HesapMakinesi objesi oluşturuluyor.");
        hesapMakinesi = new HesapMakinesi();
    }

    @Test
    @DisplayName("İki sayının toplamını doğru hesaplamalı")
    void testTopla() {
        assertEquals(5, hesapMakinesi.topla(2, 3), "Toplama işlemi yanlış.");
    }

    @Test
    @DisplayName("İki sayının farkını doğru hesaplamalı")
    void testCikar() {
        assertEquals(1, hesapMakinesi.cikar(3, 2), () -> "Çıkarma işlemi yanlış oldu."); // Lambda ile mesaj
    }

    @Test
    @DisplayName("İki sayının çarpımını doğru hesaplamalı")
    void testCarp() {
        assertEquals(6, hesapMakinesi.carp(2, 3));
    }

    @Test
    @DisplayName("Sıfıra bölme durumunda hata fırlatmalı")
    void testBolmeSifiraHata() {
        Exception exception = assertThrows(IllegalArgumentException.class, () ->
            hesapMakinesi.bol(10, 0));
        assertEquals("Sıfıra bölme hatası!", exception.getMessage());
    }

    @Test
    @DisplayName("Birden fazla assertion'ı aynı anda kontrol etmeli")
    void testCokluAssertion() {
        // Tüm assertion'lar çalışır ve başarısız olanların hepsi raporlanır.
        assertAll("Hesap Makinesi Temel İşlemleri",
            () -> assertEquals(5, hesapMakinesi.topla(2, 3)),
            () -> assertEquals(1, hesapMakinesi.cikar(3, 2)),
            () -> assertEquals(6, hesapMakinesi.carp(2, 3))
        );
    }

    @AfterEach
    void teardown() {
        System.out.println("Her testten sonra çalışır: HesapMakinesi objesi temizlendi.");
        hesapMakinesi = null; // Bellek yönetimi için null'a atama
    }

    @AfterAll
    static void teardownAll() {
        System.out.println("Tüm testler bittikten sonra bir kez çalışır.");
        // Örneğin, veritabanı bağlantısı burada kapatılabilir.
    }
}

Gelişmiş JUnit5 Özellikleri

JUnit5, daha karmaşık test senaryolarını ele almak için bir dizi gelişmiş özellik sunar. Bu özellikler, test kodunuzu daha okunabilir, sürdürülebilir ve esnek hale getirmenize yardımcı olur.

H3.1. Parametreli Testler (@ParameterizedTest)

Aynı test mantığını farklı veri setleriyle tekrar tekrar çalıştırmak istediğinizde parametreli testler çok kullanışlıdır. Bu, test kodunuzdaki tekrarı azaltır ve daha kapsamlı testler yazmanızı sağlar.

  • @ParameterizedTest: Bir metodun parametreli test olduğunu belirtir.
  • @ValueSource: Primitif değerler (String, int, long, double vb.) için tek bir argüman kaynağı sağlar.
  • @CsvSource: CSV (Comma Separated Values) formatında birden fazla argüman kaynağı sağlar.
  • @MethodSource: Bir metodun döndürdüğü Stream veya Collection türündeki argümanları kullanır.
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.CsvSource;
import org.junit.jupiter.params.provider.ValueSource;

import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.junit.jupiter.api.Assertions.assertEquals;

class StringUtils {
    public boolean isPalindrom(String text) {
        String cleanText = text.replaceAll("\\s+", "").toLowerCase();
        return new StringBuilder(cleanText).reverse().toString().equals(cleanText);
    }

    public String buyukHarfYap(String text) {
        return text.toUpperCase();
    }
}

@DisplayName("StringUtils Testleri")
class StringUtilsTest {

    private StringUtils stringUtils = new StringUtils();

    @ParameterizedTest
    @ValueSource(strings = {"madam", "kayak", "level", "radar"})
    @DisplayName("Palindrom testleri - ValueSource")
    void testIsPalindrom(String adayKelime) {
        assertTrue(stringUtils.isPalindrom(adayKelime), adayKelime + " bir palindrom olmalıydı.");
    }

    @ParameterizedTest
    @CsvSource({
        "hello, HELLO",
        "world, WORLD",
        "java, JAVA"
    })
    @DisplayName("Büyük harf dönüştürme testleri - CsvSource")
    void testBuyukHarfYap(String girdi, String beklenenCikti) {
        assertEquals(beklenenCikti, stringUtils.buyukHarfYap(girdi));
    }
}

H3.2. Tekrarlanan Testler (@RepeatedTest)

Bir testi belirli bir sayıda tekrarlamak istediğinizde @RepeatedTest annotasyonunu kullanabilirsiniz. Bu, özellikle rastgele değerler içeren veya zamanlama hassasiyeti olan testler için faydalı olabilir.

import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.RepeatedTest;
import org.junit.jupiter.api.RepetitionInfo;

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

@DisplayName("Tekrarlanan Test Örneği")
class RepeatedTestExample {

    @RepeatedTest(value = 5, name = "{displayName} - Tekrar {currentRepetition}/{totalRepetitions}")
    @DisplayName("Rastgele Sayı Testi")
    void testRastgeleSayi(RepetitionInfo repetitionInfo) {
        System.out.println("Test " + repetitionInfo.getCurrentRepetition() + ". kez çalışıyor.");
        double randomValue = Math.random();
        assertTrue(randomValue >= 0 && randomValue < 1, "Rastgele sayı 0 ile 1 arasında olmalı.");
    }
}

H3.3. Koşullu Test Yürütme (@EnabledOnOs, @DisabledOnJre vb.)

Testlerin belirli koşullara bağlı olarak çalıştırılmasını veya atlanmasını sağlayabilirsiniz. Bu, farklı işletim sistemleri, Java sürümleri veya sistem özellikleri için özelleştirilmiş testler yazarken faydalıdır.

  • @EnabledOnOs({OS.LINUX, OS.MAC}): Sadece belirtilen işletim sistemlerinde çalışır.
  • @DisabledOnOs(OS.WINDOWS): Belirtilen işletim sisteminde çalışmaz.
  • @EnabledOnJre(JRE.JAVA_11): Sadece belirtilen Java Runtime Environment (JRE) sürümünde çalışır.
  • @DisabledOnJre(JRE.JAVA_8): Belirtilen JRE sürümünde çalışmaz.
  • @EnabledIfSystemProperty(named = "os.arch", matches = "aarch64"): Belirli bir sistem özelliğinin değerine göre etkinleştirir.
  • @DisabledIfEnvironmentVariable(named = "ENV", matches = "prod"): Belirli bir ortam değişkeninin değerine göre devre dışı bırakır.
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.condition.DisabledOnOs;
import org.junit.jupiter.api.condition.EnabledOnOs;
import org.junit.jupiter.api.condition.OS;
import org.junit.jupiter.api.condition.JRE;
import org.junit.jupiter.api.condition.EnabledOnJre;

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

class ConditionalTestExample {

    @Test
    @EnabledOnOs(OS.LINUX)
    void onlyOnLinux() {
        System.out.println("Bu test sadece Linux'ta çalışır.");
        assertTrue(true);
    }

    @Test
    @DisabledOnOs(OS.WINDOWS)
    void notOnWindows() {
        System.out.println("Bu test Windows'ta çalışmaz.");
        assertTrue(true);
    }

    @Test
    @EnabledOnJre(JRE.JAVA_17)
    void onlyOnJava17() {
        System.out.println("Bu test sadece Java 17'de çalışır.");
        assertTrue(true);
    }
}

H3.4. İç İçe Testler (@Nested)

@Nested annotasyonu, test sınıflarını hiyerarşik bir şekilde organize etmenizi sağlar. Bu, belirli bir senaryonun alt senaryolarını gruplamak için idealdir ve test raporlarının daha okunabilir olmasına yardımcı olur.

import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;

class BankaHesabi {
    private double bakiye;

    public BankaHesabi(double bakiye) {
        this.bakiye = bakiye;
    }

    public void paraYatir(double miktar) {
        if (miktar <= 0) {
            throw new IllegalArgumentException("Yatırılacak miktar pozitif olmalı.");
        }
        this.bakiye += miktar;
    }

    public void paraCek(double miktar) {
        if (miktar <= 0) {
            throw new IllegalArgumentException("Çekilecek miktar pozitif olmalı.");
        }
        if (this.bakiye < miktar) {
            throw new IllegalArgumentException("Yetersiz bakiye.");
        }
        this.bakiye -= miktar;
    }

    public double getBakiye() {
        return bakiye;
    }
}

@DisplayName("Banka Hesabı Testleri")
class BankaHesabiTest {

    BankaHesabi hesap;

    @BeforeEach
    void setup() {
        hesap = new BankaHesabi(1000);
    }

    @Test
    @DisplayName("Başlangıç bakiyesi doğru olmalı")
    void testBaslangicBakiyesi() {
        assertEquals(1000, hesap.getBakiye());
    }

    @Nested
    @DisplayName("Para Yatırma İşlemleri")
    class ParaYatirmaTestleri {

        @Test
        @DisplayName("Geçerli miktar yatırıldığında bakiye artmalı")
        void testParaYatirGecerliMiktar() {
            hesap.paraYatir(200);
            assertEquals(1200, hesap.getBakiye());
        }

        @Test
        @DisplayName("Negatif miktar yatırıldığında hata fırlatmalı")
        void testParaYatirNegatifMiktar() {
            assertThrows(IllegalArgumentException.class, () -> hesap.paraYatir(-50));
            assertEquals(1000, hesap.getBakiye(), "Hata durumunda bakiye değişmemeli");
        }
    }

    @Nested
    @DisplayName("Para Çekme İşlemleri")
    class ParaCekmeTestleri {

        @Test
        @DisplayName("Geçerli miktar çekildiğinde bakiye azalmalı")
        void testParaCekGecerliMiktar() {
            hesap.paraCek(300);
            assertEquals(700, hesap.getBakiye());
        }

        @Test
        @DisplayName("Yetersiz bakiye durumunda hata fırlatmalı")
        void testParaCekYetersizBakiye() {
            assertThrows(IllegalArgumentException.class, () -> hesap.paraCek(1500));
            assertEquals(1000, hesap.getBakiye(), "Hata durumunda bakiye değişmemeli");
        }
    }
}

Pratik İpuçları ve En İyi Uygulamalar

Etkili ve sürdürülebilir birim testleri yazmak için bazı en iyi uygulamaları takip etmek önemlidir:

  • AAA Prensibi (Arrange-Act-Assert): Test metotlarınızı bu üç bölüme ayırın:
    1. Arrange (Hazırlık): Test edilecek nesneleri ve verileri hazırlayın.
    2. Act (Eylem): Test edilen metodu çağırın.
    3. Assert (Onaylama): Metodun beklenen çıktıyı veya davranışı sergilediğini doğrulayın.
  • Testlerin Bağımsızlığı: Her test metodu birbirinden bağımsız olmalı ve çalışma sırası diğer testleri etkilememelidir. @BeforeEach ve @AfterEach bu bağımsızlığı sağlamak için kullanılır.
  • Okunabilir Test İsimleri: @DisplayName kullanarak testlerin neyi test ettiğini açıkça belirten isimler verin. Metot isimleri de açıklayıcı olmalıdır (örneğin, shouldReturnCorrectSumWhenTwoPositiveNumbersAreAdded()).
  • Tek Sorumluluk Prensibi: Her test metodu tek bir şeyi test etmelidir. Karmaşık senaryoları birden fazla küçük teste bölün.
  • Hızlı Testler: Birim testleri hızlı çalışmalıdır. Harici bağımlılıkları (veritabanı, ağ çağrıları vb.) içeren testler entegrasyon testleri olarak sınıflandırılmalı ve ayrılmalıdır. Birim testlerinde bu tür bağımlılıkları Mocking (Mockito gibi kütüphanelerle) kullanarak simüle edin.
  • Do Not Test Private Methods (Özel Metotları Test Etmeyin): Genellikle özel metotları doğrudan test etmek yerine, bu metotları kullanan public metotları test edin. Eğer özel bir metot çok karmaşıksa ve ayrı test gerektiriyorsa, bu, metodun sorumluluğunun çok fazla olduğunu ve refactor edilmesi gerektiğini gösterebilir.
  • Test Kapsamı (Code Coverage): Testlerinizin kodunuzun ne kadarını kapsadığını ölçmek için JaCoCo gibi araçları kullanın. Yüksek test kapsamı iyi bir gösterge olsa da, testlerin kalitesini garanti etmez. Kapsamlı ve anlamlı testler yazmaya odaklanın.

Sonuç ve Sıkça Sorulan Sorular (SSS)

JUnit5, modern Java uygulamaları için güçlü ve esnek bir test çatısı sunar. Modüler yapısı, zengin annotasyon seti ve gelişmiş özellikleri sayesinde geliştiricilerin daha kaliteli, sürdürülebilir ve güvenilir kod yazmalarına yardımcı olur. Birim testleri, yazılım geliştirme yaşam döngüsünün ayrılmaz bir parçası olmalı ve sürekli olarak güncellenmelidir.

Sıkça Sorulan Sorular (SSS)

JUnit 4'ten JUnit 5'e geçiş nasıl yapılır?
JUnit 5, geriye dönük uyumluluğu sağlamak için "Vintage Engine" sunar. Bu motoru bağımlılıklarınıza ekleyerek JUnit 4 testlerinizi JUnit 5 ortamında çalıştırabilirsiniz. Yeni testlerinizi JUnit 5 annotasyonları ile yazmaya başlayabilir ve mevcut testlerinizi kademeli olarak migrate edebilirsiniz. Temel geçiş, org.junit yerine org.junit.jupiter.api paketlerini kullanmak ve yeni annotasyonları öğrenmektir.
Testleri IDE'den (IntelliJ IDEA, Eclipse) nasıl çalıştırırım?
Çoğu modern IDE, JUnit5'i doğrudan destekler. Test sınıfı veya metot isimlerinin yanında genellikle yeşil bir "oynat" (run) ikonu bulunur. Bu ikona tıklayarak ilgili testi veya tüm test sınıfını çalıştırabilirsiniz. Ayrıca, projenizin build aracını (Maven/Gradle) kullanarak da testleri komut satırından çalıştırabilirsiniz.
Parameterized testler neden önemlidir?
Parametreli testler, aynı test mantığını farklı girdi değerleriyle tekrar tekrar çalıştırmanıza olanak tanır. Bu, test kodunuzdaki tekrarı azaltır, daha kapsamlı test senaryoları oluşturur ve testlerinizi daha okunabilir hale getirir. Örneğin, bir metodun farklı geçerli ve geçersiz girdilerle nasıl davrandığını tek bir test metoduyla kontrol edebilirsiniz.
Testlerde istisna (exception) kontrolü nasıl yapılır?
Assertions.assertThrows(ExpectedExceptionType.class, () -> yourMethodThatThrowsException()) metodu ile belirli bir kod bloğunun beklenen bir istisnayı fırlatıp fırlatmadığını kontrol edebilirsiniz. Bu metot, fırlatılan istisna nesnesini de döndürerek istisnanın mesajını veya diğer özelliklerini kontrol etmenize olanak tanır.
Testleri gruplamak veya filtrelemek için ne kullanmalıyım?
JUnit5'te @Tag("etiket_adi") annotasyonunu kullanarak testlerinizi etiketleyebilirsiniz. Daha sonra Maven Surefire Plugin veya Gradle'ın test yapılandırması aracılığıyla bu etiketlere göre testleri dahil edebilir (includeTags) veya hariç tutabilirsiniz (excludeTags). Bu, sadece belirli bir modülle ilgili testleri çalıştırmak istediğinizde veya hızlı/yavaş testleri ayırmak istediğinizde çok kullanışlıdır.

Yorumlar
İçeriği beğendiniz mi? Bir tartışma başlatın veya görüşlerinizi paylaşın.
Yorum Yaz

Bir yanıt yazın

E-posta adresiniz yayınlanmayacak. Gerekli alanlar * ile işaretlenmişlerdir

E-posta Bülteni
Yazılım Topluluğuna Katılın
En son güncellemeleri, yaratıcı ipuçlarını ve özel kaynakları doğrudan e-posta kutunuza alın. Tasarım ve inovasyonun geleceğini birlikte keşfedelim.