İçeriğe geç
Educora
İleri22 dk14 / 18

JUnit 5 ile test yazma

JUnit 5 ile otomatik birim testleri yaz: doğrulamalar, parametreli testler ve Mockito ile bağımlılıkları taklit etme.

Kendini test et
Bu derste öğreneceklerin
  • @Test ve assertEquals, assertThrows, assertAll ile bir test sınıfı yazmak
  • Tek bir testi @ParameterizedTest ile birçok veri kümesi için çalıştırmak
  • Bir bağımlılığı Mockito ile taklit etmek ve çağrıları verify ile doğrulamak

Bir metotta küçük bir değişiklik yaptın ve program hâlâ çalışıyor. Peki başka on yerde bir şey bozulmadı mı? Her seferinde her şeyi elle kontrol etmek mümkün değil. Profesyonel ekipler bunun için otomatik testler yazar: kodunu çağıran ve sonucu beklenen değerle karşılaştıran küçük metotlar. Yüzlerce test birkaç saniyede çalışır ve hatayı kullanıcıdan önce yakalar.

Test nedir?

Bir birim testi (unit test), küçük bir kod parçasını, genellikle tek bir metodu, diğerlerinden ayrı olarak sınar. Fikir basittir: beklenen sonucu bilirsin, metodu çağırırsın ve ikisini karşılaştırırsın. Aşağıda hiçbir kütüphane kullanmadan yazılmış minik bir test “çatısı” var. Üçüncü kontrolde beklenti bilerek yanlış yazıldı ki başarısız bir testin nasıl göründüğünü gör.

Java
import java.util.List;

public class Main {
    static int passed = 0, failed = 0;

    static void check(String name, Object expected, Object actual) {
        if (expected.equals(actual)) {
            passed++;
            System.out.println("PASS " + name);
        } else {
            failed++;
            System.out.println("FAIL " + name + ": expected " + expected + " but was " + actual);
        }
    }

    public static void main(String[] args) {
        check("average of 85, 90, 95", 90.0, Grades.average(List.of(85, 90, 95)));
        check("95 is an A", "A", Grades.letter(95));
        check("79 is a B", "B", Grades.letter(79));
        System.out.println(passed + " passed, " + failed + " failed");
    }
}

class Grades {
    static double average(List<Integer> scores) {
        if (scores.isEmpty()) throw new IllegalArgumentException("No scores");
        return scores.stream().mapToInt(Integer::intValue).average().getAsDouble();
    }

    static String letter(int score) {
        if (score < 0 || score > 100) throw new IllegalArgumentException("Score out of range: " + score);
        return score >= 90 ? "A" : score >= 80 ? "B" : score >= 70 ? "C" : score >= 50 ? "D" : "F";
    }
}
Beklenen çıktı
PASS average of 85, 90, 95
PASS 95 is an A
FAIL 79 is a B: expected B but was C
2 passed, 1 failed
Testin özü: beklenen ve gerçek sonucu karşılaştırmak

JUnit aynı işi profesyonel düzeyde yapar: test metotlarını kendisi bulur, her birini ayrı çalıştırır, hataları toplar ve IDE'de, Maven'da ya da Gradle'da düzenli bir rapor sunar. Önceki dersteki org.junit.jupiter:junit-jupiter bağımlılığı (test kapsamıyla) yeterlidir. JUnit 5'in ana kısmının adı Jupiter'dir; bu yüzden paketleri org.junit.jupiter ile başlar.

İlk JUnit 5 testlerin

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

import java.util.List;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;

class GradesTest {

    @Test
    void averageOfThreeScores() {
        List<Integer> scores = List.of(85, 90, 95);   // Arrange
        double result = Grades.average(scores);        // Act
        assertEquals(90.0, result, 0.001);             // Assert
    }

    @Test
    @DisplayName("An empty list is rejected")
    void emptyListThrows() {
        var ex = assertThrows(IllegalArgumentException.class, () -> Grades.average(List.of()));
        assertEquals("No scores", ex.getMessage());
    }

    @Test
    void lettersAtBoundaries() {
        assertAll(
            () -> assertEquals("A", Grades.letter(90)),
            () -> assertEquals("B", Grades.letter(80)),
            () -> assertEquals("F", Grades.letter(49)));
    }
}
src/test/java/GradesTest.java — Grades sınıfı yukarıdakiyle aynıdır

Her test metodunun üstünde @Test ek açıklaması vardır; metodun public olması gerekmez ve bir şey döndürmez. İlk test AAA kalıbını izler: Arrange (veriyi hazırla), Act (metodu çağır), Assert (sonucu doğrula). assertEquals(expected, actual, delta) ondalıklı sayıları 0.001 hassasiyetle karşılaştırır, çünkü Java'da 0.1 + 0.2 tam olarak 0.3 değil, 0.30000000000000004'tür. assertThrows bir istisnanın fırlatıldığını doğrular ve onu döndürür; böylece mesajını da kontrol edebilirsin. assertAll ise tüm kontrolleri çalıştırır ve ilk hatada durmaz.

Ek açıklamaNe yapar
@Testmetodu test olarak işaretler
@DisplayName("...")raporda okunaklı bir ad verir
@BeforeEach / @AfterEachher testten önce / sonra çalışır
@BeforeAll / @AfterAlltüm sınıf için bir kez çalışır (static metot)
@Disabledtesti geçici olarak devre dışı bırakır
@ParameterizedTesttesti her veri satırı için ayrı çalıştırır

JUnit her test için yeni bir nesne oluşturur; bu yüzden testler birbirini etkilemez ve herhangi bir sırayla çalışabilir. Testleri IDE'de yeşil ▶ düğmesiyle, terminalde ise mvn test ya da ./gradlew test ile çalıştırırsın. Birinin letter metodundaki score >= 80 koşulunu yanlışlıkla score > 80 yaptığını düşün:

Terminal
mvn test
Beklenen çıktı
[INFO] Running GradesTest
[ERROR] Tests run: 3, Failures: 1, Errors: 0, Skipped: 0
[ERROR] Failures:
[ERROR]   GradesTest.lettersAtBoundaries:25 Multiple Failures (1 failure)
	org.opentest4j.AssertionFailedError: expected: <B> but was: <C>
[INFO] BUILD FAILURE
Örnek çıktı (kısaltılmış): test hatayı hemen yakaladı; 80 puan artık “B” değil
JUnit 4 (eski)
import org.junit.Test;                          // JUnit 4
import static org.junit.Assert.assertEquals;

public class GradesTest {
    @Test(expected = IllegalArgumentException.class)
    public void emptyListThrows() {
        Grades.average(List.of());
    }
}
JUnit 5 (modern)
import org.junit.jupiter.api.Test;              // JUnit 5
import static org.junit.jupiter.api.Assertions.*;

class GradesTest {
    @Test
    void emptyListThrows() {
        var ex = assertThrows(IllegalArgumentException.class, () -> Grades.average(List.of()));
        assertEquals("No scores", ex.getMessage());
    }
}
JUnit 5'te sınıf ve metotların public olması gerekmez; istisnanın mesajı da sınanabilir

Parametreli testler

Aynı kontrolü farklı verilerle tekrarlamak için testi kopyalaman gerekmez. @ParameterizedTest testi her veri kümesi için ayrı çalıştırır: @CsvSource virgülle ayrılmış her satırı metodun parametrelerine aktarır, @ValueSource ise basit bir değer listesi içindir. Raporda her durum ayrı görünür, örneğin 85 -> B. Bu ek açıklamalar junit-jupiter-params modülündedir ve zaten junit-jupiter bağımlılığına dahildir.

Kopyala-yapıştır testler
@Test void letterFor95() { assertEquals("A", Grades.letter(95)); }
@Test void letterFor85() { assertEquals("B", Grades.letter(85)); }
@Test void letterFor72() { assertEquals("C", Grades.letter(72)); }
@Test void letterFor49() { assertEquals("F", Grades.letter(49)); }
// one more case = one more copy-pasted method...
Parametreli testler: yeni durum = yeni satır
@ParameterizedTest(name = "{0} -> {1}")
@CsvSource({ "95, A", "85, B", "72, C", "49, F" })
void letterForScore(int score, String expected) {
    assertEquals(expected, Grades.letter(score));
}

@ParameterizedTest
@ValueSource(ints = { -1, 101, 150 })
void outOfRangeScoresAreRejected(int score) {
    assertThrows(IllegalArgumentException.class, () -> Grades.letter(score));
}

Mockito: bağımlılıkları taklit etmek

Gerçek bir sınıf çoğu zaman başka nesnelere bağlıdır: bir veritabanına, bir e-posta sunucusuna, dış bir API'ye. Birim testinde bunlara gerçekten erişmek yavaş ve güvenilmezdir. Çözüm, bağımlılığı yapıcı üzerinden vermek ve testte onu bir mock (sahte nesne) ile değiştirmektir. Aşağıdaki ReportService puanları ScoreRepository arayüzünden alır ve onun veritabanını mı okuduğunu yoksa sahte mi olduğunu bilmez.

Java
interface ScoreRepository {
    List<Integer> findScores(String student);   // e.g. reads a database
}

class ReportService {
    private final ScoreRepository repository;

    ReportService(ScoreRepository repository) {
        this.repository = repository;
    }

    String report(String student) {
        return student + ": " + Grades.average(repository.findScores(student));
    }
}
Bağımlılık yapıcıdan gelir; bu, sınıfı test etmeyi kolaylaştırır
Java
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.mockito.Mockito.*;

import java.util.List;
import org.junit.jupiter.api.Test;

class ReportServiceTest {

    @Test
    void reportUsesScoresFromRepository() {
        ScoreRepository repo = mock(ScoreRepository.class);
        when(repo.findScores("Aysel")).thenReturn(List.of(90, 80));

        ReportService service = new ReportService(repo);
        String report = service.report("Aysel");

        assertEquals("Aysel: 85.0", report);
        verify(repo).findScores("Aysel");
        verifyNoMoreInteractions(repo);
    }
}
Mockito için org.mockito:mockito-junit-jupiter bağımlılığı (test kapsamı) gerekir

mock(...), arayüzün boş bir sahte kopyasını oluşturur. when(...).thenReturn(...) ona “Aysel sorulursa 90 ve 80 döndür” der; buna stub (saplama) denir. verify, servisin depoyu tam bir kez ve doğru argümanla çağırdığını doğrular; verifyNoMoreInteractions ise fazladan çağrı olmadığını teyit eder. Büyük test sınıflarında aynı iş ek açıklamalarla yapılır: sınıfın üstüne @ExtendWith(MockitoExtension.class), alanın üstüne @Mock yazılır.

Önemli noktalar

  • Birim testi küçük bir kod parçasını ayrı olarak sınar: beklenen sonucu gerçek sonuçla karşılaştırır.
  • JUnit 5: @Test, assertEquals(expected, actual), assertThrows, assertAll; testler src/test/java içinde yaşar.
  • AAA kalıbı: Arrange → Act → Assert; her test yeni bir nesnede çalışır ve diğerlerine bağlı değildir.
  • @ParameterizedTest + @CsvSource / @ValueSource tek bir testi birçok veriyle çalıştırır.
  • Mockito bağımlılıkları taklit eder: mock, when(...).thenReturn(...), verify; bağımlılığı yapıcıyla vermek testi kolaylaştırır.

Kendini test et

10 soru. Her doğru cevap XP kazandırır.

1 / 10
assertEquals(0.3, 0.1 + 0.2) testi neden başarısız olur?