Tüm Yazılar
KategoriTesting
Okuma Süresi
22 dk
Yayın Tarihi
...
Kelime Sayısı
1.788kelime

Kahveni hazırla - bu içerikli bir makale!

Red-Green-Refactor döngüsü, XCTest framework'ü, mock/stub/spy pattern'leri, async testing ve UI testleri ile profesyonel iOS test stratejileri.

iOS'ta Test-Driven Development (TDD): Eksiksiz Rehber

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

  1. TDD Nedir ve Neden Önemli?
  2. Red-Green-Refactor Döngüsü
  3. XCTest Framework Temelleri
  4. Test Doubles: Mock, Stub, Spy
  5. Dependency Injection ile Testable Code
  6. Async/Await Testing
  7. UI Testing Stratejileri
  8. Code Coverage ve Best Practices
  9. Gerçek Dünya Örneği: Login Flow
  10. 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 XCTest
2@testable import MyApp
3 
4final class EmailValidatorTests: XCTestCase {
5
6 var sut: EmailValidator! // System Under Test
7
8 override func setUp() {
9 super.setUp()
10 sut = EmailValidator()
11 }
12
13 override func tearDown() {
14 sut = nil
15 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 true
12 }
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) != nil
11 }
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ışır
6 }
7
8 override func setUp() {
9 super.setUp()
10 // Her test method'undan ÖNCE çalışır
11 }
12
13 override func tearDown() {
14 // Her test method'undan SONRA çalışır
15 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 kontrolleri
2XCTAssertEqual(actual, expected)
3XCTAssertNotEqual(actual, notExpected)
4 
5// Boolean kontrolleri
6XCTAssertTrue(condition)
7XCTAssertFalse(condition)
8 
9// Nil kontrolleri
10XCTAssertNil(optional)
11XCTAssertNotNil(optional)
12 
13// Fırlatma kontrolleri
14XCTAssertThrowsError(try riskyOperation())
15XCTAssertNoThrow(try safeOperation())
16 
17// Performans testi
18measure {
19 // Ölçülecek kod
20}
21 
22// Floating point karşılaştırma
23XCTAssertEqual(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 -> T
3}
4 
5protocol UserRepositoryProtocol {
6 func getUser(id: String) async throws -> User
7 func saveUser(_ user: User) async throws
8}
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 = 0
5 var lastEndpoint: String?
6
7 func fetch<T: Decodable>(from url: URL) async throws -> T {
8 fetchCallCount += 1
9 lastEndpoint = url.absoluteString
10
11 if let error = stubbedError {
12 throw error
13 }
14
15 guard let result = stubbedResult as? T else {
16 throw NetworkError.decodingError
17 }
18
19 return result
20 }
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 }.count
14 }
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: NetworkServiceProtocol
3 private let analytics: AnalyticsServiceProtocol
4
5 init(
6 networkService: NetworkServiceProtocol = NetworkService.shared,
7 analytics: AnalyticsServiceProtocol = AnalyticsService.shared
8 ) {
9 self.networkService = networkService
10 self.analytics = analytics
11 }
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 user
18 }
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 // Given
16 let expectedUser = User(id: "123", name: "John", email: "[email protected]")
17 mockNetwork.stubbedResult = expectedUser
18
19 // When
20 let user = try await sut.loadUser(id: "123")
21
22 // Then
23 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 // Given
5 let service = ProductService()
6
7 // When
8 let products = try await service.fetchProducts()
9
10 // Then
11 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 XCTest
2 
3final class LoginUITests: XCTestCase {
4
5 var app: XCUIApplication!
6
7 override func setUp() {
8 super.setUp()
9 continueAfterFailure = false
10
11 app = XCUIApplication()
12 app.launchArguments = ["--uitesting", "--reset-state"]
13 app.launch()
14 }
15
16 func test_login_withValidCredentials_shouldShowHomeScreen() {
17 // Given
18 let emailField = app.textFields["email_field"]
19 let passwordField = app.secureTextFields["password_field"]
20 let loginButton = app.buttons["login_button"]
21
22 // When
23 emailField.tap()
24 emailField.typeText("[email protected]")
25
26 passwordField.tap()
27 passwordField.typeText("Password123!")
28
29 loginButton.tap()
30
31 // Then
32 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 Models
2struct LoginCredentials {
3 let email: String
4 let password: String
5}
6 
7struct AuthToken {
8 let accessToken: String
9 let refreshToken: String
10 let expiresAt: Date
11}
12 
13enum AuthError: Error, Equatable {
14 case emptyEmail
15 case emptyPassword
16 case invalidEmail
17 case weakPassword
18 case invalidCredentials
19 case networkError
20}
swift
1// Login Use Case
2final class LoginUseCase {
3 private let authRepository: AuthRepositoryProtocol
4 private let validator: CredentialsValidatorProtocol
5 private let tokenStore: TokenStoreProtocol
6
7 init(
8 authRepository: AuthRepositoryProtocol,
9 validator: CredentialsValidatorProtocol,
10 tokenStore: TokenStoreProtocol
11 ) {
12 self.authRepository = authRepository
13 self.validator = validator
14 self.tokenStore = tokenStore
15 }
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.password
22 )
23 try await tokenStore.save(token)
24 return token
25 }
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ı:

  1. Başla - Küçük bir class ile başla (Validator, Calculator)
  2. Pratik yap - Her gün 1 saat TDD kata
  3. Sabırlı ol - İlk ay yavaşlayacaksın, normal
  4. Takımına anlat - Öğretmek = öğrenmek

Kaynaklar


*Bu yazıyı faydalı bulduysan, bir iOS developer arkadaşınla paylaş! Testler yaygınlaşsın, bug'lar yok olsun.* 🧪

Etiketler

#Testing#TDD#XCTest#iOS#Unit Testing#Swift
Muhittin Çamdalı

Muhittin Çamdalı

Senior iOS Developer

12+ yıllık deneyime sahip iOS Developer. Swift, SwiftUI ve modern iOS mimarileri konusunda uzman. Apple platformlarında performanslı ve kullanıcı dostu uygulamalar geliştiriyorum.

iOS Geliştirme Haberleri

Haftalık Swift tips, SwiftUI tricks ve iOS best practices. Spam yok, sadece değerli içerik.

Gizliliğinize saygı duyuyoruz. İstediğiniz zaman abonelikten çıkabilirsiniz.

Paylaş

Bunu da begenebilirsiniz