Ответ
Да, прямое создание экземпляров (hard-coded dependencies) серьезно затрудняет модульное тестирование.
Основные проблемы:
- Невозможность изоляции — нельзя заменить реальную зависимость (API, базу данных, файловую систему) на контролируемый мок или стаб.
- Побочные эффекты — тесты могут выполнять реальные сетевые запросы или изменять данные в БД, что делает их медленными, недетерминированными и разрушительными.
- Сложность проверки взаимодействий — нельзя проверить, был ли вызван метод зависимости с правильными аргументами.
Пример проблемного кода:
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(). Запускаешь — и тут тебе на голову сваливается овердохуища проблем:
-
Изоляция? Не, не слышал. Твой тест вместо того, чтобы проверить логику
DataLoader, внезапно начинает реально лезть в интернет или в базу данных. Это как проверять, работает ли микроволновка, подключая её к атомной электростанции. Пидарас шерстяной, да это же пиздец, а не тест! Он медленный, он падает, если нет сети, он может наебнуть продакшен-данные! -
Побочки на каждом шагу. Твой «юнит-тест» отправляет письма, списывает деньги и вызывает такси. Волнение ебать! Терпения ноль ебать! Ты просто хотел проверить, правильно ли данные форматируются, а в итоге у тебя вся команда поддержки в ахуе от спама, который ты нагенерил.
-
А че там внутри происходит? Хуй его знает! Ты не можешь проверить, вызывался ли вообще
APIClientи с какими параметрами. Может, он вообще не вызывается, а данные из кэша берутся? А может, вызывается десять раз? Пизда с ушами, не разберёшь.
Вот смотри на этот ужас, прям как в плохом кино:
class DataLoader {
func loadUserData() -> [User] {
let apiClient = APIClient() // О, сука! Прямо здесь, внаглую!
return apiClient.fetchUsers() // Пиздец, поехали в интернет, мальчики!
}
}
А решение-то, блядь, проще пареной репы! Называется Dependency Injection, или, по-нашему, «не тыкай зависимости внутрь, давай их снаружи».
Суть в том, чтобы твой класс не знал, как создавать свои зависимости. Он должен их получать уже готовенькими. Как будто ты не сам готовишь обед, а тебе его приносят. И в тестах ты можешь принести муляж — пластиковую курицу вместо настоящей.
Делается это так:
- Выделяешь протокол для своей зависимости. Это как договор: «вот такие методы у меня есть».
- Прокидываешь эту зависимость извне — через инициализатор (лучше всего) или свойство.
- В продакшене ты передаёшь реальный
APIClient. - В тесте ты передаёшь свою подставную суку — мок, который делает ровно то, что тебе нужно для проверки.
Смотри, как красивше становится:
// 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) — завись от абстракций, а не от конкретики.
Так что прекращай быть Герасимом, который сам всё делает и нихуя не может проверить. Стань архитектором, который грамотно принимает инструменты извне. И тогда твои тесты перестанут быть русской рулеткой с доступом в прод.