Введение: Зачем тестировать Swift-приложения?
Разработка качественного iOS или macOS приложения невозможна без грамотного тестирования. В экосистеме Swift существует мощный фреймворк XCTest, который позволяет писать все три уровня тестов: модульные (unit), интеграционные (integration) и сквозные (E2E). Каждый из этих уровней отвечает за свою зону ответственности и помогает выявлять ошибки на разных этапах разработки.
В этой статье мы разберём, как правильно настроить и написать тесты для Swift-приложений, используя современные практики и инструменты. Вы узнаете, чем отличаются разные типы тестов, и получите готовые примеры кода, которые можно сразу применить в своём проекте.
Unit-тестирование в Swift
Unit-тесты — это фундамент пирамиды тестирования. Они проверяют работу отдельных компонентов (методов, классов, структур) в изоляции от внешних зависимостей.
Настройка Unit-тестов в Xcode
Для создания тестового таргета в Xcode, при создании проекта достаточно отметить галочку "Include Tests". Если проект уже существует, добавьте новый таргет: File → New → Target → iOS Unit Testing Bundle.
Базовый пример Unit-теста
Рассмотрим простой пример тестирования калькулятора:
import XCTest
final class CalculatorTests: XCTestCase {
var calculator: Calculator!
override func setUpWithError() throws {
calculator = Calculator()
}
override func tearDownWithError() throws {
calculator = nil
}
func testAddition() {
// Given
let a = 5
let b = 3
// When
let result = calculator.add(a, b)
// Then
XCTAssertEqual(result, 8, "5 + 3 должно быть равно 8")
}
func testDivisionByZero() {
XCTAssertThrowsError(try calculator.divide(10, 0)) { error in
XCTAssertEqual(error as? CalculatorError, CalculatorError.divisionByZero)
}
}
}
Лучшие практики для Unit-тестов
- Изоляция: Каждый тест должен быть независим. Используйте
setUpWithErrorиtearDownWithErrorдля подготовки и очистки. - Паттерн AAA: Arrange (подготовка), Act (действие), Assert (проверка).
- Одна проверка на тест: Идеальный тест проверяет только одно поведение.
- Именование: Используйте описательные имена:
testMethodName_WhenCondition_ExpectedResult.
Интеграционное тестирование в Swift
Интеграционные тесты проверяют взаимодействие между компонентами: работа с базой данных, сетевыми запросами, файловой системой. В отличие от unit-тестов, здесь мы не мокаем всё подряд, а тестируем реальные взаимодействия.
Тестирование Core Data
Пример тестирования работы с Core Data:
import XCTest
@testable import MyApp
final class CoreDataIntegrationTests: XCTestCase {
var coreDataStack: CoreDataStack!
override func setUpWithError() throws {
coreDataStack = CoreDataStack(inMemory: true) // Используем in-memory хранилище
}
func testSaveAndFetchUser() {
// Arrange
let user = User(context: coreDataStack.viewContext)
user.name = "Иван"
user.age = 30
// Act
try? coreDataStack.viewContext.save()
let fetchRequest: NSFetchRequest<User> = User.fetchRequest()
let users = try? coreDataStack.viewContext.fetch(fetchRequest)
// Assert
XCTAssertEqual(users?.count, 1)
XCTAssertEqual(users?.first?.name, "Иван")
}
}
Тестирование сетевого слоя
Для интеграционных тестов сети используйте реальные запросы к тестовому серверу или URLProtocol для перехвата запросов:
class MockURLProtocol: URLProtocol {
static var requestHandler: ((URLRequest) throws -> (HTTPURLResponse, Data))?
override class func canInit(with request: URLRequest) -> Bool {
return true
}
override class func canonicalRequest(for request: URLRequest) -> URLRequest {
return request
}
override func startLoading() {
guard let handler = MockURLProtocol.requestHandler else {
XCTFail("Handler not set")
return
}
do {
let (response, data) = try handler(request)
client?.urlProtocol(self, didReceive: response, cacheStoragePolicy: .notAllowed)
client?.urlProtocol(self, didLoad: data)
client?.urlProtocolDidFinishLoading(self)
} catch {
client?.urlProtocol(self, didFailWithError: error)
}
}
override func stopLoading() {}
}
E2E-тестирование (XCUITest)
Сквозные (end-to-end) тесты проверяют приложение целиком, имитируя действия пользователя. В iOS для этого используется фреймворк XCUITest.
Настройка E2E-тестов
Добавьте новый таргет: File → New → Target → iOS UI Testing Bundle. XCUITest запускает приложение на симуляторе или реальном устройстве.
Пример UI-теста для логина
import XCTest
final class LoginUITests: XCTestCase {
var app: XCUIApplication!
override func setUpWithError() throws {
continueAfterFailure = false
app = XCUIApplication()
app.launch()
}
func testSuccessfulLogin() {
// Находим элементы интерфейса
let emailTextField = app.textFields["emailField"]
let passwordTextField = app.secureTextFields["passwordField"]
let loginButton = app.buttons["loginButton"]
// Действия пользователя
emailTextField.tap()
emailTextField.typeText("test@example.com")
passwordTextField.tap()
passwordTextField.typeText("password123")
loginButton.tap()
// Проверяем результат
let welcomeLabel = app.staticTexts["welcomeLabel"]
XCTAssertTrue(welcomeLabel.waitForExistence(timeout: 5))
XCTAssertEqual(welcomeLabel.label, "Добро пожаловать!")
}
func testInvalidEmailError() {
let emailTextField = app.textFields["emailField"]
let loginButton = app.buttons["loginButton"]
emailTextField.tap()
emailTextField.typeText("invalid")
loginButton.tap()
let errorLabel = app.staticTexts["errorLabel"]
XCTAssertTrue(errorLabel.exists)
XCTAssertEqual(errorLabel.label, "Некорректный email")
}
}
Советы по XCUITest
- Accessibility Identifiers: Всегда добавляйте
accessibilityIdentifier