Testing in Swift: Unit, Integration, E2E from A to Z

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

Introduction: Why Testing is the Foundation of a Reliable Swift Application

Developing for iOS and macOS requires not only a beautiful interface but also stable logic. Testing in Swift is not an option, but a necessity. Without tests, even a small change can break functionality that has been working for years. In this article, we will break down three levels of testing: unit, integration, and e2e (end-to-end). You will learn how to properly configure, write, and run them on Apple platforms. At the end — practical tips for organizing test architecture.



Level 1: Unit Tests — The Foundation

Unit tests check individual functions, methods, or classes in isolation. In Swift, the main framework is XCTest. It is built into Xcode, requires no third-party dependencies, and is ideal for testing business logic.



How to Write Your First Unit Test

Create a new Unit Test Target in Xcode (File → New → Target → Unit Testing Bundle). Then add a test class:

import XCTest@testable import MyApp

final class CalculatorTests: XCTestCase { var sut: Calculator! override func setUpWithError() throws { sut = Calculator() } override func tearDownWithError() throws { sut = nil } func testAddition() { // Given let a = 5 let b = 3 // When let result = sut.add(a, b) // Then XCTAssertEqual(result, 8, "Addition should be correct") }}


Best Practices for Unit Testing

  • Isolation: each test tests only one unit of code. Use setUp and tearDown to reset state.
  • Naming: use the pattern test[Method]_[Condition]_[ExpectedResult].
  • Mocks and Stubs: for external dependencies (network, database), use protocols and substitute them in tests.
  • Coverage: aim for 70-80% code coverage, but don't chase 100% — test critical paths.


Example with a Mock

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

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

func testFetchDataSuccess() { let mock = MockNetworkService() let viewModel = ViewModel(networkService: mock) mock.shouldReturnError = false viewModel.loadData() XCTAssertTrue(viewModel.isDataLoaded)}


Level 2: Integration Testing — Checking Interactions

Integration tests check how different modules of the system work together: database + network, UI + business logic, multiple services. In Swift, XCTest is used for this with real (or in-memory) dependencies.



Typical Scenario: Core Data + Network

import XCTest@testable import MyApp

class UserServiceIntegrationTests: XCTestCase { var coreDataStack: CoreDataStack! var networkService: NetworkService! var userService: UserService! override func setUp() { coreDataStack = CoreDataStack(inMemory: true) // in-memory for tests networkService = NetworkService() userService = UserService(coreDataStack: coreDataStack, networkService: networkService) } func testSaveUserFromNetwork() { // Create an expectation for the async request let expectation = self.expe

Blogs

Book Recommendations