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, 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ü
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, 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
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
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
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 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
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
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
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.* 🧪

