Ответ
Имею опыт написания модульных тестов с использованием нативного фреймворка XCTest.
Что тестирую:
- Бизнес-логику: Чистые функции, преобразователи данных, валидаторы.
- Сетевой слой: Мокаю
URLSessionдля тестирования сетевых клиентов. - Модели данных: Корректность конформности протоколам (Codable, Equatable).
Пример теста для ViewModel:
import XCTest
@testable import MyApp
class LoginViewModelTests: XCTestCase {
var viewModel: LoginViewModel!
var mockAuthService: MockAuthService!
override func setUp() {
super.setUp()
mockAuthService = MockAuthService()
viewModel = LoginViewModel(authService: mockAuthService)
}
func testLoginWithValidCredentials() {
// Arrange
let expectation = XCTestExpectation(description: "Login succeeds")
viewModel.onLoginSuccess = { expectation.fulfill() }
// Act
viewModel.email = "test@example.com"
viewModel.password = "validPass123"
viewModel.login()
// Assert
wait(for: [expectation], timeout: 1.0)
XCTAssertTrue(mockAuthService.loginWasCalled)
}
}
Подходы и best practices:
- Моки через протоколы: Зависимости инжектятся через протоколы, что позволяет легко подменять их мок-реализациями.
- Тестирование асинхронного кода: Использую
XCTestExpectation. - Структура теста (AAA): Arrange (подготовка), Act (действие), Assert (проверка).
- Покрытие: Фокусируюсь на тестировании критической логики и сложных сценариев, а не на достижении 100% покрытия.
Ответ 18+ 🔞
Давай разберём этот ваш священный грааль модульного тестирования, а то я вижу, тут люди пишут код, а потом ходят и молятся, чтобы он не разъехался в продакшене. Опыт с XCTest, говоришь? Ну, это как иметь дома огнетушитель — вроде и не нужен, пока не начнётся пиздец.
Что я там обычно проверяю, пока все спят:
- Бизнес-логику. Эти ваши чистые функции, которые из
"123"делают123, а из"пиздец"—nil. Если они сломаются, то вся остальная архитектура превратится в красивое, но бесполезное хуйло, которое только показывает белый экран или, что хуже, неправильные цифры. Это надо дрочить тестами в первую очередь. - Сетевой слой. Тут без вариантов. Подменяю
URLSessionна какую-нибудь манду с ушами, которая не ходит в интернет, а тупо возвращает мне заранее приготовленный JSON (или ошибку, чтобы проверить, как приложение не обосрётся). Иначе тесты будут ебаться каждый раз, когда у оператора плохой сигнал или API легло. Доверия ебать ноль к сети. - Модели данных. Ну тут всё просто, но обязательно. Декодируется ли ответ от сервера? Сохраняется ли в
UserDefaultsилиKeychain? Не забыл ли я для модели протоколEquatableдобавить, чтобы тесты вообще могли сравнивать ожидаемое с полученным? Мелочь, а потом удивление пиздец — почему тест не проходит.
Вот, смотри, как это примерно выглядит в коде, когда тестируешь какую-нибудь ViewModel для логина:
import XCTest
@testable import MyApp
class LoginViewModelTests: XCTestCase {
var viewModel: LoginViewModel!
var mockAuthService: MockAuthService! // Вот она, наша подстава!
override func setUp() {
super.setUp()
mockAuthService = MockAuthService() // Создаём муляж
viewModel = LoginViewModel(authService: mockAuthService) // И подсовываем его вьюмодели
}
func testLoginWithValidCredentials() {
// Arrange (Готовим)
let expectation = XCTestExpectation(description: "Логин-то должен пройти")
viewModel.onLoginSuccess = { expectation.fulfill() }
// Act (Жмём на кнопку)
viewModel.email = "test@example.com"
viewModel.password = "validPass123"
viewModel.login()
// Assert (Смотрим, что получилось)
wait(for: [expectation], timeout: 1.0)
XCTAssertTrue(mockAuthService.loginWasCalled) // А вызывался ли наш мок вообще?
}
}
А теперь по поводу лучших практик, чтобы не вышло как всегда:
- Моки через протоколы — это наше всё. Не надо пытаться мокать конкретный класс
SuperNetworkManager. Объявляешь протоколSuperNetworkManaging, и вьюмодель ждёт именно его. А в тестах подсовываешьMockSuperNetworkManager, который просто флажок поднимает или данные из файла возвращает. Вилкой в глаз тому, кто инжектит синглтоны напрямую. - Асинхронщина.
XCTestExpectation— твой друг. Главное — не забывать егоfulfill()вызывать, иначе тест будет висеть, пока таймаут не ебанёт, и ты будешь чесать репу, думая, что всё сломалось. - Структура AAA (Arrange, Act, Assert). Это как инструкция «разбери, почисти, собери». Сначала всё подготовил (моки, данные), потом вызвал метод, который тестируешь, и в конце проверил, что мир соответствует твоим ожиданиям. Без этого — каша в голове и в тестах.
- Про покрытие. Гнаться за 100% — это занятие для идиотов или для менеджеров, которые в коде не хуя не шарят. Нужно покрывать критическую логику и сложные сценарии. Тестировать геттеры/сеттеры или
UIViewController.viewDidLoad()— это чих-пых тебя в сраку, мартышлюшка. Тратишь время, а пользы — ноль. Лучше десять годных тестов на важную фичу, чем сто хуёвых на всякую ерунду.
Вот так вот, примерно. Пишешь тесты — спишь спокойнее. Не пишешь — живёшь на вулкане. Выбор за тобой.