Unit Testing: JUnit 5 ve Mockito
Kod yazdın, çalıştırdın, ekranda doğru sonucu gördün. "Tamam, çalışıyor" dedin. Peki ya bir hafta sonra başka bir yeri değiştirince bu kod bozulursa? Peki ya edge case'leri düşünmeyi unuttuysan? Peki ya ekibe yeni katılan biri senin kodunu yanlışlıkla kırarsa?
İşte unit testing tam burada devreye giriyor. Test yazarak kodunun doğru çalıştığını kanıtlarsın — her seferinde elle kontrol etmek yerine, bir tuşla tüm sistemi doğrularsın.
Bu derste Java'nın standart test framework'ü JUnit 5 ve mock kütüphanesi Mockito ile test yazmanın temellerini öğreneceğiz.
1. Neden Test Yazmalıyız?
"Çalışıyor ya, neden test yazayım?" sorusunu hemen herkes sorar. Cevap basit: bugün çalışması yarın çalışacağı anlamına gelmez.
🎯 Analoji — Paraşüt Kontrolü:
>
Test yazmayı paraşüt kontrolü gibi düşün. Uçaktan atlamadan önce paraşütü kontrol etmezsen, muhtemelen açılır — ama "muhtemelen" hayatını riske atacak bir kelime. Her atlayıştan önce kontrol yapmak zaman alır mı? Evet. Ama bir gün seni kurtarır. Unit test de öyle — her commit'ten önce testleri koşarsın, bir gün seni canlı ortamda büyük bir hatadan kurtarır.
Test yazmanın somut faydaları:
Güven: Kodu değiştirirken bir şeyi kırmadığından emin olursun
Dokümantasyon: Test kodu, metodun nasıl kullanılacağını gösterir
Tasarım: Test yazılabilir kod yazmak, doğal olarak daha iyi tasarıma yönlendirir
Hız: Manuel test dakikalar sürer, otomatik test milisaniyeler
Refactoring: Testlerin varsa kodu cesurca yeniden yapılandırabilirsin
2. JUnit 5 — Kurulum
JUnit 5, Java ekosisteminin standart test framework'üdür. Maven veya Gradle ile projeye eklersin:
Maven:
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<version>5.10.2</version>
<scope>test</scope>
</dependency>Gradle:
testImplementation 'org.junit.jupiter:junit-jupiter:5.10.2'
test {
useJUnitPlatform()
}Test dosyaları src/test/java/ altına, ana kodla aynı paket yapısında konur:
src/
├── main/java/com/example/
│ └── Calculator.java ← Ana kod
└── test/java/com/example/
└── CalculatorTest.java ← Test kodu3. İlk Test — @Test ve Assertions
Basit bir Calculator sınıfı ve testi:
// src/main/java/com/example/Calculator.java
class Calculator {
int add(int a, int b) {
return a + b;
}
int divide(int a, int b) {
if (b == 0) throw new ArithmeticException("Sıfıra bölme!");
return a / b;
}
boolean isPositive(int n) {
return n > 0;
}
}// src/test/java/com/example/CalculatorTest.java
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;
class CalculatorTest {
private final Calculator calc = new Calculator();
@Test
void testAdd() {
assertEquals(5, calc.add(2, 3));
assertEquals(0, calc.add(-1, 1));
assertEquals(-5, calc.add(-2, -3));
}
@Test
void testDivide() {
assertEquals(3, calc.divide(9, 3));
assertEquals(0, calc.divide(1, 3)); // integer bölme
}
@Test
void testDivideByZero() {
assertThrows(ArithmeticException.class, () -> calc.divide(10, 0));
}
@Test
void testIsPositive() {
assertTrue(calc.isPositive(5));
assertFalse(calc.isPositive(-3));
assertFalse(calc.isPositive(0));
}
}Temel Assertion'lar
| Assertion | Ne Yapar |
|---|---|
assertEquals(expected, actual) | İki değerin eşit olduğunu doğrular |
assertNotEquals(a, b) | İki değerin farklı olduğunu doğrular |
assertTrue(condition) | Koşulun true olduğunu doğrular |
assertFalse(condition) | Koşulun false olduğunu doğrular |
assertNull(obj) | Değerin null olduğunu doğrular |
assertNotNull(obj) | Değerin null olmadığını doğrular |
assertThrows(ExType.class, () -> ...) | Exception fırlatıldığını doğrular |
assertDoesNotThrow(() -> ...) | Exception fırlatılmadığını doğrular |
assertArrayEquals(arr1, arr2) | İki dizinin eşit olduğunu doğrular |
assertIterableEquals(list1, list2) | İki koleksiyonun eşit olduğunu doğrular |
Assertion mesajları: Her assertion'a son parametre olarak hata mesajı ekleyebilirsin:
assertEquals(5, calc.add(2, 3), "2 + 3 sonucu 5 olmalı");Test başarısız olursa bu mesaj çıktıda görünür — hata nedenini anlamayı kolaylaştırır.
assertAll — Birden Fazla Doğrulama
Normalde ilk başarısız assertion'da test durur. assertAll() ile tüm assertion'ları çalıştırıp hepsinin sonucunu görebilirsin:
@Test
void testUserProperties() {
User user = new User("Ali", "ali@test.com", 25);
assertAll("Kullanıcı özellikleri",
() -> assertEquals("Ali", user.name()),
() -> assertEquals("ali@test.com", user.email()),
() -> assertEquals(25, user.age()),
() -> assertNotNull(user.name())
);
}4. Test Yaşam Döngüsü — @BeforeEach, @AfterEach
Testler arasında ortak kurulum (setup) ve temizlik (cleanup) işlemleri yapman gerekebilir. JUnit 5 bunun için yaşam döngüsü annotation'ları sunar:
import org.junit.jupiter.api.*;
class DatabaseTest {
private Connection connection;
@BeforeAll
static void setupAll() {
System.out.println("Tüm testlerden ÖNCE bir kez çalışır");
// Veritabanı şemasını oluştur
}
@BeforeEach
void setup() {
System.out.println("Her testten ÖNCE çalışır");
// Her test için temiz bir bağlantı aç
connection = DriverManager.getConnection("jdbc:h2:mem:test");
}
@AfterEach
void teardown() {
System.out.println("Her testten SONRA çalışır");
// Bağlantıyı kapat
connection.close();
}
@AfterAll
static void teardownAll() {
System.out.println("Tüm testlerden SONRA bir kez çalışır");
// Veritabanını kaldır
}
@Test
void testInsert() {
// connection kullanılabilir, setup() tarafından açıldı
}
@Test
void testQuery() {
// her test kendi temiz connection'ına sahip
}
}| Annotation | Ne Zaman Çalışır | Static? |
|---|---|---|
@BeforeAll | Tüm testlerden önce (1 kez) | Evet |
@BeforeEach | Her testten önce | Hayır |
@AfterEach | Her testten sonra | Hayır |
@AfterAll | Tüm testlerden sonra (1 kez) | Evet |
💡 İpucu:
@BeforeAllve@AfterAllmetodlarıstaticolmalı (varsayılan test yaşam döngüsünde). Pahalı kaynakları (veritabanı bağlantısı, dosya sistemi) burada hazırla.
5. Test Organizasyonu — @DisplayName, @Disabled, @Tag, @Nested
@DisplayName — Okunabilir Test İsimleri
@DisplayName("Hesap Makinesi Testleri")
class CalculatorTest {
@Test
@DisplayName("İki pozitif sayıyı toplar")
void testAddPositive() {
assertEquals(5, new Calculator().add(2, 3));
}
@Test
@DisplayName("Sıfıra bölme ArithmeticException fırlatır")
void testDivideByZero() {
assertThrows(ArithmeticException.class,
() -> new Calculator().divide(10, 0));
}
}Test raporlarında metod adları yerine bu açıklamalar görünür. Özellikle CI/CD raporlarında çok işe yarar.
@Disabled — Testi Geçici Olarak Kapat
@Test
@Disabled("Veritabanı migration'ı tamamlanınca açılacak")
void testComplexQuery() {
// Bu test şu an çalıştırılmaz
}@Tag — Testleri Kategorize Et
@Tag("slow")
@Test
void testLargeDataImport() { /* ... */ }
@Tag("fast")
@Test
void testSimpleCalculation() { /* ... */ }Maven'da sadece belirli tag'leri çalıştır:
<plugin>
<artifactId>maven-surefire-plugin</artifactId>
<configuration>
<groups>fast</groups>
<excludedGroups>slow</excludedGroups>
</configuration>
</plugin>@Nested — İç İçe Test Grupları
@DisplayName("UserService Testleri")
class UserServiceTest {
@Nested
@DisplayName("Kullanıcı oluşturma")
class CreateUser {
@Test
@DisplayName("Geçerli bilgilerle kullanıcı oluşturur")
void validUser() { /* ... */ }
@Test
@DisplayName("Boş isimle hata fırlatır")
void emptyName() { /* ... */ }
}
@Nested
@DisplayName("Kullanıcı silme")
class DeleteUser {
@Test
@DisplayName("Var olan kullanıcıyı siler")
void existingUser() { /* ... */ }
@Test
@DisplayName("Olmayan kullanıcı için false döner")
void nonExistingUser() { /* ... */ }
}
}@Nested ile testleri mantıksal gruplar halinde organize edebilirsin. Test raporlarında güzel bir hiyerarşi oluşur.
6. Parameterized Tests — Aynı Testi Farklı Verilerle Çalıştır
Aynı testi farklı girdi değerleriyle tekrar tekrar yazmak yerine parametreli testler kullanabilirsin.
@ValueSource — Basit Değerler
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.ValueSource;
class StringTest {
@ParameterizedTest
@ValueSource(strings = {"hello", "world", "java", "test"})
void testNotEmpty(String input) {
assertFalse(input.isEmpty());
}
@ParameterizedTest
@ValueSource(ints = {2, 4, 6, 8, 100})
void testEvenNumbers(int number) {
assertEquals(0, number % 2, number + " çift olmalı");
}
}@CsvSource — Girdi-Çıktı Çiftleri
@ParameterizedTest
@CsvSource({
"1, 1, 2",
"2, 3, 5",
"-1, 1, 0",
"0, 0, 0",
"100, -50, 50"
})
void testAdd(int a, int b, int expected) {
assertEquals(expected, new Calculator().add(a, b));
}
@ParameterizedTest
@CsvSource({
"hello, 5",
"java, 4",
"'', 0",
"'hello world', 11"
})
void testStringLength(String input, int expectedLength) {
assertEquals(expectedLength, input.length());
}@MethodSource — Kompleks Veri Kaynakları
@ParameterizedTest
@MethodSource("provideUserData")
void testUserCreation(String name, String email, boolean valid) {
User user = new User(name, email);
assertEquals(valid, user.isValid());
}
static Stream<Arguments> provideUserData() {
return Stream.of(
Arguments.of("Ali", "ali@test.com", true),
Arguments.of("", "ali@test.com", false),
Arguments.of("Ali", "", false),
Arguments.of("Ali", "invalid-email", false)
);
}@EnumSource — Enum Değerleriyle Test
@ParameterizedTest
@EnumSource(Month.class)
void testMonthRange(Month month) {
int value = month.getValue();
assertTrue(value >= 1 && value <= 12);
}
@ParameterizedTest
@EnumSource(value = Month.class, names = {"JUNE", "JULY", "AUGUST"})
void testSummerMonths(Month month) {
assertTrue(month.getValue() >= 6 && month.getValue() <= 8);
}7. Exception Testi ve Timeout
Exception Doğrulama
@Test
void testExceptionMessage() {
var exception = assertThrows(IllegalArgumentException.class,
() -> new User("", "ali@test.com"));
// Exception mesajını da kontrol et
assertEquals("İsim boş olamaz", exception.getMessage());
assertTrue(exception.getMessage().contains("boş"));
}Timeout Testi
@Test
@Timeout(value = 2, unit = TimeUnit.SECONDS)
void testPerformance() {
// 2 saniye içinde bitmezse test başarısız olur
var result = slowOperation();
assertNotNull(result);
}
@Test
void testWithAssertTimeout() {
// Lambda içindeki kod 1 saniyede bitmeli
assertTimeout(Duration.ofSeconds(1), () -> {
Thread.sleep(500); // 0.5 sn — geçer
});
}8. Mockito — Bağımlılıkları Taklit Etme
Gerçek dünyada sınıflar tek başına çalışmaz — veritabanına bağlanır, API çağırır, e-posta gönderir. Unit test yazarken bu dış bağımlılıkları gerçekten çalıştırmak istemezsin. Yavaş, güvenilmez ve yan etkileri var.
Mockito ile bu bağımlılıkları taklit (mock) edersin. Mock nesneler gerçek nesne gibi davranır ama aslında senin kontrol ettiğin sahte nesnelerdir.
Kurulum
Maven:
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-core</artifactId>
<version>5.10.0</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-junit-jupiter</artifactId>
<version>5.10.0</version>
<scope>test</scope>
</dependency>Temel Kullanım — @Mock ve when/thenReturn
Diyelim bir OrderService var ve UserRepository'ye bağımlı:
// Ana kod
interface UserRepository {
User findById(int id);
boolean existsByEmail(String email);
}
class OrderService {
private final UserRepository userRepo;
OrderService(UserRepository userRepo) {
this.userRepo = userRepo;
}
String placeOrder(int userId, String product) {
User user = userRepo.findById(userId);
if (user == null) {
throw new IllegalArgumentException("Kullanıcı bulunamadı");
}
return "Sipariş oluşturuldu: " + user.name() + " → " + product;
}
}// Test
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import static org.mockito.Mockito.*;
import static org.junit.jupiter.api.Assertions.*;
@ExtendWith(MockitoExtension.class)
class OrderServiceTest {
@Mock
UserRepository userRepo; // sahte UserRepository
@InjectMocks
OrderService orderService; // mock'lar otomatik enjekte edilir
@Test
void testPlaceOrderSuccess() {
// Mock davranışını tanımla
when(userRepo.findById(1))
.thenReturn(new User(1, "Ali", "ali@test.com", 25));
// Test et
String result = orderService.placeOrder(1, "Laptop");
assertEquals("Sipariş oluşturuldu: Ali → Laptop", result);
// Mock'un çağrıldığını doğrula
verify(userRepo).findById(1);
}
@Test
void testPlaceOrderUserNotFound() {
when(userRepo.findById(99)).thenReturn(null);
assertThrows(IllegalArgumentException.class,
() -> orderService.placeOrder(99, "Tablet"));
verify(userRepo).findById(99);
}
}Açıklama:
@Mock— Sahte (mock) nesne oluşturur@InjectMocks— Mock'ları constructor üzerinden test edilen sınıfa enjekte ederwhen(...).thenReturn(...)— "Bu metod bu parametreyle çağrılırsa şunu döndür"verify(...)— "Bu metod gerçekten çağrıldı mı?" doğrulaması
verify — Detaylı Doğrulama
// Tam olarak 1 kez çağrıldı mı?
verify(userRepo, times(1)).findById(1);
// Hiç çağrılmadı mı?
verify(userRepo, never()).existsByEmail(anyString());
// En az 2 kez çağrıldı mı?
verify(userRepo, atLeast(2)).findById(anyInt());
// Parametre yakalamak
ArgumentCaptor<Integer> captor = ArgumentCaptor.forClass(Integer.class);
verify(userRepo).findById(captor.capture());
assertEquals(1, captor.getValue());thenThrow — Exception Simülasyonu
@Test
void testDatabaseError() {
when(userRepo.findById(anyInt()))
.thenThrow(new RuntimeException("DB bağlantı hatası"));
assertThrows(RuntimeException.class,
() -> orderService.placeOrder(1, "Mouse"));
}⚠️ Dikkat: Mock sadece dış bağımlılıklar için kullan. Test ettiğin sınıfın kendisini mock'lama — bu testin anlamını yok eder.
OrderService'i test ediyorsan,OrderService'i mock'lama; onun bağımlılığı olanUserRepository'yi mock'la.
9. Mockito — İleri Teknikler
any() Matchers — Esnek Parametre Eşleme
// Herhangi bir int değeri için
when(userRepo.findById(anyInt())).thenReturn(new User(1, "Ali", "a@t.com", 25));
// Herhangi bir String için
when(userRepo.existsByEmail(anyString())).thenReturn(true);
// Belirli bir koşul
when(userRepo.findById(argThat(id -> id > 0)))
.thenReturn(new User(1, "Ali", "a@t.com", 25));spy — Gerçek Nesnenin Kısmi Mock'u
@Mock tüm metodları sahte yapar. @Spy ise gerçek nesneyi kullanır ama istediğin metodları override edebilirsin:
@Spy
List<String> spyList = new ArrayList<>();
@Test
void testSpy() {
spyList.add("gerçek eleman"); // Gerçekten eklenir
assertEquals(1, spyList.size());
// Sadece size() metodunu sahte yap
doReturn(100).when(spyList).size();
assertEquals(100, spyList.size());
// Ama eleman hâlâ orada
assertEquals("gerçek eleman", spyList.get(0));
}void Metod Mock'lama
// void metodlar için doNothing / doThrow kullan
doNothing().when(emailService).sendEmail(anyString(), anyString());
doThrow(new RuntimeException("SMTP hatası"))
.when(emailService).sendEmail(eq("invalid"), anyString());10. Test-Driven Development (TDD)
TDD, önce testi yazıp sonra kodu yazmayı öngören bir yaklaşımdır. Döngüsü çok basit:
1. RED → Başarısız test yaz (kod henüz yok)
2. GREEN → Testi geçirecek minimum kodu yaz
3. REFACTOR → Kodu düzelt, iyileştir (testler hâlâ geçmeli)TDD Örneği — Password Validator
Adım 1 — RED: Testi yaz
class PasswordValidatorTest {
private final PasswordValidator validator = new PasswordValidator();
@Test
void shortPasswordShouldFail() {
assertFalse(validator.isValid("abc"));
}
@Test
void passwordWithoutDigitShouldFail() {
assertFalse(validator.isValid("abcdefgh"));
}
@Test
void validPasswordShouldPass() {
assertTrue(validator.isValid("securePass1"));
}
}Adım 2 — GREEN: Minimum kodu yaz
class PasswordValidator {
boolean isValid(String password) {
if (password == null || password.length() < 8) return false;
return password.chars().anyMatch(Character::isDigit);
}
}Adım 3 — REFACTOR: İyileştir (gerekiyorsa)
TDD başta yavaş hissettirir ama uzun vadede daha az hata, daha iyi tasarım ve daha cesur refactoring sağlar.
💡 İpucu: TDD'yi her yerde uygulamak zorunda değilsin. Karmaşık iş mantığı, algoritma ve validation kuralları için harika. UI kodu veya basit CRUD için overkill olabilir. Deneyimle dengeyi bulursun.
11. Best Practice — İyi Test Nasıl Yazılır?
Test İsimlendirmesi
// ❌ Kötü
@Test void test1() { }
// ✅ İyi — ne test edildiği belli
@Test void addTwoPositiveNumbersReturnsSum() { }
// ✅ İyi — BDD stili
@Test void shouldThrowExceptionWhenDividingByZero() { }AAA Pattern (Arrange-Act-Assert)
@Test
void testDiscountCalculation() {
// Arrange — hazırla
var calculator = new PriceCalculator();
double originalPrice = 100.0;
double discountPercent = 20.0;
// Act — çalıştır
double finalPrice = calculator.applyDiscount(originalPrice, discountPercent);
// Assert — doğrula
assertEquals(80.0, finalPrice, 0.001);
}Her Test Bağımsız Olmalı
Testler birbirinden bağımsız çalışmalı — birinin sonucu diğerini etkilememeli. Ortak durumu @BeforeEach ile her test öncesi sıfırla.
Tek Bir Şeyi Test Et
Bir test metodu tek bir davranışı doğrulamalı. "Hem ekleme hem silme hem güncelleme" diye bir test yazma — her biri ayrı test olsun.
Test Sayısı
Genel kural: Her public metod için en az 3 test:
Normal durum (happy path)
Sınır değerler (edge cases)
Hata durumları (error cases)
12. Testleri Çalıştırma
# Maven ile tüm testleri çalıştır
mvn test
# Belirli bir test sınıfını çalıştır
mvn test -Dtest=CalculatorTest
# Belirli bir test metodunu çalıştır
mvn test -Dtest=CalculatorTest#testAdd
# Gradle ile
./gradlew test
# IDE'de: Test sınıfına sağ tık → RunTest sonuçları target/surefire-reports/ (Maven) veya build/reports/tests/ (Gradle) dizininde HTML rapor olarak da bulunur.
Özet
Unit test, kodun doğru çalıştığını otomatik olarak doğrular. Manuel test yerine bir tuşla tüm sistemi kontrol edersin — refactoring güveni, regresyon koruması ve canlı dokümantasyon sağlar.
JUnit 5 ile
@Testannotation'ı veassertEquals,assertTrue,assertThrowsgibi assertion'larla testler yazılır.@BeforeEach/@AfterEachile test öncesi/sonrası hazırlık yapılır.Parameterized tests (
@ValueSource,@CsvSource,@MethodSource) ile aynı testi farklı veri setleriyle tekrar tekrar çalıştırabilirsin — kod tekrarını önler.Mockito ile dış bağımlılıkları (veritabanı, API, e-posta servisi) taklit edersin.
@Mocksahte nesne oluşturur,when/thenReturndavranışı tanımlar,verifyçağrıyı doğrular.TDD (Test-Driven Development): Önce test yaz (RED), sonra kodu yaz (GREEN), sonra iyileştir (REFACTOR). Karmaşık iş mantığı için güçlü bir teknik.
İyi test yazmanın anahtarı: AAA pattern (Arrange-Act-Assert), her test bağımsız, her test tek bir şeyi doğrular ve test isimleri ne test edildiğini açıkça anlatır.
AI Asistan
Sorularını yanıtlamaya hazır