Как решить проблему тестирования синглтона с использованием моков
Что проверяют
Знаете ли вы, почему синглтон мешает тестам, и умеете ли подменить его без хаков над глобальным состоянием. Следом обычно спрашивают, как быть, если код трогать нельзя, например это сторонний SDK или URLSession.shared.
Как отвечать
Корень проблемы: код сам обращается к Service.shared, и тест не может подсунуть другую реализацию. Причём static let shared неизменяем, так что присвоить туда мок, как иногда пишут в примерах, просто не получится: код не скомпилируется.
Рабочий путь в три шага:
- выделить протокол с методами, которые реально нужны потребителю;
- внедрить через инициализатор:
init(service: ServiceProtocol = Service.shared), в тесте передаётся мок; - написать мок, который запоминает вызовы и возвращает заданный ответ:
var trackedEvents: [Event] = [],var stubbedResult: Result<User, Error>. В Swift нет рантайм-моков в духе Mockito, поэтому их пишут вручную или генерируют через Sourcery или Mockolo.
Когда код изменить нельзя:
- сеть: свой подкласс
URLProtocol, зарегистрированный вURLSessionConfiguration.protocolClasses. Запросы перехватываются без правки клиента; - UserDefaults: отдельный экземпляр
UserDefaults(suiteName:)на тест с очисткой черезremovePersistentDomain(forName:); - сторонний SDK: собственная обёртка с протоколом, дальше как выше.
Крайний вариант: static var shared: ServiceProtocol с подменой в тестах. Это работает, но оставляет глобальное изменяемое состояние, и вы должны объяснить, как его сбрасываете.
Где спотыкаются
Подменяют глобальный shared в тесте и не восстанавливают. Swift Testing по умолчанию гоняет тесты параллельно, и соседний тест внезапно получает ваш мок.
Путают мок и стаб. Стаб только возвращает данные, мок ещё и проверяет, что и сколько раз вызвали, и на собеседовании это различие ждут.
Тестируют сам синглтон вместо кода, который им пользуется. Интересно поведение потребителя при успехе, ошибке и пустом ответе, а не то, что shared возвращает один и тот же объект.