Всегда ли в модульном тестировании нужны mock-объекты?

Ответ

Нет, mock-объекты нужны не для всех тестов. Они используются для изоляции тестируемого модуля от его внешних зависимостей (сеть, база данных, файловая система, сложные сервисы).

Когда моки НЕ нужны:

  • При тестировании pure-функций (детерминированных, без побочных эффектов).
  • Для простой логики, не зависящей от других объектов.

Пример теста без мока:

// Pure-функция, тестируется без моков.
func add(_ a: Int, _ b: Int) -> Int {
    return a + b
}

func testAddition() {
    let result = add(2, 2)
    XCTAssertEqual(result, 4)
}

Когда моки НУЖНЫ:

  1. Зависимости медленные или нестабильные (сетевые запросы, доступ к БД).
  2. Нужно проверить взаимодействие (например, был ли вызван метод сервиса с правильными параметрами).
  3. Зависимости имеют сложное состояние или побочные эффекты, которые трудно воспроизвести в тесте.
  4. Тестирование сценариев ошибок, которые сложно вызвать у реальной зависимости.

Пример использования мока для сетевого сервиса:

protocol NetworkServiceProtocol {
    func fetchData(completion: (Data?) -> Void)
}

class MockNetworkService: NetworkServiceProtocol {
    var simulatedData: Data?
    var wasCalled = false

    func fetchData(completion: (Data?) -> Void) {
        wasCalled = true // Проверяем факт вызова
        completion(simulatedData) // Подменяем ответ
    }
}

// В тесте инжектим мок вместо реального сервиса.
let mockService = MockNetworkService()
mockService.simulatedData = Data()
let sut = ViewModel(networkService: mockService)
sut.loadData()
XCTAssertTrue(mockService.wasCalled)

Ответ 18+ 🔞

Давай разберёмся, что за хуйня с этими моками. Многие думают, что это какая-то обязательная магия для всех тестов, но это, блядь, не так. Это как молоток — отличный инструмент, но не для того, чтобы яйца на завтрак жарить.

Когда можно обойтись без этой всей ебалы с моками?

  • Когда у тебя чистая функция, как монашка в церкви. Никаких побочных эффектов, только вход и выход. Зависимостей — ноль ебать.
  • Когда логика проще, чем у табуретки. Никаких внешних сервисов, просто свои мозги.

Вот, смотри, пример, где всё просто, как три копейки:

// Функция, которая просто складывает. Ни с кем не спит, никуда не ходит.
func add(_ a: Int, _ b: Int) -> Int {
    return a + b
}

func testAddition() {
    let result = add(2, 2)
    XCTAssertEqual(result, 4) // Вот и вся проверка, блядь.
}

Видишь? Никаких моков, всё на своих двоих. Теперь, когда моки — это наше всё, наш спасительный хуй в пальто:

  1. Когда зависимость медленная или нестабильная, как поезд в час пик. Сеть, база данных, файлы. Не будешь же в тестах реально в интернет лазить или базу поднимать? Нахуй надо.
  2. Когда нужно проверить, а звонили ли вообще твоему другу. То есть, был ли вызван метод у зависимости с правильными параметрами.
  3. Когда у зависимости состояние сложнее, чем у мартышки после банановой вечеринки. Или побочные эффекты, которые в тесте не воспроизвести.
  4. Когда нужно протестировать сценарии "всё пошло по пизде". Как заставить реальный сетевой сервис вернуть именно ошибку 500 в нужный момент? Легче накрутить мок.

Смотри, как это выглядит в деле, на примере какого-нибудь сетевого костыля:

// Протокол — наша лазейка, чтобы подсунуть фальшивку.
protocol NetworkServiceProtocol {
    func fetchData(completion: (Data?) -> Void)
}

// А вот и наш подставной мудак, мок-объект.
class MockNetworkService: NetworkServiceProtocol {
    var simulatedData: Data? // Что будем подсовывать вместо ответа
    var wasCalled = false // Флажок, звонили или нет

    func fetchData(completion: (Data?) -> Void) {
        wasCalled = true // Отмечаем, что вызов был, ёпта!
        completion(simulatedData) // И плюём в лицо тесту тем, что приготовили
    }
}

// А теперь сам тест, где мы всех наёбываем.
func testLoadData() {
    // Создаём подставу
    let mockService = MockNetworkService()
    mockService.simulatedData = Data() // Говорим, что она вернёт пустые данные

    // Создаём наш тестируемый объект и подсовываем ему фальшивку вместо реального сервиса
    let sut = ViewModel(networkService: mockService)

    // Запускаем логику
    sut.loadData()

    // И проверяем: а вызывался ли вообще наш мок?
    XCTAssertTrue(mockService.wasCalled, "Сервис должен был быть вызван, а его нихуя!")
}

Вот и вся философия, блядь. Не нужно мокать всё подряд, как последний распиздяй. Используй там, где без этого реально нихуя не проверишь. А в остальных случаях — не усложняй, ёпта.