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
setUpandtearDownto 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