Моки, стабы и фикстуры в Swift: как тестировать без боли

Онлайн-тренажер Python для начинающих

Изучайте Python легко и без перегрузки теорией. Решайте практические задачи с автоматической проверкой, получайте подсказки на русском языке и пишите код прямо в браузере — без необходимости что-либо устанавливать.

Начать курс

Тестирование в Swift: от теории к практике с моками и фикстурами

Представьте, что вы печете торт, а у вас закончилась мука. Вы не можете проверить, получится ли крем, пока не съездите в магазин. Примерно так же чувствует себя разработчик, который пишет юнит-тесты для кода, работающего с сетью, базой данных или сложными зависимостями. Вместо того чтобы ждать настоящего сервера или реальной базы, мы используем mock-объекты и фикстуры в Swift тестах — это наши "заменители муки", которые позволяют тестировать изолированные куски кода здесь и сейчас.

В этой статье мы не просто перечислим определения. Мы разберем, как правильно применять тестирование Swift на практике, когда использовать моки, а когда — стабы, и как фикстуры помогают не сойти с ума при подготовке данных. Поехали!

Что такое mock, stub и fixture? Разбираемся в терминах

Прежде чем писать код, давайте договоримся о понятиях. В мире юнит-тестирования iOS часто путают моки и стабы. Запомните простое правило:

  • Stub (заглушка) — объект, который возвращает заранее заданные данные. Он не проверяет, как его вызвали. Просто отдает ответ.
  • Mock (имитация) — объект, который не только возвращает данные, но и проверяет, были ли вызваны определенные методы и с какими параметрами. Это "шпион".
  • Fixture (фикстура) — заранее подготовленный набор данных (обычно JSON или структура), который используется в тестах для создания предсказуемого окружения.

В Swift тестировании мы часто комбинируем эти подходы. Например, загружаем JSON-фикстуру, парсим её, а затем подменяем сетевой слой на mock, который проверяет, что запрос был отправлен на правильный URL.

Пишем первый mock на Swift с помощью протоколов

Самый распространенный способ создать mock-объекты в Swift — использовать протоколы. В реальном проекте у вас есть сервис, который ходит в сеть. В тестах вы подменяете его на mock.

Вот пример абстракции:

// Протокол для сетевого слоя
protocol NetworkServiceProtocol {
    func fetchUsers(completion: @escaping (Result<[User], Error>) -> Void)
}

// Реальная реализация (ходит в сеть)
class RealNetworkService: NetworkServiceProtocol {
    func fetchUsers(completion: @escaping (Result<[User], Error>) -> Void) {
        // URLSession, Alamofire и т.д.
    }
}

// Mock для тестов
class MockNetworkService: NetworkServiceProtocol {
    var fetchUsersCalled = false
    var resultToReturn: Result<[User], Error> = .success([])

    func fetchUsers(completion: @escaping (Result<[User], Error>) -> Void) {
        fetchUsersCalled = true
        completion(resultToReturn)
    }
}

Теперь в тесте мы можем проверить, что метод был вызван:

func testViewModelFetchesUsers() {
    let mockService = MockNetworkService()
    mockService.resultToReturn = .success([User(id: 1, name: "Test")])

    let viewModel = UserViewModel(networkService: mockService)
    viewModel.loadUsers()

    XCTAssertTrue(mockService.fetchUsersCalled, "Метод fetchUsers должен быть вызван")
}

Обратите внимание: мы используем dependency injection Swift — передаем зависимость через инициализатор. Это ключевой паттерн для тестирования.

Фикстуры: как не писать 100500 строк в тестах

Когда вы тестируете парсинг JSON или работу с CoreData, вам нужны реальные данные. Но генерировать их каждый раз в коде — это ад. Тут на помощь приходят фикстуры в Swift тестах.

Создайте в тестовой папке файл users_fixture.json:

[
    {"id": 1, "name": "Alice", "email": "alice@example.com"},
    {"id": 2, "name": "Bob", "email": "bob@example.com"}
]

Теперь напишем хелпер для загрузки фикстур:

class FixtureLoader {
    static func loadJSON(from file: String) -> Data? {
        guard let path = Bundle.module.path(forResource: file, ofType: "json") else {
            return nil
        }
        return try? Data(contentsOf: URL(fileURLWithPath: path))
    }
}

И используем в тесте:

func testUserParsing() {
    guard let data = FixtureLoader.loadJSON(from: "users_fixture") else {
        XCTFail("Не удалось загрузить фикстуру")
        return
    }

    let decoder = JSONDecoder()
    let users = try? decoder.decode([User].self, from: data)

    XCTAssertEqual(users?.count, 2)
    XCTAssertEqual(users?.first?.name, "Alice")
}

Такой подход делает тестирование iOS приложений предсказуемым. Вы всегда знаете, какие данные придут, и не зависите от сети.

Продвинутые техники: Stub для URLSession

Иногда вы не хотите писать целый протокол для сетевого слоя. Или работаете с легаси-кодом. В таких случаях можно создать stub для URLSession в Swift.

Есть классный трюк: подменить URLProtocol. Это встроенный механизм, который позволяет перехватывать запросы на уровне системы.

class MockURLProtocol: URLProtocol {
    static var stubResponseData: Data?
    static var stubError: Error?

    override class func canInit(with request: URLRequest) -> Bool {
        return true
    }

    override class func canonicalRequest(for request: URLRequest) -> URLRequest {
        return request
    }

    override func startLoading() {
        if let error = MockURLProtocol.stubError {
            client?.urlProtocol(self, didFailWithError: error)
        } else if let data = MockURLProtocol.stubResponseData {
            client?.urlProtocol(self, didLoad: data)
            client?.urlProtocolDidFinishLoading(self)
        }
    }

    override func stopLoading() {}
}

В тесте настраиваем:

func testNetworkRequest() {
    let configuration = URLSessionConfiguration.ephemeral
    configuration.protocolClasses = [MockURLProtocol.self]
    let session = URLSession(configuration: configuration)

    MockURLProtocol.stubResponseData = """
    {"status": "ok"}
    """.data(using: .utf8)

    let expectation = XCTestExpectation(description: "Запрос выполнен")

    session.dataTask(with: URL(string: "https://api.example.com")!) { data, _, _ in
        XCTAssertNotNil(data)
        expectation.fulfill()
    }.resume()

    wait(for: [expectation], timeout: 1.0)
}

Это мощный инструмент для тестирования сетевых запросов Swift, особенно когда вы не хотите менять архитектуру проекта.

Подводные камни и best practices

Даже опытные разработчики допускают ошибки при работе с mock-объектами и фикстурами. Вот что стоит запомнить:

1. Не мокайте то, что не ваше

Не создавайте моки для Apple-классов (URLSession, UserDefaults), если это не критично.

Блоги

Рекомендаций по книгам