Introduction: why testing is the foundation of a reliable Swift application
\n\nDevelopment for iOS and macOS requires high code reliability. An error in application logic can cost user experience, and in commercial projects — money. Testing in Swift is not just "good practice," but a mandatory development stage. It allows you to detect bugs at early stages, simplify refactoring, and improve code quality.
\n\nIn this article, we will break down three levels of testing: unit, integration, and e2e. You will learn how they differ, when to apply each one, and see working examples in Swift using the XCTest framework.
1. Unit testing in Swift: the foundation
\n\nUnit tests check individual modules (functions, methods, classes) in isolation. Their main goal is to ensure that each "unit" of code works correctly.
\n\nSetting up XCTest
\n\nXCTest is built into Xcode. To create a test target:
\n- \n
- Open your project in Xcode. \n
- Select File → New → Target. \n
- Select Unit Testing Bundle. \n
After creation, you will get a file like YourAppTests.swift.
Unit test example
\n\nSuppose we have a discount calculation function:
\n\nfunc calculateDiscount(price: Double, discount: Double) -> Double {\n guard discount >= 0 && discount <= 100 else { return price }\n return price * (1 - discount / 100)\n}\n\n\nWe write a test:
\n\nimport XCTest\n@testable import YourApp\n\nclass DiscountTests: XCTestCase {\n func testCalculateDiscount_ValidInput() {\n let result = calculateDiscount(price: 1000, discount: 20)\n XCTAssertEqual(result, 800, accuracy: 0.01)\n }\n \n func testCalculateDiscount_InvalidDiscount() {\n let result = calculateDiscount(price: 500, discount: 150)\n XCTAssertEqual(result, 500)\n }\n}\n\n\nBest practices for unit tests:
\n- \n
- One test — one assertion. \n
- Use
setUp()to initialize common objects. \n - Avoid dependencies on network, database, or UI — use mocks. \n
2. Integration tests: checking component interaction
\n\nIntegration tests check how different modules work together. For example, the interaction of a data model with a network layer or a CoreData database.
\n\nExample: testing a network request
\n\nLet's create a simple service:
\n\nclass NetworkService {\n func fetchUser(completion: @escaping (Result<User, Error>) -> Void) {\n // Real request to API\n }\n}\n\n\nAn integration test can verify that the service correctly handles a response from a real server (or from a local mock server):
\n\nclass NetworkServiceIntegrationTests: XCTestCase {\n var service: NetworkService!\n \n override func setUp() {\n super.setUp()\n service = NetworkService()\n }\n \n func testFetchUser_ReturnsUser() {\n let expectation = self.expectation(description: "Fetch user")\n \n service.fetchUser { result in\n switch result {\n case .success(let user):\n XCTAssertNotNil(user.name)\n case .failure:\n XCTFail("Request failed")\n }\n expectation.fulfill()\n }\n \n waitForExpectations(timeout: 5)\n }\n}\n\n\nImportant: Integration tests are slower than unit tests, so they are run less frequently (e.g., before merging to main).
\n\n3. E2E testing (end-to-end) with XCUITest
\n\nE2E tests (End-to-End) simulate user actions: button presses, text input, UI display verification. In Swift, this is done using