Чистое тестирование iOS-приложений

Это вторая часть продолжающегося списка статей. Настоятельно рекомендую прочитать первую часть, прежде чем продолжить.

Основы

Тестирование iOS-приложения, созданного с помощью SwiftUI и Combine с использованием чистой архитектуры, можно выполнить в несколько шагов. Во-первых, вам нужно будет создать тестовые примеры для вашего приложения. Это можно сделать с помощью платформы XCTest, входящей в состав Xcode.

После того, как вы создали свои тестовые примеры, вы можете использовать тип AnyPublisher для проверки вывода вашего приложения. Тип AnyPublisher — это издатель, который может создавать выходные данные любого типа, поэтому вы можете использовать его для проверки вывода Publisher вашего приложения.

Чтобы протестировать Publisher, который создает значения String, вы можете использовать метод XCTAssertEqual для сравнения выходных данных издателя с ожидаемыми выходными данными. Например:

В приведенном выше коде publisher проверяется путем подписки на него с использованием метода sink. Когда издатель создает значение, вызывается замыкание receiveValue, и выходные данные сравниваются с expectedOutput с помощью метода XCTAssertEqual. Тест завершится ошибкой, если результат не соответствует ожидаемому результату.

Вы можете использовать этот подход для тестирования любого Publisher, который производит значения String и сигнализирует о завершении или неудаче, используя тип Error. Это может быть полезным способом убедиться, что ваше приложение работает правильно и выдает ожидаемый результат.

Резюме

Это последний результат, который мы получили при построении нашей чистой архитектуры:

Чтобы протестировать это приложение, вы можете использовать следующий подход:

  1. Напишите модульные тесты для классов доменного уровня, включая класс MyViewModel. Эти тесты не должны зависеть от классов на уровнях представления или доступа к данным и должны проверять правильность работы бизнес-логики и обработки данных класса MyViewModel.
  2. Напишите интеграционные тесты для классов в слоях представления и предметной области. Эти тесты должны создавать экземпляры классов MyView и MyViewModel и проверять, правильно ли они работают вместе, чтобы отображать данные, полученные из уровня доступа к данным.
  3. Напишите интеграционные тесты для классов на уровне доступа к данным. Эти тесты должны создавать экземпляры реализаций протокола DataFetcher (например, NetworkDataFetcher и DatabaseDataFetcher) и проверять, могут ли они правильно извлекать данные из сети или локальной базы данных.

Тестирование

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

ViewModel

Например, модульный тест для класса MyViewModel может выглядеть так:

В предыдущем примере кода создаются тестовые наборы для проверки функциональности класса MyViewModel. Класс MyViewModel отвечает за выборку данных из DataFetcher и делает их доступными для представления.

Класс MyViewModelTests является подклассом XCTestCase и содержит единственный тестовый метод под названием testFetchData. Этот метод используется для проверки метода fetchData класса MyViewModel.

Метод testFetchData создает экземпляр MockDataFetcher и использует его для инициализации экземпляра MyViewModel. MockDataFetcher — это фиктивный объект, который используется вместо реального DataFetcher в тесте. MockDataFetcher имеет свойство fetchDataResult, которое используется для имитации вывода метода fetchData.

Затем метод testFetchData устанавливает для свойства fetchDataResult экземпляра MockDataFetcher определенное значение и вызывает метод fetchData для экземпляра MyViewModel. Это имитирует сценарий, в котором DataFetcher успешно извлекает данные и возвращает их в MyViewModel.

После вызова метода fetchData метод testFetchData использует метод XCTAssertEqual для сравнения свойства data объекта MyViewModel с ожидаемым значением. Если свойство data не соответствует ожидаемому значению, тест завершится ошибкой.

Метод testFetchData также использует XCTestExpectation для ожидания завершения теста перед завершением. Это необходимо, поскольку метод fetchData является асинхронным, и тест должен ждать, прежде чем завершит проверку вывода.

Таким образом, тестовый пример гарантирует, что MyViewModel может правильно получить данные из DataFetcher и сделать их доступными для представления. Это может помочь убедиться, что приложение работает правильно и выдает ожидаемый результат.

View-ViewModel

Точно так же интеграционный тест для классов MyView и MyViewModel может выглядеть так:

Определен класс MockDataFetcher, который является фиктивной реализацией класса DataFetcher. Этот фиктивный класс имеет свойство с именем fetchDataResult, которое используется для имитации результата операции fetchData().

Метод fetchData() класса MockDataFetcher определен для возврата экземпляра AnyPublisher<String, Error>. Это означает, что вызывающая сторона может подписаться на публикатора и асинхронно обработать результат операции fetchData().

В тестовом случае fetchDataResult устанавливается в строковое значение, а метод fetchData() вызывается для экземпляра MyViewModel. Результирующий экземпляр AnyPublisher<String, Error> не обрабатывается непосредственно в тестовом примере, но ожидается, что тестируемый экземпляр MyView подпишется на издателя и обработает результат операции fetchData().

Затем тестовый пример проверяет, что метка внутри экземпляра MyView имеет ожидаемый текст, который указывает, что операция fetchData() прошла успешно и экземпляр MyView смог правильно обработать результат операции.

Заключение

Основная цель этих двух статей — разделить слои.

Чтобы разделить слои с помощью чистой архитектуры, SwiftUI и Combine, выполните следующие действия:

  1. Определите различные уровни в приложении, такие как уровни представления, домена и доступа к данным.
  2. Вы можете использовать SPM для каждого из идентифицированных слоев. Например, создайте пакет «Представление» для уровня представления, пакет «Домен» для уровня предметной области и пакет «Доступ к данным» для уровня доступа к данным.
  3. Импортируйте в каждый пакет необходимые фреймворки, такие как SwiftUI и Combine для уровня представления и Core Data или Realm для уровня доступа к данным.
  4. Определите необходимые типы, протоколы и классы каждого пакета, придерживаясь принципов чистой архитектуры. Например, определите модели представлений, модели и варианты использования на уровне предметной области, а представления и контроллеры представлений на уровне представления.
  5. Настройте зависимости между пакетами, импортировав нужные пакеты друг в друга. Например, уровень представления может зависеть от уровня предметной области, а уровень предметной области может зависеть от уровня доступа к данным.
  6. Протестируйте приложение, чтобы убедиться, что слои правильно разделены, а зависимости настроены правильно.
  7. Постоянно проводите рефакторинг и улучшайте архитектуру по мере роста и развития приложения.

Спасибо за прочтение.