> Как реализовать unit test для функции записи данных в файл (iOS, Swift)
Уровень: senior · Роль: mobile · Категория: Технические вопросы
Компании: Revolut
Стек: iOS, Swift
> Пример ответа
Короткий ответ
Unit test для функции записи данных в файл строится на инъекции зависимостей: вместо реальной файловой системы передаётся протокол или mock-объект, который имитирует запись. Это позволяет проверить логику вызова, параметры и обработку ошибок без создания реальных файлов. Для Swift и iOS используется протокол FileWriting с методами записи, а в тестах - mock, реализующий этот протокол. Такой подход изолирует тест от состояния диска и делает его детерминированным.
Подробное объяснение
Основная проблема при тестировании записи в файл - зависимость от глобального состояния файловой системы. Реальный FileManager создаёт файлы, требует cleanup, зависит от прав доступа и может вести себя по-разному на разных устройствах. Поэтому unit test должен проверять не сам факт записи на диск, а то, что функция корректно вызывает API записи с правильными аргументами и обрабатывает результат.
Для этого вводится абстракция - протокол, который описывает операцию записи. Функция принимает этот протокол как зависимость (через инициализатор или параметр). В production-коде используется реальная реализация на FileManager, в тестах - mock, который фиксирует вызовы и возвращает заранее заданные результаты.
Такой подход даёт несколько преимуществ:
- тест не зависит от реальной файловой системы;
- можно проверить сценарии ошибок (например,
fileExistsвозвращаетfalse, запись бросает исключение); - тест выполняется быстро и не оставляет артефактов на диске;
- легко проверить, что функция вызывает запись ровно один раз и с правильными данными.
Для senior-уровня важно также обсудить trade-off: если тестировать только через mock, мы не проверяем реальную сериализацию данных. Поэтому для критичных форматов (например, JSON) добавляют отдельный integration test, который пишет во временную директорию и читает обратно.
На практике
На практике для iOS-проекта обычно создают протокол вида:
SWIFTprotocol FileWriting {func write(_ data: Data, to url: URL) throws}
Реальная реализация оборачивает FileManager. Функция, которую тестируем, принимает FileWriting через инициализатор или как параметр. В тестах используется mock, который хранит записанные данные и может имитировать ошибку.
Важно: не нужно использовать реальный FileManager даже с временными директориями в unit-тестах - это уже integration test. Для unit-теста достаточно mock. Если функция сама создаёт FileManager внутри, её нельзя протестировать без рефакторинга - это сигнал к изменению дизайна.
Также стоит проверять не только успешный сценарий, но и:
- что функция пробрасывает ошибку от
FileWritingнаверх; - что функция корректно обрабатывает
nilили пустые данные; - что URL формируется правильно (если функция строит путь сама).
Пример кода
SWIFTprotocol FileWriting {func write(_ data: Data, to url: URL) throws}final class FileManagerWriter: FileWriting {func write(_ data: Data, to url: URL) throws {try data.write(to: url)}}final class DataExporter {private let writer: FileWritinginit(writer: FileWriting) {self.writer = writer}func export(_ data: Data, to url: URL) throws {guard !data.isEmpty else {throw ExportError.emptyData}try writer.write(data, to: url)}enum ExportError: Error {case emptyData}}// Mock для тестовfinal class MockFileWriter: FileWriting {var writtenData: Data?var writtenURL: URL?var errorToThrow: Error?func write(_ data: Data, to url: URL) throws {if let error = errorToThrow {throw error}writtenData = datawrittenURL = url}}// Тестimport XCTestfinal class DataExporterTests: XCTestCase {func testExportWritesDataToURL() throws {let writer = MockFileWriter()let exporter = DataExporter(writer: writer)let data = Data("hello".utf8)let url = URL(fileURLWithPath: "/tmp/test.txt")try exporter.export(data, to: url)XCTAssertEqual(writer.writtenData, data)XCTAssertEqual(writer.writtenURL, url)}func testExportThrowsOnEmptyData() {let writer = MockFileWriter()let exporter = DataExporter(writer: writer)XCTAssertThrowsError(try exporter.export(Data(), to: URL(fileURLWithPath: "/tmp/test.txt")))XCTAssertNil(writer.writtenData)}func testExportPropagatesWriterError() {let writer = MockFileWriter()writer.errorToThrow = NSError(domain: "test", code: 1)let exporter = DataExporter(writer: writer)XCTAssertThrowsError(try exporter.export(Data("x".utf8), to: URL(fileURLWithPath: "/tmp/test.txt")))}}
Как отвечать на собеседовании
Начните с ключевой идеи: unit test не должен работать с реальной файловой системой. Объясните, что для этого вводится протокол и mock. Покажите, как это решает проблему детерминизма и изоляции. Упомяните, что проверяются три вещи: вызов с правильными аргументами, обработка ошибок, edge cases (пустые данные). Если спросят про реальную запись - скажите, что это integration test, и он должен использовать временную директорию с cleanup. Для senior-уровня добавьте рассуждение о том, что mock не проверяет сериализацию, поэтому для форматов вроде JSON нужен отдельный тест с реальным JSONEncoder.
Что проверяет интервьюер
Интервьюер оценивает:
- понимание разницы между unit и integration тестами;
- умение проектировать код с учётом тестируемости (dependency injection);
- знание паттерна mock/stub и его ограничений;
- способность рассуждать о trade-off между изоляцией и реальной проверкой;
- аккуратность в обработке ошибок и edge cases.
Для senior-позиции важно, чтобы кандидат не просто написал тест, а объяснил, почему такой дизайн правильный и какие альтернативы существуют (например, protocol witness, замыкания вместо протокола).
Типичные ошибки
- Использование реального
FileManagerв unit-тесте с временными файлами - это делает тест медленным и нестабильным. - Тестирование только успешного сценария, без проверки ошибок.
- Создание
FileManagerвнутри тестируемой функции - без инъекции зависимости тест невозможен без рефакторинга. - Проверка содержимого файла на диске в unit-тесте - это уже integration test.
- Игнорирование проверки, что запись вызвана с правильным URL или данными.
- Mock, который слишком сложный или повторяет логику реальной реализации - тест становится бесполезным.
> Похожие задачи по mobile
Что такое side table
Как реализовать функции setValue и getValue с поддержкой nil значений
Как реализовать функцию умножения числа на десять с обработкой символов
Что такое сайд таблица (side table) в контексте weak ссылок и как она работает
> ГОТОВЫ К СЛЕДУЮЩЕМУ СОБЕСЕДОВАНИЮ?
Запустите тренировочную сессию с ИИ и получите детальную обратную связь, чтобы увереннее проходить реальные интервью