Давайте воспользуемся XCTest Framework в iOS для тестирования кодовой базы Combine.

Combine — это среда реактивного программирования, выпущенная Apple в iOS 13, которая позволяет нам работать с асинхронными потоками данных.

По словам Apple, Combine позволяет нам:

Настройте обработку асинхронных событий, комбинируя операторы обработки событий.

В этой статье мы рассмотрим, как выполнять модульное тестирование кода на основе Combine с использованием платформы XCTest Apple.

Для этого мы напишем модульные тесты для примера приложения, которое будет отображать список заданий. Основные классы, которые мы будем использовать:

Job: представляет объект задания. Наша модель.

JobsViewModel: Отвечает за всю логику презентации. Мы будем работать с двумя состояниями просмотра: Populated и Empty.

ПротоколJobClientProtocol будет служить фасадом для нашей сетевой логики.

В вашем приложении должен быть соответствующий ему тип JobClient, но мы не будем его реализовывать в этой статье (тем более, что он нам не нужен):

Примечание. Для простоты JobsViewModel будет иметь JobClientProtocol в качестве зависимости (в идеале мы хотели бы иметь дополнительный уровень, чтобы наша модель представления не взаимодействовала напрямую с сетевым кодом).

Что будет протестировано

Нашей основной целью было бы проверить, правильно ли настроены все состояния представления в модели представления в соответствии с выбранными заданиями.

Два состояния, которые мы будем тестировать, и их условия:

  1. Populated: Если у нас есть одно или несколько загруженных заданий.
  2. Empty: Если нет выбранных заданий для отображения.

Создание макетов

Мы начнем с издевательства над нашим JobClientProtocol. Это не должно быть сложной задачей, потому что наша модель представления зависит от абстракций (в данном случае от протокола), а не от конкретных типов. Таким образом, MockJobClient будет выглядеть так:

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

Примечание. Мы работаем с AnyPublisher, потому что эта форма стирания типов сохраняет абстракцию и, следовательно, скрывает базовую реализацию нашего сетевого клиента.

Определение наших сохраненных свойств

Сначала нам нужно будет объявить два хранимых свойства:

  1. viewModelToTest: Это будет экземпляр модели представления, который будет протестирован. Также известен как SUT (тестируемая система).
  2. mockJobClient: Экземпляр нашего фиктивного JobClientProtocol.

Нам нужно хранимое свойство для каждого из них, потому что мы будем напрямую изменять mockJobClient для имитации различных сценариев, которые нам нужны (MockJobClient— этокласс,поэтому он будет передан как ссылка в инициализаторе модели представления, и любое обновление или изменение, выполненное в экземпляре MockJobClient, повлияет на поведение модели представления).

Наконец, у нас будет экземпляр AnyCancellableколлекции, потому что мы присоединим подписчика к издателям, возвращенным mockJobClient, и этого подписчика необходимо сохранить; в противном случае он будет немедленно освобожден:

private var cancellables: Set<AnyCancellable> = []

Написание наших модульных тестов

Чтобы выполнять наши модульные тесты, нам нужно подписаться на издателя viewState в JobsViewModel. Для этого мы будем использовать метод sink, который будет прикреплять подписчика к viewState, используя поведение на основе замыкания:

viewModelToTest.$viewState.dropFirst().sink { state in
    // Evaluate state here
}.store(in: &cancellables)

Следующим шагом является имитация ответов сетевых клиентов. Мы можем легко сделать это, используя наш MockJobClient. Чтобы смоделировать пустой ответ, мы должны сделать:

mockJobClient.fetchJobsResult = Result.success([]).publisher.eraseToAnyPublisher()

И для имитации заполненного ответа:

let jobsToTest = [Job(id: "1", title: "title", description: "desc")]
mockJobClient.fetchJobsResult = Result.success(jobsToTest).publisher.eraseToAnyPublisher()

Последним шагом будет вызов метода loadJobs, который запустит процесс извлечения с помощью MockJobClient:

viewModelToTest.loadJobs()

Примечание. Важно вызвать метод loadJobs после предыдущих шагов (присоединение подписчика и имитация ответа сети); в противном случае наш viewState издатель не будет выдавать никакого значения, потому что к этому моменту еще не был подключен ни один подписчик (издатели Combine не выдают никакого значения, если к нему не подключены подписчики).

Чтобы убедиться, что мы получаем правильное состояние, мы будем использовать XCTestExpectation — класс, который Apple предоставляет нам для создания асинхронных тестов.

Окончательный тестовый класс будет выглядеть так:

Вот и все. Вы можете найти полный пример кода в этом репозитории: