@TestveassertEquals,assertThrows,assertAllile bir test sınıfı yazmak- Tek bir testi
@ParameterizedTestile birçok veri kümesi için çalıştırmak - Bir bağımlılığı Mockito ile taklit etmek ve çağrıları
verifyile 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.
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";
}
}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
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
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ırHer 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çıklama | Ne yapar |
|---|---|
@Test | metodu test olarak işaretler |
@DisplayName("...") | raporda okunaklı bir ad verir |
@BeforeEach / @AfterEach | her testten önce / sonra çalışır |
@BeforeAll / @AfterAll | tüm sınıf için bir kez çalışır (static metot) |
@Disabled | testi geçici olarak devre dışı bırakır |
@ParameterizedTest | testi 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:
mvn test[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
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());
}
}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());
}
}public olması gerekmez; istisnanın mesajı da sınanabilirParametreli 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.
@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...@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.
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));
}
}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);
}
}org.mockito:mockito-junit-jupiter bağımlılığı (test kapsamı) gerekirmock(...), 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; testlersrc/test/javaiç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/@ValueSourcetek 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.
assertEquals(0.3, 0.1 + 0.2) testi neden başarısız olur?