> Как реализовать 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-проекта обычно создают протокол вида:

SWIFT
protocol FileWriting {
func write(_ data: Data, to url: URL) throws
}

Реальная реализация оборачивает FileManager. Функция, которую тестируем, принимает FileWriting через инициализатор или как параметр. В тестах используется mock, который хранит записанные данные и может имитировать ошибку.

Важно: не нужно использовать реальный FileManager даже с временными директориями в unit-тестах - это уже integration test. Для unit-теста достаточно mock. Если функция сама создаёт FileManager внутри, её нельзя протестировать без рефакторинга - это сигнал к изменению дизайна.

Также стоит проверять не только успешный сценарий, но и:

  • что функция пробрасывает ошибку от FileWriting наверх;
  • что функция корректно обрабатывает nil или пустые данные;
  • что URL формируется правильно (если функция строит путь сама).

Пример кода

SWIFT
protocol 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: FileWriting
init(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 = data
writtenURL = url
}
}
// Тест
import XCTest
final 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, который слишком сложный или повторяет логику реальной реализации - тест становится бесполезным.

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

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