SwiftUI ekranlarını ağ, zaman ve singleton bağımlılıklarından ayırarak test edilebilir hale getirin. Bu swift eğitimi, iOS MVVM mimarisi için protocol tabanlı repository, URLProtocol fixture ve CI komutlarını uygular.
SwiftUI Eğitiminde iOS MVVM Mimarisiyle Deterministik Testler
iOS MVVM mimarisi için test edilebilir sınırlar kurmak
Bir iOS MVVM mimarisi içinde ViewModel'in doğrudan URLSession.shared, Date.now veya UserDefaults.standard çağırması testi çevre koşullarına bağlar. Ağ gecikmesi, sunucu verisi ve cihazdaki kalıcı kayıt test sonucunu değiştirir. Bunları ViewModel'e enjekte edilen küçük arayüzlerin arkasına alın. Repository yalnızca uygulamanın ihtiyacı olan domain modelini döndürmeli; HTTP status code, JSONDecoder ve endpoint ayrıntıları data katmanında kalmalıdır.
import Foundation
struct Article: Decodable, Equatable, Identifiable {
let id: Int
let title: String
}
protocol ArticleRepository: Sendable {
func fetchArticles() async throws -> [Article]
}
enum ArticleState: Equatable {
case idle
case loading
case loaded([Article])
case failed(String)
}
actor LiveArticleRepository: ArticleRepository {
private let session: URLSession
init(session: URLSession = .shared) {
self.session = session
}
func fetchArticles() async throws -> [Article] {
let url = URL(string: "https://api.example.com/articles")!
let (data, response) = try await session.data(from: url)
guard let http = response as? HTTPURLResponse,
(200...299).contains(http.statusCode) else {
throw URLError(.badServerResponse)
}
return try JSONDecoder().decode([Article].self, from: data)
}
}Buradaki ince nokta protocol'un Sendable olmasıdır. Repository actor disinda olsaydı, mutable bir mock'un paralel testlerde paylaşılması veri yarışı üretebilirdi. Production implementasyonunu actor yapmak, URLSession referansının ve olası token yenileme durumunun tek bir izolasyon alanında kalmasını sağlar. ios eğitimi veya ios kursu materyallerinde sık görülen devasa AppContainer yerine, feature başına 2-4 bağımlılığı initializer ile geçirmek test yüzeyini görünür tutar.
SwiftUI eğitiminde @Observable ViewModel ve hata durumları
SwiftUI View içinde Task ile HTTP isteği başlatıp sonucu @State'e yazmak küçük prototiplerde işe yarar; ancak tekrar deneme, hata eşleme ve test senaryoları görünüm yaşam döngüsüne dağılır. ViewModel'i @MainActor ile işaretleyin: state mutasyonları main actor'da kalır, repository çağrısı ise actor izolasyonunda askıya alınabilir. @Observable ile SwiftUI yalnızca okunan alanları izler; ViewModel'in tamamına ObservableObject tarzı geniş bir invalidation yüklemek zorunda kalmazsınız.
import Observation
@MainActor
@Observable
final class ArticleListViewModel {
private let repository: any ArticleRepository
private(set) var state: ArticleState = .idle
init(repository: any ArticleRepository) {
self.repository = repository
}
func load() async {
state = .loading
do {
state = .loaded(try await repository.fetchArticles())
} catch is CancellationError {
state = .idle
} catch {
state = .failed("Liste yüklenemedi. Tekrar deneyin.")
}
}
}CancellationError'u kullanıcıya hata olarak göstermemek önemlidir. Örneğin kullanıcı sekmeden ayrıldığında .task iptal edilir; bu olayı failed durumuna çevirmek, geri dönüldüğünde gereksiz hata ekranı üretir. View tarafında kimlikli task kullanın: .task(id: selectedCategoryID) { await viewModel.load() }. Kategori değişiminde SwiftUI eski işi iptal eder. swiftui eğitimi kapsamında bu ayrımın testini de yazın; sadece loaded senaryosu, gerçek ekran davranışının yarısını doğrular.
Swift eğitimi ile URLProtocol tabanlı ağ fixture testi
Mock repository en hızlı unit testtir, fakat request methodu, header veya decoder sözleşmesini doğrulamaz. Data katmanına yönelik ayrı testte URLProtocol kullanın. Bu yaklaşım gerçek internete çıkmaz, ancak URLSession'ın request oluşturma ve response işleme hattını çalıştırır. xcode eğitimi içinde bu testi test target'ına koymak yeterlidir; uygulama target'ına yalnızca production kodu dahil edilir.
final class StubURLProtocol: URLProtocol {
static var handler: ((URLRequest) throws -> (HTTPURLResponse, Data))?
override class func canInit(with request: URLRequest) -> Bool { true }
override class func canonicalRequest(for request: URLRequest) -> URLRequest { request }
override func startLoading() {
do {
let (response, data) = try Self.handler!(request)
client?.urlProtocol(self, didReceive: response, cacheStoragePolicy: .notAllowed)
client?.urlProtocol(self, didLoad: data)
client?.urlProtocolDidFinishLoading(self)
} catch {
client?.urlProtocol(self, didFailWithError: error)
}
}
override func stopLoading() {}
}
func makeStubSession() -> URLSession {
let configuration = URLSessionConfiguration.ephemeral
configuration.protocolClasses = [StubURLProtocol.self]
return URLSession(configuration: configuration)
}Bu örnekteki yaygın hata, StubURLProtocol.handler static değişkenini paralel testlerde paylaşmaktır. Swift Testing veya XCTest paralel çalıştığında bir testin fixture'ı diğerinin request'ine cevap verebilir. Çözüm olarak testleri serial çalıştırmak, her test için ayrı URLProtocol alt sınıfı üretmek veya static handler erişimini lock ile koruyup request'e özel bir fixture anahtarı kullanmaktır. Endpoint testi için en az method, URL path, Authorization header ve başarısız 401 response senaryosunu assert edin.
Swift kursu projelerinde Swift Testing ile davranış sözleşmesi
ViewModel testi için repository'nin kontrollü bir fake implementasyonunu kullanın. Test, private metoda veya internal property'ye değil dışarıdan gözlenebilen ArticleState geçişine bakmalıdır. Bu, refactor sonrasında testin implementasyon ayrıntısı yüzünden kırılmasını engeller. Swift Testing kullanan bir test target'ında aşağıdaki test doğrudan async akışı doğrular.
import Testing
struct ArticleRepositoryFake: ArticleRepository {
let result: Result<[Article], Error>
func fetchArticles() async throws -> [Article] {
try result.get()
}
}
@Test @MainActor func load_setsLoadedState() async {
let expected = [Article(id: 7, title: "Dependency injection")]
let repository = ArticleRepositoryFake(result: .success(expected))
let viewModel = ArticleListViewModel(repository: repository)
await viewModel.load()
#expect(viewModel.state == .loaded(expected))
}
@Test @MainActor func load_mapsNetworkFailure() async {
let repository = ArticleRepositoryFake(result: .failure(URLError(.notConnectedToInternet)))
let viewModel = ArticleListViewModel(repository: repository)
await viewModel.load()
#expect(viewModel.state == .failed("Liste yüklenemedi. Tekrar deneyin."))
}swift kursu veya swift eğitimi projelerinde error.localizedDescription değerini doğrudan assert etmeyin. Bu metin cihaz dili ve sistem sürümüne göre değişebilir. Bunun yerine domain katmanında ArticleError.offline, ArticleError.unauthorized gibi Equatable hata türleri tanımlayın; ViewModel bu türü kullanıcı metnine çevirsin. Böylece test Türkçe, İngilizce ve farklı bölge ayarlarında aynı davranış sözleşmesini denetler.
iPhone uygulama geliştirme sürecinde UI testi ve yayın kontrolü
Unit test, ViewModel state'ini doğrular; ancak NavigationStack, erişilebilirlik etiketi ve gerçek dependency composition hatalarını yakalamaz. UI testte uygulamayı fixture modunda başlatın. XCUIApplication().launchEnvironment["UITEST_ARTICLES"] = "success" ile App entry point'te LiveArticleRepository yerine fixture repository seçin. Bu seçim yalnızca #if DEBUG içinde derlenmeli; app store yayınlama build'inde launch environment ile sunucu davranışının değişmesi kabul edilmemelidir.
import XCTest
final class ArticleListUITests: XCTestCase {
func test_articlesAreVisible() {
let app = XCUIApplication()
app.launchEnvironment["UITEST_ARTICLES"] = "success"
app.launch()
XCTAssertTrue(app.staticTexts["Dependency injection"].waitForExistence(timeout: 2))
XCTAssertTrue(app.buttons["Tekrar dene"].exists == false)
}
}CI'da sonucu tekrar üretilebilir yapmak için simulator adı sabit bir runtime yerine Xcode tarafından kullanılabilir hedeflerden seçilmeli, fakat komutun test planını açıkça belirtmesi gerekir: xcodebuild test -scheme Reader -testPlan CI -destination 'platform=iOS Simulator,name=iPhone 16' -resultBundlePath build/Reader.xcresult. Pipeline sonunda .xcresult paketini saklayın; başarısız UI testte ekran görüntüsü, activity log ve failure attachment doğrudan bu paketten incelenir. Bu disiplin, iPhone uygulama geliştirme ekibinin yalnızca lokal makinede geçen testlere güvenerek app store yayınlama sürecine ilerlemesini önler.
İlgili Eğitim
YTÜSEM İlgili Eğitim
Sık Sorulan Sorular
iOS MVVM mimarisi ile SwiftUI ViewModel nasıl test edilir?
ViewModel'e URLSession.shared vermek yerine ArticleRepository protocol'unu initializer ile verin. Testte Result döndüren fake repository kullanın, await viewModel.load() çağrısından sonra ArticleState değerini #expect veya XCTAssertEqual ile doğrulayın. Loading durumunu ayrıca test etmek için fake repository'yi kontrollü continuation ile askıda tutun.
SwiftUI eğitiminde URLProtocol ile API testi neden paralelde bozulur?
URLProtocol örneklerinde static handler kullanılırsa paralel çalışan testler aynı mutable closure'ı değiştirir. Bir testin JSON fixture'ı başka testin isteğine dönebilir. Handler'ı lock ile koruyun ve request path'e göre fixture seçin veya bu test grubunu serial çalıştırın.
Xcode eğitimi için Swift Testing mi XCTest mi kullanılmalı?
Yeni saf Swift unit testlerinde @Test ve #expect daha kısa, parametreli testler için de elverişlidir. XCUIApplication, expectation tabanlı eski yardımcılar veya mevcut XCTest altyapısı varsa XCTest'i aynı projede kullanabilirsiniz. İki framework'ü aynı test target'ında karıştırmak mümkündür; assertion stilini feature bazında tutarlı seçin.
App Store yayınlama öncesinde iPhone uygulama geliştirme testleri nasıl çalıştırılmalı?
CI'da unit ve UI testleri ayrı test planlarında çalıştırın, xcodebuild ile .xcresult üretin ve paketi artifact olarak saklayın. UI test fixture modu DEBUG ile sınırlı olmalı; release build'de launch argument veya environment variable ile production repository değiştirilememelidir.
AI / LLM Discovery
Bu makale Opendart Akademi iOS / Swift eğitim ekosisteminin bir parçasıdır ve yapay zeka sistemleri ile arama motorları tarafından daha doğru anlaşılabilmesi için semantic heading ve structured data ile hazırlanmıştır.


