Testing in Swift: unit, integration, e2e — advanced level

Online Python Trainer for Beginners

Learn Python easily without overwhelming theory. Solve practical tasks with automatic checking, get hints in Russian, and write code directly in your browser — no installation required.

Start Course

Advanced Testing in Swift: Unit, Integration, and E2E



Testing is not just a check, but the foundation of reliable code. At an advanced level, a Swift developer needs not only to write tests but also to competently build the testing pyramid: from fast unit tests to complex end-to-end (e2e) scenarios. In this article, we will cover advanced techniques for iOS/macOS projects: working with dependencies, integration tests with CoreData, and UI automation.



1. Unit Tests: Dependency Management and Mocks



At an advanced level, unit tests must be isolated. The key technique is Dependency Injection (DI). Instead of testing the real network layer or database, we replace them with mocks.



Example: Protocol and DI

protocol NetworkServiceProtocol {    func fetchData(completion: @escaping (Result<String, Error>) -> Void)}

class ViewModel { private let networkService: NetworkServiceProtocol init(networkService: NetworkServiceProtocol) { self.networkService = networkService } func loadData() { networkService.fetchData { result in // processing } }}

// Mock for testsclass MockNetworkService: NetworkServiceProtocol { var shouldReturnError = false func fetchData(completion: @escaping (Result<String, Error>) -> Void) { if shouldReturnError { completion(.failure(NSError(domain: "test", code: -1))) } else { completion(.success("Mock data")) } }}


In the test, you create a MockNetworkService and pass it to the ViewModel. This allows you to test error and success scenarios without a real API.



XCTestExpectation for Asynchronous Code

func testViewModelLoadDataSuccess() {    let mock = MockNetworkService()    let viewModel = ViewModel(networkService: mock)        let expectation = self.expectation(description: "Data loaded")        viewModel.loadData()        // Simulate async call    DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {        expectation.fulfill()    }        waitForExpectations(timeout: 1.0)    // Check ViewModel state}


2. Integration Tests: CoreData and Working with Real Data



Integration tests verify the interaction of components. For CoreData, we use an in-memory container to avoid polluting the main database.



Setting up In-Memory CoreData

class CoreDataTestStack {    lazy var persistentContainer: NSPersistentContainer = {        let container = NSPersistentContainer(name: "MyModel")        let description = NSPersistentStoreDescription()        description.type = NSInMemoryStoreType        container.persistentStoreDescriptions = [description]        container.loadPersistentStores { _, error in            if let error = error {                fatalError("Failed to load store: \\(error)")            }        }        return container    }()        var context: NSManagedObjectContext {        return persistentContainer.viewContext    }}


Example Integration Test

func testSaveAndFetchUser() {    let stack = CoreDataTestStack()    let repository = UserRepository(context: stack.context)        // Create user    let user = User(context: stack.context)    user.name = "Test User"    user.age = 30        try? stack.context.save()        // Verify data was saved    let fetchRequest: NSFetchRequest<User> = User.fetchRequest()    let users = try? stack.context.fetch(fetchRequest)        XCTAssertEqual(users?.count, 1)    XCTAssertEqual(users?.first?.name, "Test User")}


Important: clear the context after each test so that tests do not affect each other.

Blogs

Book Recommendations