Давайте воспользуемся XCTest Framework в iOS для тестирования кодовой базы Combine.
Combine — это среда реактивного программирования, выпущенная Apple в iOS 13, которая позволяет нам работать с асинхронными потоками данных.
По словам Apple, Combine позволяет нам:
Настройте обработку асинхронных событий, комбинируя операторы обработки событий.
В этой статье мы рассмотрим, как выполнять модульное тестирование кода на основе Combine с использованием платформы XCTest Apple.
Для этого мы напишем модульные тесты для примера приложения, которое будет отображать список заданий. Основные классы, которые мы будем использовать:
Job
: представляет объект задания. Наша модель.
JobsViewModel
: Отвечает за всю логику презентации. Мы будем работать с двумя состояниями просмотра: Populated
и Empty
.
ПротоколJobClientProtocol
будет служить фасадом для нашей сетевой логики.
В вашем приложении должен быть соответствующий ему тип JobClient
, но мы не будем его реализовывать в этой статье (тем более, что он нам не нужен):
Примечание. Для простоты JobsViewModel
будет иметь JobClientProtocol
в качестве зависимости (в идеале мы хотели бы иметь дополнительный уровень, чтобы наша модель представления не взаимодействовала напрямую с сетевым кодом).
Что будет протестировано
Нашей основной целью было бы проверить, правильно ли настроены все состояния представления в модели представления в соответствии с выбранными заданиями.
Два состояния, которые мы будем тестировать, и их условия:
Populated
: Если у нас есть одно или несколько загруженных заданий.Empty
: Если нет выбранных заданий для отображения.
Создание макетов
Мы начнем с издевательства над нашим JobClientProtocol
. Это не должно быть сложной задачей, потому что наша модель представления зависит от абстракций (в данном случае от протокола), а не от конкретных типов. Таким образом, MockJobClient
будет выглядеть так:
Это позволит нам контролировать поведение сетевого клиента так, как мы хотим, давая нам большую гибкость для написания наших модульных тестов.
Примечание. Мы работаем с AnyPublisher
, потому что эта форма стирания типов сохраняет абстракцию и, следовательно, скрывает базовую реализацию нашего сетевого клиента.
Определение наших сохраненных свойств
Сначала нам нужно будет объявить два хранимых свойства:
viewModelToTest
: Это будет экземпляр модели представления, который будет протестирован. Также известен как SUT (тестируемая система).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 предоставляет нам для создания асинхронных тестов.
Окончательный тестовый класс будет выглядеть так:
Вот и все. Вы можете найти полный пример кода в этом репозитории: