Какой у вас опыт модульного (unit) тестирования в iOS?

Ответ

Имею опыт написания модульных тестов с использованием нативного фреймворка 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) // А вызывался ли наш мок вообще?
    }
}

А теперь по поводу лучших практик, чтобы не вышло как всегда:

  1. Моки через протоколы — это наше всё. Не надо пытаться мокать конкретный класс SuperNetworkManager. Объявляешь протокол SuperNetworkManaging, и вьюмодель ждёт именно его. А в тестах подсовываешь MockSuperNetworkManager, который просто флажок поднимает или данные из файла возвращает. Вилкой в глаз тому, кто инжектит синглтоны напрямую.
  2. Асинхронщина. XCTestExpectation — твой друг. Главное — не забывать его fulfill() вызывать, иначе тест будет висеть, пока таймаут не ебанёт, и ты будешь чесать репу, думая, что всё сломалось.
  3. Структура AAA (Arrange, Act, Assert). Это как инструкция «разбери, почисти, собери». Сначала всё подготовил (моки, данные), потом вызвал метод, который тестируешь, и в конце проверил, что мир соответствует твоим ожиданиям. Без этого — каша в голове и в тестах.
  4. Про покрытие. Гнаться за 100% — это занятие для идиотов или для менеджеров, которые в коде не хуя не шарят. Нужно покрывать критическую логику и сложные сценарии. Тестировать геттеры/сеттеры или UIViewController.viewDidLoad() — это чих-пых тебя в сраку, мартышлюшка. Тратишь время, а пользы — ноль. Лучше десять годных тестов на важную фичу, чем сто хуёвых на всякую ерунду.

Вот так вот, примерно. Пишешь тесты — спишь спокойнее. Не пишешь — живёшь на вулкане. Выбор за тобой.