Композиция и тестирование возможностей
Объединяйте редьюсеры и выполняйте исчерпывающее тестирование.
«Композиция и тестирование возможностей» — бесплатный урок Swift Academy на CoddyKit. Это урок 4 из 4. Ты можешь прочитать весь урок бесплатно ниже — а потом практиковать его прямо в браузере с встроенным редактором кода и ИИ-репетитором 24/7. Это часть пути обучения Swift Academy, и твой прогресс синхронизируется между веб-версией и приложением CoddyKit. Курс Swift Academy содержит 4 уроков всего.
Объединение функциональностей
Реальные приложения состоят из множества небольших функциональностей. TCA позволяет родительской функциональности встраивать дочерние, чтобы каждая сохраняла узкую специализацию, а затем объединять их в более крупное целое. Для этого используются Scope и встраивание состояния и действий дочерней функциональности.
Именно композиция дала TCA название «компонуемая архитектура».
import ComposableArchitecture
// A parent feature embeds child State and Action,
// then uses Scope to run the child's reducer.Встраивание состояния дочерней функциональности
State родительской функциональности хранит состояние дочерней как свойство. Перечисление Action родительской функциональности содержит вариант, оборачивающий действия дочерней. Такая вложенность отражает вложенность пользовательского интерфейса.
Здесь родительская функциональность содержит дочерний счётчик.
import ComposableArchitecture
@Reducer
struct AppFeature {
@ObservableState
struct State: Equatable {
var counter = CounterFeature.State()
}
enum Action {
case counter(CounterFeature.Action)
}
var body: some ReducerOf<Self> { EmptyReducer() }
}Настройка области дочерней функциональности
Scope запускает редьюсер дочерней функциональности для той части состояния и действия родительской, которая относится к дочерней. Сначала указываются путь к ключу состояния и путь к варианту действия, а затем редьюсер дочерней функциональности.
Так дочерняя функциональность подключается к циклу обновления родительской.
import ComposableArchitecture
var body: some ReducerOf<Self> {
Scope(state: \.counter, action: \.counter) {
CounterFeature()
}
Reduce { state, action in
// parent-specific logic here
return .none
}
}Объединение нескольких редьюсеров
В body функциональности можно перечислить несколько редьюсеров; TCA запускает их по порядку для каждого действия. Это позволяет разместить один или несколько Scope рядом с собственным Reduce родительской функциональности.
Здесь одновременно работают две дочерние функциональности и логика родительской.
import ComposableArchitecture
var body: some ReducerOf<Self> {
Scope(state: \.profile, action: \.profile) { ProfileFeature() }
Scope(state: \.settings, action: \.settings) { SettingsFeature() }
Reduce { state, action in
return .none
}
}Настройка store для дочерних представлений
На уровне представления дочернему представлению нужен специализированный store. Родительская функциональность получает его с помощью store.scope, выполняя переход к состоянию и действию дочерней функциональности.
Здесь родительское представление передаёт настроенный store представлению счётчика.
import ComposableArchitecture
import SwiftUI
struct AppView: View {
let store: StoreOf<AppFeature>
var body: some View {
CounterView(
store: store.scope(state: \.counter, action: \.counter)
)
}
}Знакомство с TestStore
TestStore позволяет точно проверять поведение функциональности. Вы отправляете действия и описываете, как должно измениться состояние; если результат отличается, тест завершается с ошибкой. Такая исчерпывающая проверка выявляет незаметные регрессии.
Его создают как обычный store, передавая начальное состояние и редьюсер.
import ComposableArchitecture
import XCTest
func testIncrement() async {
let store = await TestStore(initialState: CounterFeature.State()) {
CounterFeature()
}
}Проверка изменений состояния
Отправляя действие в TestStore, вы передаёте замыкание, описывающее ожидаемое изменение. Тест проходит только в том случае, если состояние изменилось именно так.
Здесь увеличение счётчика должно изменить его значение с 0 на 1.
import ComposableArchitecture
import XCTest
func testIncrement() async {
let store = await TestStore(initialState: CounterFeature.State()) {
CounterFeature()
}
await store.send(.incrementTapped) {
$0.count = 1
}
}Исчерпывающая проверка
По умолчанию TestStore работает в исчерпывающем режиме: необходимо учесть каждое изменение состояния и каждое действие, которое возвращает эффект. Непредусмотренные изменения приводят к сбою теста и не позволяют делать необоснованные утверждения.
Для целевых тестов этот режим можно отключить с помощью store.exhaustivity = .off.
import ComposableArchitecture
// Exhaustive (default): every change must be asserted.
// Relax when you only care about part of the behavior:
store.exhaustivity = .offПолучение действий от эффектов
Когда эффект отправляет действие обратно, тест должен получить его через receive и проверить результирующее состояние. Это подтверждает работу асинхронного потока от начала до конца.
Здесь перезагрузка запускает ответ, которого тест ожидает и который затем проверяет.
import ComposableArchitecture
import XCTest
func testReload() async {
let store = await TestStore(initialState: NumberFeature.State()) {
NumberFeature()
}
await store.send(.reload)
await store.receive(\.response) {
$0.value = 42
}
}Переопределение зависимостей в тестах
Чтобы сделать эффекты детерминированными, переопределяйте зависимости при создании TestStore. Передайте поддельный клиент или тестовые часы, чтобы результаты были предсказуемыми.
Здесь клиент чисел заменён клиентом, который всегда возвращает 42.
import ComposableArchitecture
import XCTest
func testReload() async {
let store = await TestStore(initialState: NumberFeature.State()) {
NumberFeature()
} withDependencies: {
$0.numberClient.fetch = { 42 }
}
await store.send(.reload)
await store.receive(\.response) { $0.value = 42 }
}Проверка объединённых функциональностей
Поскольку при композиции состояния и действия вкладываются друг в друга, родительскую функциональность проверяют, отправляя действия дочерней, обёрнутые в вариант действия родительской. Те же исчерпывающие проверки подтверждают работу всего дерева.
Здесь родительская функциональность получает увеличение счётчика через свою область.
import ComposableArchitecture
import XCTest
func testChildInParent() async {
let store = await TestStore(initialState: AppFeature.State()) {
AppFeature()
}
await store.send(.counter(.incrementTapped)) {
$0.counter.count = 1
}
}Быстрая проверка: поведение TestStore
Вспомните, что именно требуется от ваших проверок при использовании TestStore по умолчанию.
Итоги: объединение и проверка
Вы освоили инструменты TCA:
Scopeзапускает редьюсер дочерней функциональности для части состояния и действия родительской, а несколько редьюсеров можно объединить в одномbody.store.scopeпередаёт дочернему представлению собственный специализированный store.TestStoreпроверяет точные изменения состояния при отправке действий черезsend.- По умолчанию он работает в исчерпывающем режиме и использует
receiveдля действий, вызванных эффектами. - Для детерминированных тестов переопределяйте зависимости с помощью
withDependencies.
Теперь вы умеете создавать, объединять и тщательно проверять функциональности с помощью TCA.
import ComposableArchitecture
// Recap: Scope to compose, TestStore to verify.
// Exhaustive assertions + injected fakes = confidence.Часто задаваемые вопросы
Урок «Композиция и тестирование возможностей» бесплатный?
Да — полный текст урока «Композиция и тестирование возможностей» бесплатно доступен здесь в веб-версии. Чтобы практиковать его интерактивно (встроенный редактор кода и ИИ-репетитор 24/7) и разблокировать остальной курс Swift Academy, подпишись на CoddyKit PRO. Курс Swift Academy содержит 4 уроков всего.
Чему я научусь в уроке «Композиция и тестирование возможностей»?
Объединяйте редьюсеры и выполняйте исчерпывающее тестирование. Ты практикуешь Swift Academy с помощью реального кода, который запускаешь прямо в браузере, и ИИ-репетитор 24/7 отвечает на твои вопросы во время урока.
Нужен ли мне опыт, чтобы начать Swift Academy?
Предыдущий опыт не требуется. Swift Academy на CoddyKit структурирован для всех уровней — от новичков до продвинутых, поэтому ты можешь начать отсюда или с самого начала и учиться в своем темпе. Это урок 4 из 4.
Сколько времени занимает урок «Композиция и тестирование возможностей»?
Большинство уроков CoddyKit занимают около 5–10 минут. Каждый из них компактный и интерактивный, поэтому ты постоянно делаешь прогресс и продолжаешь с того же места в веб-версии и приложении.
Можно ли писать и запускать код в этом уроке Swift Academy?
Да. Каждый урок Swift Academy включает встроенный редактор кода, поэтому ты пишешь и запускаешь реальный код прямо в браузере и получаешь моментальную обратную связь от AI — локальная установка не требуется.
Все уроки этого курса
- Состояние, действие и редьюсер
- Хранилище и интеграция со SwiftUI
- Эффекты и зависимости
- Композиция и тестирование возможностей