iOS Unit Testing Expert Expert in iOS testing with XCTest framework and best practices. Core Testing Principles Test Structure and Organization Follow the Arrange-Act-Assert (AAA) pattern Use descriptive test method names explaining scenario and expected outcome Group related tests using nested test classes or test suites Maintain test independence - each test should run in isolation XCTest Framework Fundamentals import XCTest @testable import YourApp class UserServiceTests : XCTestCase { // System Under Test var sut : UserService ! var mockNetworkManager : MockNetworkManager ! override func setUpWithError ( ) throws { try super . setUpWithError ( ) mockNetworkManager = MockNetworkManager ( ) sut = UserService ( networkManager : mockNetworkManager ) } override func tearDownWithError ( ) throws { sut = nil mockNetworkManager = nil try super . tearDownWithError ( ) } // MARK: - fetchUser Tests func test_fetchUser_withValidId_returnsUser ( ) async throws { // Arrange let expectedUser = User ( id : "123" , name : "John Doe" ) mockNetworkManager . fetchUserResult = . success ( expectedUser ) // Act let result = try await sut . fetchUser ( id : "123" ) // Assert XCTAssertEqual ( result . id , expectedUser . id ) XCTAssertEqual ( result . name , expectedUser . name ) XCTAssertEqual ( mockNetworkManager . fetchUserCallCount , 1 ) XCTAssertEqual ( mockNetworkManager . lastFetchedUserId , "123" ) } func test_fetchUser_withInvalidId_throwsError ( ) async { // Arrange mockNetworkManager . fetchUserResult = . failure ( NetworkError . notFound ) // Act & Assert do { _ = try await sut . fetchUser ( id : "invalid" ) XCTFail ( "Expected error to be thrown" ) } catch { XCTAssertTrue ( error is NetworkError ) XCTAssertEqual ( error as ? NetworkError , . notFound ) } } } Mocking and Dependency Injection Protocol-Based Mocking // Protocol definition protocol NetworkManagerProtocol { func fetchUser ( id : String ) async throws -> User func saveUser ( _ user : User ) async throws } // Mock implementation class MockNetworkManager : NetworkManagerProtocol { // Call tracking var fetchUserCallCount = 0 var lastFetchedUserId : String ? var saveUserCallCount = 0 var lastSavedUser : User ? // Configurable results var fetchUserResult : Result < User , Error
? var saveUserResult : Result < Void , Error
= . success ( ( ) ) func fetchUser ( id : String ) async throws -> User { fetchUserCallCount += 1 lastFetchedUserId = id switch fetchUserResult { case . success ( let user ) : return user case . failure ( let error ) : throw error case . none : throw TestError . noMockResult } } func saveUser ( _ user : User ) async throws { saveUserCallCount += 1 lastSavedUser = user switch saveUserResult { case . success : return case . failure ( let error ) : throw error } } // Reset for reuse func reset ( ) { fetchUserCallCount = 0 lastFetchedUserId = nil saveUserCallCount = 0 lastSavedUser = nil fetchUserResult = nil saveUserResult = . success ( ( ) ) } } enum TestError : Error { case noMockResult } Spy Pattern class NetworkManagerSpy : NetworkManagerProtocol { private ( set ) var messages : [ Message ] = [ ] enum Message : Equatable { case fetchUser ( id : String ) case saveUser ( User ) } var stubbedFetchUserResult : Result < User , Error
= . failure ( TestError . noMockResult ) func fetchUser ( id : String ) async throws -> User { messages . append ( . fetchUser ( id : id ) ) return try stubbedFetchUserResult . get ( ) } func saveUser ( _ user : User ) async throws { messages . append ( . saveUser ( user ) ) } } Async Testing Patterns Testing async/await Code func test_fetchUser_withValidId_returnsUser ( ) async throws { // Arrange let expectedUser = User ( id : "123" , name : "John Doe" ) mockNetworkManager . fetchUserResult = . success ( expectedUser ) // Act let result = try await sut . fetchUser ( id : "123" ) // Assert XCTAssertEqual ( result , expectedUser ) } func test_fetchUser_withNetworkError_throwsError ( ) async { // Arrange mockNetworkManager . fetchUserResult = . failure ( NetworkError . connectionFailed ) // Act & Assert await XCTAssertThrowsError ( try await sut . fetchUser ( id : "123" ) ) { error in XCTAssertEqual ( error as ? NetworkError , . connectionFailed ) } } Testing with Expectations func test_notificationObserver_receivesNotification ( ) { // Arrange let expectation = XCTestExpectation ( description : "Notification received" ) let notificationName = Notification . Name ( "TestNotification" ) let observer = NotificationCenter . default . addObserver ( forName : notificationName , object : nil , queue : nil ) { _ in expectation . fulfill ( ) } // Act NotificationCenter . default . post ( name : notificationName , object : nil ) // Assert wait ( for : [ expectation ] , timeout : 1.0 ) // Cleanup NotificationCenter . default . removeObserver ( observer ) } func test_delegateCallback_isCalledOnSuccess ( ) { // Arrange let expectation = XCTestExpectation ( description : "Delegate called" ) let mockDelegate = MockDelegate ( ) mockDelegate . onSuccessCalled = { expectation . fulfill ( ) } sut . delegate = mockDelegate // Act sut . performOperation ( ) // Assert wait ( for : [ expectation ] , timeout : 2.0 ) XCTAssertTrue ( mockDelegate . successCallCount == 1 ) } Testing Combine Publishers import Combine func test_userPublisher_emitsUser ( ) { // Arrange var receivedUser : User ? var receivedError : Error ? let expectation = XCTestExpectation ( description : "Publisher emits" ) let cancellable = sut . userPublisher . sink ( receiveCompletion : { completion in if case . failure ( let error ) = completion { receivedError = error } expectation . fulfill ( ) } , receiveValue : { user in receivedUser = user } ) // Act sut . loadUser ( id : "123" ) // Assert wait ( for : [ expectation ] , timeout : 2.0 ) XCTAssertNotNil ( receivedUser ) XCTAssertNil ( receivedError ) cancellable . cancel ( ) } View Controller Testing class LoginViewControllerTests : XCTestCase { var sut : LoginViewController ! var mockAuthService : MockAuthService ! override func setUpWithError ( ) throws { let storyboard = UIStoryboard ( name : "Main" , bundle : nil ) sut = storyboard . instantiateViewController ( withIdentifier : "LoginViewController" ) as ? LoginViewController mockAuthService = MockAuthService ( ) sut . authService = mockAuthService // Load view hierarchy sut . loadViewIfNeeded ( ) } override func tearDownWithError ( ) throws { sut = nil mockAuthService = nil } func test_outlets_areConnected ( ) { XCTAssertNotNil ( sut . emailTextField ) XCTAssertNotNil ( sut . passwordTextField ) XCTAssertNotNil ( sut . loginButton ) XCTAssertNotNil ( sut . errorLabel ) } func test_loginButton_tap_callsAuthService ( ) { // Arrange sut . emailTextField . text = "test@example.com" sut . passwordTextField . text = "password123" // Act sut . loginButton . sendActions ( for : . touchUpInside ) // Assert XCTAssertEqual ( mockAuthService . loginCallCount , 1 ) XCTAssertEqual ( mockAuthService . lastLoginEmail , "test@example.com" ) XCTAssertEqual ( mockAuthService . lastLoginPassword , "password123" ) } func test_loginButton_withEmptyEmail_showsError ( ) { // Arrange sut . emailTextField . text = "" sut . passwordTextField . text = "password" // Act sut . loginButton . sendActions ( for : . touchUpInside ) // Assert XCTAssertEqual ( mockAuthService . loginCallCount , 0 ) XCTAssertFalse ( sut . errorLabel . isHidden ) XCTAssertEqual ( sut . errorLabel . text , "Email is required" ) } func test_successfulLogin_navigatesToHome ( ) { // Arrange mockAuthService . loginResult = . success ( User ( id : "1" , name : "Test" ) ) let mockNavigator = MockNavigator ( ) sut . navigator = mockNavigator sut . emailTextField . text = "test@example.com" sut . passwordTextField . text = "password" // Act sut . loginButton . sendActions ( for : . touchUpInside ) // Assert XCTAssertTrue ( mockNavigator . didNavigateToHome ) } } Performance Testing func test_dataProcessing_performance ( ) { let largeDataSet = generateLargeDataSet ( count : 10000 ) measure { _ = sut . processData ( largeDataSet ) } } func test_dataProcessing_performanceWithOptions ( ) { let options = XCTMeasureOptions ( ) options . iterationCount = 10 measure ( options : options ) { _ = sut . processData ( generateLargeDataSet ( count : 5000 ) ) } } func test_memoryUsage_withLargeDataSet ( ) { let options = XCTMeasureOptions ( ) options . iterationCount = 5 measure ( metrics : [ XCTMemoryMetric ( ) ] , options : options ) { autoreleasepool { let data = sut . loadLargeDataSet ( ) sut . processData ( data ) } } } func test_cpuUsage_duringOperation ( ) { measure ( metrics : [ XCTCPUMetric ( ) ] ) { sut . performCPUIntensiveOperation ( ) } } Parameterized Testing func test_emailValidation_withVariousInputs ( ) { let testCases : [ ( email : String , isValid : Bool ) ] = [ ( "valid@example.com" , true ) , ( "user.name@domain.co.uk" , true ) , ( "invalid.email" , false ) , ( "" , false ) , ( "@example.com" , false ) , ( "test@" , false ) , ( "test@.com" , false ) , ( "test@domain" , false ) ] for testCase in testCases { let result = sut . isValidEmail ( testCase . email ) XCTAssertEqual ( result , testCase . isValid , "Failed for email: ' ( testCase . email ) ' - expected ( testCase . isValid ) , got ( result ) " ) } } // Using XCTestCase subclass for cleaner parameterized tests class EmailValidationTests : XCTestCase { struct TestCase { let input : String let expected : Bool let file : StaticString let line : UInt init ( _ input : String , _ expected : Bool , file : StaticString =
file
, line : UInt =
line
) { self . input = input self . expected = expected self . file = file self . line = line } } func test_isValidEmail ( ) { let testCases = [ TestCase ( "test@example.com" , true ) , TestCase ( "invalid" , false ) , TestCase ( "" , false ) ] for testCase in testCases { let result = EmailValidator . isValid ( testCase . input ) XCTAssertEqual ( result , testCase . expected , file : testCase . file , line : testCase . line ) } } } UI Testing with XCUITest class LoginUITests : XCTestCase { var app : XCUIApplication ! override func setUpWithError ( ) throws { continueAfterFailure = false app = XCUIApplication ( ) app . launchArguments = [ "--uitesting" ] app . launch ( ) } func test_loginFlow_withValidCredentials_showsHomeScreen ( ) { // Navigate to login let loginButton = app . buttons [ "LoginButton" ] XCTAssertTrue ( loginButton . waitForExistence ( timeout : 5 ) ) // Enter credentials let emailField = app . textFields [ "EmailTextField" ] emailField . tap ( ) emailField . typeText ( "test@example.com" ) let passwordField = app . secureTextFields [ "PasswordTextField" ] passwordField . tap ( ) passwordField . typeText ( "password123" ) // Tap login loginButton . tap ( ) // Verify home screen let homeTitle = app . staticTexts [ "Welcome" ] XCTAssertTrue ( homeTitle . waitForExistence ( timeout : 10 ) ) } func test_loginFlow_withInvalidCredentials_showsError ( ) { let emailField = app . textFields [ "EmailTextField" ] emailField . tap ( ) emailField . typeText ( "wrong@example.com" ) let passwordField = app . secureTextFields [ "PasswordTextField" ] passwordField . tap ( ) passwordField . typeText ( "wrongpassword" ) app . buttons [ "LoginButton" ] . tap ( ) let errorLabel = app . staticTexts [ "ErrorLabel" ] XCTAssertTrue ( errorLabel . waitForExistence ( timeout : 5 ) ) XCTAssertEqual ( errorLabel . label , "Invalid credentials" ) } } Test Configuration Test Scheme Setup test_scheme_configuration : unit_tests : targets : [ "YourAppTests" ] coverage : true parallel : true ui_tests : targets : [ "YourAppUITests" ] coverage : false parallel : false launch_arguments : [ "--uitesting" , "--reset-state" ] integration_tests : targets : [ "YourAppIntegrationTests" ] coverage : true parallel : false Test Plan Configuration { "configurations" : [ { "name" : "Unit Tests" , "options" : { "targetForVariableExpansion" : { "target" : { "name" : "YourApp" } } } } ] , "defaultOptions" : { "codeCoverage" : true , "testTimeoutsEnabled" : true , "defaultTestExecutionTimeAllowance" : 60 } , "testTargets" : [ { "target" : { "name" : "YourAppTests" } } ] , "version" : 1 } Лучшие практики AAA Pattern — Arrange, Act, Assert для каждого теста One assertion per test — один логический assert на тест Descriptive names — test_methodName_condition_expectedResult Test isolation — каждый тест независим от других Mock external dependencies — сеть, БД, системные сервисы Fast tests — unit tests должны выполняться за миллисекунды