Bir gün production'da critical bir bug keşfettin. Kullanıcılar şikayet ediyor, App Store yorumları düşüyor, PM panik halinde. "Bu nasıl oldu?" diye soruyorsun kendine. Cevap basit: Test yazmadın.
Test-Driven Development (TDD), bu kabuslardan kurtulmanın en etkili yolu. Bu rehberde, TDD'yi sadece teorik değil, gerçek dünya senaryoları ile öğreneceksin. Hazır mısın?
İçindekiler
- TDD Nedir ve Neden Önemli?
- Red-Green-Refactor Döngüsü
- XCTest Framework Temelleri
- Test Doubles: Mock, Stub, Spy
- Dependency Injection ile Testable Code
- Async/Await Testing
- UI Testing Stratejileri
- Code Coverage ve Best Practices
- Gerçek Dünya Örneği: Login Flow
- Sonuç ve Öneriler
TDD Nedir ve Neden Önemli? {#tdd-nedir}
TDD, kodu yazmadan önce testleri yazma disiplinidir. "Önce test mi? Bu çılgınlık!" diyebilirsin. Ama aslında bu yaklaşım seni daha iyi bir developer yapıyor.
TDD'nin Faydaları
Fayda | Açıklama |
|---|---|
**Daha Az Bug** | Kod yazılmadan önce edge case'ler düşünülür |
**Refactoring Güveni** | Testler safety net görevi görür |
**Daha İyi Tasarım** | Testable kod = loosely coupled kod |
**Dokümantasyon** | Testler, kodun nasıl kullanılacağını gösterir |
**Hız** | Uzun vadede development hızı artar |
💡 Pro Tip: TDD öğrenirken sabırlı ol. İlk başta yavaşlayacaksın ama 3 ay sonra kodlama hızın ikiye katlanacak. Ben bunu yaşadım!
Red-Green-Refactor Döngüsü {#red-green-refactor}
TDD'nin kalbi üç adımdan oluşur:
🔴 RED → Başarısız test yaz
🟢 GREEN → Testi geçirecek minimum kodu yaz
🔵 REFACTOR → Kodu temizle (testler hâlâ geçmeli)
Pratik Örnek: Email Validator
Hadi gerçek bir örnek üzerinden gidelim.
ADIM 1: RED - Başarısız Test Yaz
swift
1import XCTest2@testable import MyApp3 4final class EmailValidatorTests: XCTestCase {5 6 var sut: EmailValidator! // System Under Test7 8 override func setUp() {9 super.setUp()10 sut = EmailValidator()11 }12 13 override func tearDown() {14 sut = nil15 super.tearDown()16 }17 18 func test_validate_emptyEmail_shouldReturnFalse() {19 let result = sut.validate("")20 XCTAssertFalse(result, "Boş email geçersiz olmalı")21 }22 23 func test_validate_emailWithoutAtSign_shouldReturnFalse() {24 let result = sut.validate("testexample.com")25 XCTAssertFalse(result)26 }27 28 func test_validate_validEmail_shouldReturnTrue() {29 let result = sut.validate("[email protected]")30 XCTAssertTrue(result)31 }32}ADIM 2: GREEN - Minimum Kod Yaz
swift
1struct EmailValidator {2 func validate(_ email: String) -> Bool {3 guard !email.isEmpty else { return false }4 guard email.contains("@") else { return false }5 6 let parts = email.split(separator: "@")7 guard parts.count == 2 else { return false }8 guard !parts[0].isEmpty && !parts[1].isEmpty else { return false }9 guard parts[1].contains(".") else { return false }10 11 return true12 }13}ADIM 3: REFACTOR - Kodu Temizle
swift
1struct EmailValidator {2 private static let emailRegex: NSRegularExpression? = {3 let pattern = "^[A-Z0-9a-z._%+-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,64}$"4 return try? NSRegularExpression(pattern: pattern, options: [])5 }()6 7 func validate(_ email: String) -> Bool {8 guard !email.isEmpty else { return false }9 let range = NSRange(email.startIndex..., in: email)10 return Self.emailRegex?.firstMatch(in: email, options: [], range: range) != nil11 }12}⚠️ Dikkat: Refactor sırasında yeni özellik ekleme! Sadece mevcut kodu temizle. Testler hâlâ geçmeli.
XCTest Framework Temelleri {#xctest-temelleri}
XCTest, Apple'ın resmi test framework'üdür. İşte bilmen gereken temel yapılar:
Test Lifecycle
swift
1final class MyTests: XCTestCase {2 3 override class func setUp() {4 super.setUp()5 // Her test class'ı için 1 kez çalışır6 }7 8 override func setUp() {9 super.setUp()10 // Her test method'undan ÖNCE çalışır11 }12 13 override func tearDown() {14 // Her test method'undan SONRA çalışır15 super.tearDown()16 }17 18 override class func tearDown() {19 // Her test class'ı için 1 kez çalışır (sonunda)20 super.tearDown()21 }22}Assertion'lar - Tam Liste
swift
1// Eşitlik kontrolleri2XCTAssertEqual(actual, expected)3XCTAssertNotEqual(actual, notExpected)4 5// Boolean kontrolleri 6XCTAssertTrue(condition)7XCTAssertFalse(condition)8 9// Nil kontrolleri10XCTAssertNil(optional)11XCTAssertNotNil(optional)12 13// Fırlatma kontrolleri14XCTAssertThrowsError(try riskyOperation())15XCTAssertNoThrow(try safeOperation())16 17// Performans testi18measure {19 // Ölçülecek kod20}21 22// Floating point karşılaştırma23XCTAssertEqual(3.14159, .pi, accuracy: 0.001)Test Doubles: Mock, Stub, Spy {#test-doubles}
Gerçek dependency'leri test etmek istemezsin. İşte burada Test Doubles devreye girer:
Tür | Amaç | Kullanım |
|---|---|---|
**Stub** | Sabit değer döndürür | "Bu API çağrısı şu JSON'u dönsün" |
**Mock** | Davranış doğrular | "Bu method 2 kez çağrıldı mı?" |
**Spy** | Gerçek + kayıt | "Gerçek method'u çağır ama logla" |
**Fake** | Basitleştirilmiş impl. | In-memory database |
**Dummy** | Placeholder | Kullanılmayan parametre |
Protocol-Based Dependency Injection
swift
1protocol NetworkServiceProtocol {2 func fetch<T: Decodable>(from url: URL) async throws -> T3}4 5protocol UserRepositoryProtocol {6 func getUser(id: String) async throws -> User7 func saveUser(_ user: User) async throws8}9 10protocol AnalyticsServiceProtocol {11 func track(event: String, parameters: [String: Any])12}Mock Implementation
swift
1final class MockNetworkService: NetworkServiceProtocol {2 var stubbedResult: Any?3 var stubbedError: Error?4 var fetchCallCount = 05 var lastEndpoint: String?6 7 func fetch<T: Decodable>(from url: URL) async throws -> T {8 fetchCallCount += 19 lastEndpoint = url.absoluteString10 11 if let error = stubbedError {12 throw error13 }14 15 guard let result = stubbedResult as? T else {16 throw NetworkError.decodingError17 }18 19 return result20 }21}Spy Implementation
swift
1final class SpyAnalyticsService: AnalyticsServiceProtocol {2 private(set) var trackedEvents: [(event: String, parameters: [String: Any])] = []3 4 func track(event: String, parameters: [String: Any]) {5 trackedEvents.append((event, parameters))6 }7 8 func hasTracked(event: String) -> Bool {9 trackedEvents.contains { $0.event == event }10 }11 12 func eventCount(for event: String) -> Int {13 trackedEvents.filter { $0.event == event }.count14 }15}Dependency Injection ile Testable Code {#dependency-injection}
DI olmadan test yazmak işkence. İşte doğru yaklaşım:
❌ YANLIŞ: Hard-coded dependency
swift
1class UserViewModel {2 func loadUser() async {3 let user = try? await NetworkService.shared.fetch(from: userURL)4 // ...5 }6}✅ DOĞRU: Injected dependency
swift
1class UserViewModel {2 private let networkService: NetworkServiceProtocol3 private let analytics: AnalyticsServiceProtocol4 5 init(6 networkService: NetworkServiceProtocol = NetworkService.shared,7 analytics: AnalyticsServiceProtocol = AnalyticsService.shared8 ) {9 self.networkService = networkService10 self.analytics = analytics11 }12 13 func loadUser(id: String) async throws -> User {14 analytics.track(event: "user_load_started", parameters: ["id": id])15 let user: User = try await networkService.fetch(from: makeUserURL(id: id))16 analytics.track(event: "user_load_success", parameters: ["id": id])17 return user18 }19}ViewModel Test Örneği
swift
1final class UserViewModelTests: XCTestCase {2 3 var sut: UserViewModel!4 var mockNetwork: MockNetworkService!5 var spyAnalytics: SpyAnalyticsService!6 7 override func setUp() {8 super.setUp()9 mockNetwork = MockNetworkService()10 spyAnalytics = SpyAnalyticsService()11 sut = UserViewModel(networkService: mockNetwork, analytics: spyAnalytics)12 }13 14 func test_loadUser_success_shouldTrackEvents() async throws {15 // Given16 let expectedUser = User(id: "123", name: "John", email: "[email protected]")17 mockNetwork.stubbedResult = expectedUser18 19 // When20 let user = try await sut.loadUser(id: "123")21 22 // Then23 XCTAssertEqual(user.name, "John")24 XCTAssertEqual(spyAnalytics.eventCount(for: "user_load_started"), 1)25 XCTAssertEqual(spyAnalytics.eventCount(for: "user_load_success"), 1)26 }27}Async/Await Testing {#async-testing}
Swift'in modern concurrency özellikleri ile async test yazmak çok kolay:
swift
1final class AsyncTests: XCTestCase {2 3 func test_fetchProducts_shouldReturnNonEmptyList() async throws {4 // Given5 let service = ProductService()6 7 // When8 let products = try await service.fetchProducts()9 10 // Then11 XCTAssertFalse(products.isEmpty)12 }13 14 func test_slowOperation_shouldCompleteWithinTimeout() async throws {15 let expectation = XCTestExpectation(description: "Slow operation")16 17 Task {18 try await Task.sleep(nanoseconds: 500_000_000)19 expectation.fulfill()20 }21 22 await fulfillment(of: [expectation], timeout: 2.0)23 }24}UI Testing Stratejileri {#ui-testing}
UI testleri, kullanıcı senaryolarını otomatize eder:
swift
1import XCTest2 3final class LoginUITests: XCTestCase {4 5 var app: XCUIApplication!6 7 override func setUp() {8 super.setUp()9 continueAfterFailure = false10 11 app = XCUIApplication()12 app.launchArguments = ["--uitesting", "--reset-state"]13 app.launch()14 }15 16 func test_login_withValidCredentials_shouldShowHomeScreen() {17 // Given18 let emailField = app.textFields["email_field"]19 let passwordField = app.secureTextFields["password_field"]20 let loginButton = app.buttons["login_button"]21 22 // When23 emailField.tap()24 emailField.typeText("[email protected]")25 26 passwordField.tap()27 passwordField.typeText("Password123!")28 29 loginButton.tap()30 31 // Then32 let homeTitle = app.staticTexts["home_title"]33 XCTAssertTrue(homeTitle.waitForExistence(timeout: 5))34 }35}🎯 Best Practice: UI testlerinde accessibility identifier kullan, text'e bağlı kalma!
Code Coverage ve Best Practices {#code-coverage}
Coverage Hedefleri
Katman | Minimum | Önerilen |
|---|---|---|
**Business Logic** | %90 | %95+ |
**ViewModels** | %85 | %90+ |
**Networking** | %80 | %85+ |
**UI** | %60 | %75+ |
Naming Convention
Format: test_[method]_[condition]_[expected]
swift
1func test_validate_emptyEmail_shouldReturnFalse() { }2func test_calculate_negativeNumbers_shouldThrowError() { }3func test_fetch_networkError_shouldRetryThreeTimes() { }Gerçek Dünya Örneği: Login Flow {#gercek-dunya-ornegi}
Tüm öğrendiklerimizi bir araya getirelim:
swift
1// Domain Models2struct LoginCredentials {3 let email: String4 let password: String5}6 7struct AuthToken {8 let accessToken: String9 let refreshToken: String10 let expiresAt: Date11}12 13enum AuthError: Error, Equatable {14 case emptyEmail15 case emptyPassword16 case invalidEmail17 case weakPassword18 case invalidCredentials19 case networkError20}swift
1// Login Use Case2final class LoginUseCase {3 private let authRepository: AuthRepositoryProtocol4 private let validator: CredentialsValidatorProtocol5 private let tokenStore: TokenStoreProtocol6 7 init(8 authRepository: AuthRepositoryProtocol,9 validator: CredentialsValidatorProtocol,10 tokenStore: TokenStoreProtocol11 ) {12 self.authRepository = authRepository13 self.validator = validator14 self.tokenStore = tokenStore15 }16 17 func execute(credentials: LoginCredentials) async throws -> AuthToken {18 try validator.validate(credentials)19 let token = try await authRepository.login(20 email: credentials.email,21 password: credentials.password22 )23 try await tokenStore.save(token)24 return token25 }26}Easter Egg
Gizli bir bilgi buldun!
Bu bölümde gizli bir bilgi var. Keşfetmek ister misin?
Okuyucu Ödülü
Tebrikler! Bu yazıyı sonuna kadar okuduğun için sana özel bir hediyem var:
ALTIN İPUCU
Bu yazının en değerli bilgisi
Bu ipucu, yazının en önemli çıkarımını içeriyor.
Sonuç ve Öneriler {#sonuc-ve-oneriler}
TDD öğrenmek bir yolculuk. İşte yol haritası:
- Başla - Küçük bir class ile başla (Validator, Calculator)
- Pratik yap - Her gün 1 saat TDD kata
- Sabırlı ol - İlk ay yavaşlayacaksın, normal
- Takımına anlat - Öğretmek = öğrenmek
Kaynaklar
- Apple Developer:: [Testing Your Apps in Xcode](https://developer.apple.com/documentation/xcode/testing-your-apps-in-xcode)
- Swift.org:: [XCTest Documentation](https://swift.org/documentation/)
- WWDC 2023:: [Testing in Xcode](https://developer.apple.com/videos/play/wwdc2023/10175/)
*Bu yazıyı faydalı bulduysan, bir iOS developer arkadaşınla paylaş! Testler yaygınlaşsın, bug'lar yok olsun.* 🧪

