> Как решить проблему тестирования синглтона с использованием моков (iOS, Swift)

Уровень: senior · Роль: mobile · Категория: Технические вопросы

Компании: Bip.ru

Стек: iOS, Swift

> Пример ответа

Короткий ответ

Проблема тестирования синглтона сводится к его глобальному состоянию и жёсткой связи с конкретной реализацией. Решение - внедрение зависимости через протокол и инъекция мока в тестах. Для Swift это означает: объявить протокол, сделать синглтон его реализацией, а в тестах подменять экземпляр через свойство или параметр. Альтернатива - использовать специальный тестовый хук для замены shared-экземпляра. Главное - не тестировать сам синглтон, а изолировать его влияние на остальной код.

Подробное объяснение

Синглтон нарушает принцип открытости/закрытости и усложняет юнит-тестирование, потому что:

  • глобальное состояние сохраняется между тестами;
  • невозможно подменить реальную реализацию без изменения production-кода;
  • тесты становятся зависимыми от порядка выполнения.

Основной подход - protocol-based dependency injection. Вместо прямого обращения к SomeSingleton.shared код зависит от абстракции:

SWIFT
protocol DataServiceProtocol {
func fetchData() -> String
}
final class DataService: DataServiceProtocol {
static let shared = DataService()
private init() {}
func fetchData() -> String { "real data" }
}

Клиентский код принимает DataServiceProtocol через инициализатор или свойство. В тестах передаётся мок, реализующий протокол.

Для случаев, когда синглтон используется напрямую в legacy-коде, применяют тестовый хук - внутреннее свойство для замены shared-экземпляра:

SWIFT
final class DataService: DataServiceProtocol {
static var shared: DataServiceProtocol = DataService()
// ...
}

В тестах присваиваем мок: DataService.shared = mock. Важно сбрасывать состояние в tearDown.

Ещё вариант - локальная фабрика или контейнер зависимостей, но для мобильной разработки это часто избыточно.

На практике

Для senior-позиции важно показать понимание trade-off. Не всегда нужно полностью убирать синглтон - иногда достаточно:

  • сделать shared свойством с типом протокола;
  • добавить reset() для сброса состояния между тестами;
  • использовать static let только для неизменяемых конфигураций.

В iOS-проектах часто встречаются синглтоны для UserDefaults, FileManager, сетевых клиентов. Для них предпочтительнее внедрять через инициализатор, а не обращаться к shared внутри методов.

Пример правильного подхода:

SWIFT
final class ProfileViewModel {
private let dataService: DataServiceProtocol
init(dataService: DataServiceProtocol = DataService.shared) {
self.dataService = dataService
}
}

В тестах:

SWIFT
let mock = DataServiceMock()
let viewModel = ProfileViewModel(dataService: mock)

Пример кода

SWIFT
// Протокол
protocol NetworkServiceProtocol {
func request() -> String
}
// Реальная реализация
final class NetworkService: NetworkServiceProtocol {
static let shared = NetworkService()
private init() {}
func request() -> String { "real response" }
}
// Мок для тестов
final class NetworkServiceMock: NetworkServiceProtocol {
var result: String = "mock response"
func request() -> String { result }
}
// Клиент
final class ApiClient {
private let network: NetworkServiceProtocol
init(network: NetworkServiceProtocol = NetworkService.shared) {
self.network = network
}
func fetch() -> String {
network.request()
}
}
// Тест
import XCTest
final class ApiClientTests: XCTestCase {
func testFetchWithMock() {
let mock = NetworkServiceMock()
mock.result = "test data"
let client = ApiClient(network: mock)
XCTAssertEqual(client.fetch(), "test data")
}
}

Как отвечать на собеседовании

Начни с формулировки проблемы: глобальное состояние и жёсткая связь. Затем предложи основной паттерн - протокол + инъекция. Упомяни, что для legacy-кода можно использовать тестовый хук. Покажи понимание, когда синглтон оправдан (например, для неизменяемых конфигов), а когда нет. Обязательно приведи пример кода на Swift. Если спросят про Swift Concurrency - отметь, что actor может быть альтернативой для потокобезопасного синглтона, но тестируется так же через протокол.

Что проверяет интервьюер

  • понимание принципов SOLID, особенно dependency inversion;
  • умение проектировать тестируемый код;
  • знание особенностей Swift (static properties, access control);
  • способность аргументировать trade-off между простотой и тестируемостью;
  • практический опыт с XCTest и моками.

Типичные ошибки

  • предлагать тестировать сам синглтон через XCTAssertEqual(DataService.shared.fetch(), ...) - это не юнит-тест;
  • забывать сбрасывать состояние мока между тестами;
  • делать shared не-protected, что позволяет менять его из любого места;
  • использовать swizzling или runtime-подмену без необходимости - это хрупко;
  • отрицать синглтоны полностью, вместо того чтобы показать гибкий подход.

> ГОТОВЫ К СЛЕДУЮЩЕМУ СОБЕСЕДОВАНИЮ?

Запустите тренировочную сессию с ИИ и получите детальную обратную связь, чтобы увереннее проходить реальные интервью