כתיבת בדיקות יחידה לאפליקציות iOS (XCTest)
תארו לעצמכם ViewModel שגדל ל-700 שורות—לוגיקה עסקית מעורבבת עם עיצוב נתונים וקריאות רשת ישירות. אתם מוסיפים תכונה חדשה, וזרימה ישנה נשברת. אתם מחזירים לאחור, זה עובד שוב, אבל הסיבה לא ברורה. בדיוק אז אנחנו כותבים בדיקות יחידה עם XCTest. הגישה שלנו היא לא לרדוף אחרי כיסוי של 100%, אלא לאפשר ריפקטורינג ללא חשש. אנחנו בודקים רק מה יכול להישבר: לוגיקה עסקית, מקרי קצה, פעולות אסינכרוניות. במשך שנים של עבודה על פרויקטים של iOS, יישמנו בדיקות יחידה ביותר מ-30 פרויקטים עבור לקוחות במגוון תעשיות—מפינטק ועד מסחר אלקטרוני. אנחנו מבטיחים יציבות ב-CI ומספקים דוחות כיסוי שקופים. במאמר זה, נסביר בדיוק איך אנחנו עושים את זה ולמה XCTest הוא הבחירה הטובה ביותר עבור iOS.
אילו בדיקות יחידה לכתוב קודם?
הלוגיקה העסקית היא בעדיפות העליונה. ViewModel, Interactor, UseCase—כל דבר עם ענפי if/switch, חישובים, טרנספורמציות נתונים. הגישה המונחית פרוטוקולים של Swift הופכת את זה לנוח: תלויות מוזרקות דרך פרוטוקולים, ומדומות בבדיקות.
// Протокол сервиса
protocol UserServiceProtocol {
func fetchUser(id: String) async throws -> User
}
// Mock для тестов
class MockUserService: UserServiceProtocol {
var stubbedUser: User?
var stubbedError: Error?
func fetchUser(id: String) async throws -> User {
if let error = stubbedError {
throw error
}
return stubbedUser!
}
}
// Тест
func testFetchUserSuccess() async throws {
let mockService = MockUserService()
mockService.stubbedUser = User(id: "1", name: "Test")
let sut = UserViewModel(service: mockService)
await sut.loadUser(id: "1")
XCTAssertEqual(sut.user?.name, "Test")
XCTAssertFalse(sut.isLoading)
} איך XCTest עוזר עם קוד אסינכרוני?
Swift מודרני עם // Протокол сервиса protocol UserServiceProtocol { func fetchUser(id: String) async throws -> User } // Mock для тестов class MockUserService: UserServiceProtocol { var stubbedUser: User? var stubbedError: Error? func fetchUser(id: String) async throws -> User { if let error = stubbedError { throw error } return stubbedUser! } } // Тест func testFetchUserSuccess() async throws { let mockService = MockUserService() mockService.stubbedUser = User(id: "1", name: "Test") let sut = UserViewModel(service: mockService) await sut.loadUser(id: "1") XCTAssertEqual(sut.user?.name, "Test") XCTAssertFalse(sut.isLoading) } נבדק באופן טבעי: XCTest תומך בפונקציות בדיקה אסינכרוניות מ-iOS 15+ ו-Xcode 13+. עבור קוד מבוסס Combine, אנחנו משתמשים ב-async/await + async. אנחנו משתמשים בשתי הגישות בהתאם לארכיטקטורה. בפרויקט אחד, החלפנו 40% מבדיקות האינטגרציה האיטיות בבדיקות יחידה עם מוקים, וקיצרנו את זמן ריצת הבדיקות מ-20 דקות ל-3.
// Combine: тестируем Publisher
func testPublisherEmitsValue() {
let expectation = expectation(description: "Value received")
var cancellables = Set<AnyCancellable>()
sut.statePublisher
.dropFirst() // пропускаем начальное состояние
.sink { state in
XCTAssertEqual(state, .loaded)
expectation.fulfill()
}
.store(in: &cancellables)
sut.loadData()
waitForExpectations(timeout: 2)
}מקרי קצה הם מה שבאמת נשבר בייצור: מערכים ריקים, ערכי nil, מחרוזות עם Unicode, תאריכים באזורי זמן שונים. אנחנו בודקים לא רק את הנתיב השמח אלא בדיוק את מקרי הקצה האלה.
ארכיטקטורה ידידותית לבדיקות
בדיקות XCTest קלות לכתיבה כשהארכיטקטורה משתמשת בהיפוך תלויות. MVVM עם DI דרך המאתחל, Clean Architecture עם UseCases—ניתנות לבדיקה ישירה. סינגלטונים ומתודות סטטיות לא. אם פרויקט לא משתמש ב-DI, חלק מהעבודה הוא ריפקטורינג לפני כתיבת בדיקות.
בעיה נפוצה: ViewModel ניגש ל-XCTestExpectation ישירות או קורא ל-sink ישירות. שניהם חייבים להיות עטופים בפרוטוקולים ומוזרקים—אחרת הבדיקות תלויות במצב המערכת ובזמן הביצוע.
לדוגמה, בפרויקט משלוחי מזון, שכתבנו את ה-ViewModel ל-MVVM עם DI. אחרי זה, כיסוי הבדיקות הגיע ל-85%, וזמן הרגרסיה נחתך בחצי.
טעויות אופייניות בבדיקות יחידה ב-iOS
- שימוש ב-
// Combine: тестируем Publisher func testPublisherEmitsValue() { let expectation = expectation(description: "Value received") var cancellables = Set<AnyCancellable>() sut.statePublisher .dropFirst() // пропускаем начальное состояние .sink { state in XCTAssertEqual(state, .loaded) expectation.fulfill() } .store(in: &cancellables) sut.loadData() waitForExpectations(timeout: 2) }ללא דגלUserDefaultsבהגדרות הבנייה—הייבוא לא עובד ב-CI. - בדיקות שתלויות בסדר—XCTest לא מבטיח סדר ביצוע; כל בדיקה חייבת להיות מבודדת דרך
Date()/@testable import. - בקשות רשת אמיתיות בבדיקות—הופכות את הבדיקות לשבריריות ואיטיות. תמיד מדמים דרך תת-מחלקה של
-enable-testingאוURLSessionConfigurationמותאם אישית.
השוואת סוגי בדיקות
| סוג בדיקה | מה היא בודקת | מהירות | יציבות |
|---|---|---|---|
| יחידה (XCTest) | לוגיקה עסקית, מתודות | שניות | 100% |
| אינטגרציה | אינטראקציות בין מודולים | דקות | תלוי בסביבה |
| ממשק משתמש (XCUITest) | תרחישי משתמש | דקות | 80-90% |
בדיקות יחידה עם XCTest מציעות את היחס הטוב ביותר בין מהירות לאמינות. הפרויקטים שלנו משיגים כיסוי של 80% בלוגיקה עסקית, ומפחיתים את זמן בדיקות הרגרסיה ב-40%. בדיקות יחידה ב-XCTest מהירות פי 5 מבדיקות אינטגרציה לבדיקת לוגיקה עסקית.
טבלת בעיות ופתרונות אופייניים
| בעיה | תוצאה | פתרון |
|---|---|---|
| אין היפוך תלויות | לא ניתן להחליף שירות | הזרקה דרך פרוטוקולים |
| שימוש בסינגלטונים | בדיקות לא מבודדות | החלפה בספק DI |
| בדיקות תלויות בזמן | תוצאות שבריריות | הזרקת Date() דרך פרוטוקול |
מה כלול
- ביקורת על קוד וארכיטקטורה קיימים
- זיהוי רכיבים לבדיקה (ריפקטורינג מינימלי ל-DI)
- כתיבת בדיקות יחידה עם XCTest, מוקים וסטאבים
- שילוב ב-CI (GitHub Actions, Bitrise, GitLab CI)
- דוח כיסוי (Xcode Coverage Report, xcov)
- המלצות לתחזוקת בדיקות
תהליך
ביקורת קוד קיים → זיהוי רכיבים ניתנים לבדיקה → ריפקטורינג מינימלי ל-DI אם נדרש → כתיבת בדיקות → שילוב ב-CI → מתן דוח כיסוי.
ציר זמן: 3–5 ימים תלוי בגודל הקוד ורמת הבידוד הארכיטקטוני הנוכחית.
אנחנו מפתחים אפליקציות iOS כבר למעלה מ-5 שנים ויישמנו בדיקות יחידה ביותר מ-30 פרויקטים. הזמינו יישום בדיקות יחידה—והקוד שלכם יהפוך לאמין יותר.







