Какие проблемы при тестировании создает прямое создание экземпляров зависимостей в коде?

Ответ

Да, прямое создание экземпляров (hard-coded dependencies) серьезно затрудняет модульное тестирование.

Основные проблемы:

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

Пример проблемного кода:

class DataLoader {
    func loadUserData() -> [User] {
        let apiClient = APIClient() // Прямое создание зависимости
        return apiClient.fetchUsers() // Реальный сетевой вызов в тесте
    }
}

Решение — Dependency Injection (DI):

protocol APIClientProtocol {
    func fetchUsers() -> [User]
}

class DataLoader {
    private let apiClient: APIClientProtocol

    // Зависимость внедряется извне
    init(apiClient: APIClientProtocol) {
        self.apiClient = apiClient
    }

    func loadUserData() -> [User] {
        return apiClient.fetchUsers()
    }
}

// В тесте можно использовать мок
class MockAPIClient: APIClientProtocol {
    var fetchUsersCalled = false
    func fetchUsers() -> [User] {
        fetchUsersCalled = true
        return [User.testInstance]
    }
}

Практика: Используйте внедрение зависимостей через инициализатор или свойства, чтобы сделать код тестируемым и соблюсти принцип инверсии зависимостей (Dependency Inversion Principle).

Ответ 18+ 🔞

А, ну это же классика, блядь! Прямо как в том анекдоте про Герасима и Муму, только тут вместо собаки — твой код, а вместо немого силача — ты, который пытается его протестить, а нихуя не выходит!

Смотри, в чём прикол. Вот ты пишешь класс, а внутри него, как последний распиздяй, прямо в теле метода создаёшь какую-нибудь хрень типа APIClient() или DatabaseManager(). Ну, типа, «а чё такого-то, оно же работает».

А потом, сука, начинается цирк. Ты пытаешься написать юнит-тест для метода loadUserData(). Запускаешь — и тут тебе на голову сваливается овердохуища проблем:

  1. Изоляция? Не, не слышал. Твой тест вместо того, чтобы проверить логику DataLoader, внезапно начинает реально лезть в интернет или в базу данных. Это как проверять, работает ли микроволновка, подключая её к атомной электростанции. Пидарас шерстяной, да это же пиздец, а не тест! Он медленный, он падает, если нет сети, он может наебнуть продакшен-данные!

  2. Побочки на каждом шагу. Твой «юнит-тест» отправляет письма, списывает деньги и вызывает такси. Волнение ебать! Терпения ноль ебать! Ты просто хотел проверить, правильно ли данные форматируются, а в итоге у тебя вся команда поддержки в ахуе от спама, который ты нагенерил.

  3. А че там внутри происходит? Хуй его знает! Ты не можешь проверить, вызывался ли вообще APIClient и с какими параметрами. Может, он вообще не вызывается, а данные из кэша берутся? А может, вызывается десять раз? Пизда с ушами, не разберёшь.

Вот смотри на этот ужас, прям как в плохом кино:

class DataLoader {
    func loadUserData() -> [User] {
        let apiClient = APIClient() // О, сука! Прямо здесь, внаглую!
        return apiClient.fetchUsers() // Пиздец, поехали в интернет, мальчики!
    }
}

А решение-то, блядь, проще пареной репы! Называется Dependency Injection, или, по-нашему, «не тыкай зависимости внутрь, давай их снаружи».

Суть в том, чтобы твой класс не знал, как создавать свои зависимости. Он должен их получать уже готовенькими. Как будто ты не сам готовишь обед, а тебе его приносят. И в тестах ты можешь принести муляж — пластиковую курицу вместо настоящей.

Делается это так:

  1. Выделяешь протокол для своей зависимости. Это как договор: «вот такие методы у меня есть».
  2. Прокидываешь эту зависимость извне — через инициализатор (лучше всего) или свойство.
  3. В продакшене ты передаёшь реальный APIClient.
  4. В тесте ты передаёшь свою подставную суку — мок, который делает ровно то, что тебе нужно для проверки.

Смотри, как красивше становится:

// 1. Договорились, как будет выглядеть наш «поставщик данных»
protocol APIClientProtocol {
    func fetchUsers() -> [User]
}

class DataLoader {
    private let apiClient: APIClientProtocol // Храним абстракцию, а не конкретную реализацию

    // 2. Зависимость НЕ создаём, а ПРИНИМАЕМ. Инициализатор — наш лучший друг.
    init(apiClient: APIClientProtocol) {
        self.apiClient = apiClient
    }

    func loadUserData() -> [User] {
        // 3. Работаем с тем, что дали. Никаких сюрпризов.
        return apiClient.fetchUsers()
    }
}

// 4. А вот наша подставная сука для тестов. Полная контролируемая хуйня.
class MockAPIClient: APIClientProtocol {
    // Можем отслеживать, вызывался ли метод
    var fetchUsersCalled = false
    // Можем возвращать любые данные, которые нужны для теста
    var stubbedUsers: [User] = [User.testInstance]

    func fetchUsers() -> [User] {
        fetchUsersCalled = true // Отметили вызов
        return stubbedUsers // Вернули то, что сами захотели
    }
}

И теперь твой тест — это просто песня, а не ебаный цирк с конями:

func testLoadUserDataCallsAPIClient() {
    // Даём тестируемому объекту нашу подставу
    let mockClient = MockAPIClient()
    let loader = DataLoader(apiClient: mockClient)

    // Запускаем метод
    _ = loader.loadUserData()

    // Проверяем, что договор соблюдён: метод у мока был вызван
    XCTAssertTrue(mockClient.fetchUsersCalled)
}

Вот и вся магия, ёпта! Код становится гибким, тестируемым и не привязанным намертво к конкретным реализациям. Это и есть тот самый принцип инверсии зависимостей (DIP из SOLID) — завись от абстракций, а не от конкретики.

Так что прекращай быть Герасимом, который сам всё делает и нихуя не может проверить. Стань архитектором, который грамотно принимает инструменты извне. И тогда твои тесты перестанут быть русской рулеткой с доступом в прод.