Тестирование того же графа, который вы запускаете в продакшне¶
1 сентября 2026
Самое опасное различие между продакшн-кодом и тестовым кодом часто не в данных. Оно в соединении.
Сервис может пройти изолированный тест, в котором его конструктор вызывается вручную, два мока подаются в том порядке, который ожидает автор теста, а конфигурация представлена удобной константой. Развёрнутое приложение делает что-то другое. Контейнер внедрения зависимостей выбирает реализацию, между слоями сидят сгенерированные адаптеры, конфигурация маппится из внешних значений, ресурсы инициализируются в порядке зависимостей, а колбэки жизненного цикла решают, когда компонент готов и когда его безопасно останавливать. Тест доказывает класс. Он не доказывает, что класс принадлежит приложению, которое будет реально запущено.
Фреймворк Kora подходит к этой проблеме через свой граф приложения. Граф, построенный из @KoraApp, компонентов, модулей, методов-фабрик, тегов и зависимостей конструктора, — это не только механизм
продакшн-загрузки. JUnit-расширение может взять это определение приложения, выбрать часть, нужную тесту, заменить выбранные узлы, инициализировать результат и внедрить управляемые графом компоненты в
тест. Это делает продакшн-граф отправной точкой для компонентного и интеграционного тестирования, а не чем-то, что воссоздаётся независимо в тестовом коде.
В этом архитектурный смысл обещания Kora о простом и быстром тестировании. «Простое» не значит, что каждый тест тривиален, а «быстрое» не значит замену каждой зависимости моком. Это значит, что фреймворк даёт тестам прямой способ переиспользовать структуру приложения, платя только за ту часть этой структуры, которая важна для текущего вопроса.
Что на самом деле означает «тот же граф»¶
Фразу нужно уточнить. Компонентный тест не обязательно запускает полный процесс, не привязывает каждый серверный порт, не подключает каждый клиент и не запускает каждого фонового потребителя. Он также не переиспользует те же экземпляры объектов, что и продакшн-процесс. Он использует тот же сгенерированный граф приложения как источник истины: те же определения компонентов, рёбра зависимостей, теги, сгенерированные реализации и контракты жизненного цикла. Затем тестовое расширение выводит из этого определения тестовый граф.
Это различие важно. Ценность не в том, что компонентный тест становится миниатюрным развёртыванием. Ценность в том, что он перестаёт изобретать несвязанную вселенную зависимостей.
Рассмотрим путь оформления заказа:
HTTP-сервер
|
CheckoutController
|
CheckoutService
/ | \
OrderRepository PaymentGateway AuditPublisher
| | |
JDBC pool HTTP client Kafka producer
Тест, собранный вручную, может инстанцировать CheckoutService напрямую. Такой тест может быть полезен для алгоритма, но он не устанавливает, что Kora может найти CheckoutService, разрешить нужный
PaymentGateway, сохранить правильный @Tag, создать сгенерированную реализацию репозитория или предоставить типизированную конфигурацию, ожидаемую фабрикой клиента.
Компонентный тест Kora начинается с графа, который содержит эти факты. Если он запрашивает CheckoutService, расширение прослеживает его транзитивные зависимости. Если PaymentGateway и
AuditPublisher заменяются моками, реальные ветви клиента и Kafka больше не нужно инициализировать. Если OrderRepository остаётся реальным, его ветвь остаётся. Результат — частичное приложение, чьи
сохранённые отношения всё ещё берутся из продакшн-определения приложения.
Поэтому утверждение следует читать так: тестируйте ту же модель графа, которую запускает продакшн, суженную и намеренно изменённую под тестовую границу.
Продакшн-граф — это тестируемая архитектура¶
Kora обнаруживает и валидирует контейнер зависимостей приложения на этапе компиляции. Интерфейс, аннотированный @KoraApp, определяет границу приложения и подключает модули фреймворка или приложения.
Классы @Component и методы-фабрики предоставляют узлы. Параметры конструктора и параметры фабрик создают рёбра. @Root отмечает точки входа, которые должны существовать при запуске приложения. Всё,
что недостижимо из корня, удаляется.
Во время выполнения сгенерированный код материализует эту уже разрешённую структуру. Компоненты — синглтоны в пределах одного экземпляра графа. Они инициализируются с нетерпением в порядке
зависимостей, причём независимые ветви инициализируются параллельно, где это возможно. Когда граф освобождается, компоненты закрываются в обратном порядке зависимостей. Компоненты, реализующие
Lifecycle, значения, обёрнутые в LifecycleWrapper, и ресурсы AutoCloseable участвуют в этом процессе.
Эти свойства не случайны для тестирования. Они объясняют, почему граф — более сильная тестовая граница, чем набор вручную сконструированных объектов. Ребро зависимости утверждает и то, как компонент конструируется, и то, что должно быть готово до него. Замена меняет один узел в той же позиции структуры. Срез графа сохраняет вышестоящий путь, необходимый для построения запрошенного компонента. Инициализация графа отрабатывает поведение жизненного цикла до начала тестового метода, а освобождение графа — очистку после него.
Валидация времени компиляции также меняет то, когда появляются сбои. Отсутствующие компоненты, неоднозначные привязки, циклы, несовпадения тегов и недостижимые корни — это проблемы определения приложения, поэтому они могут остановить сборку, а не ждать запуска тестового контекста или развёрнутого процесса. Тестовый набор тогда сосредотачивается на поведении, граничных подстановках, совместимости инфраструктуры и эффектах жизненного цикла, а не на повторном обнаружении базовых ошибок соединения во время выполнения.
Тестовый граф начинается с вопроса¶
Поддержка JUnit в Kora живёт в io.koraframework:test-junit5. Тест выбирает приложение с @KoraAppTest и компоненты с @TestComponent.
@KoraAppTest(Application.class) сообщает расширению, какой сгенерированный граф приложения использовать. @TestComponent выполняет две работы: он запрашивает внедрение компонента и делает этот
компонент корнем ограниченного тестового графа. Kora сохраняет CheckoutService и всё, что нужно для его конструирования. Несвязанные контроллеры, планировщики, потребители и серверы не
инициализируются просто потому, что существуют в приложении.
Это центральный механизм скорости. Тест набирает скорость не отказом от внедрения зависимостей; он набирает скорость, используя достижимость графа, чтобы избежать нерелевантной работы. Небольшой срез сервиса может запуститься быстро, даже когда полное приложение содержит дорогие инфраструктурные ветви.
Компоненты можно внедрять в поля, параметры конструктора или параметры тестового метода. Внедрение в поля — самый гибкий выбор, когда тест также меняет конфигурацию или программно модифицирует граф,
потому что эти модификации должны читаться из тестового экземпляра до построения графа. Внедрение через конструктор полезно для неизменяемых тестовых полей, но его нельзя сочетать с
KoraAppTestConfigModifier или KoraAppTestGraphModifier. Параметры метода полезны, когда зависимость принадлежит одному сценарию, а не всему тестовому классу. Тегированные компоненты нужно
запрашивать с соответствующим @Tag; точка внедрения без тега сопоставляется с компонентом без тега, а не с любым компонентом того же сырого типа.
Атрибуты components и modules у @KoraAppTest покрывают узлы, которые нужно сохранить, даже если ни одно поле или параметр не запрашивает их напрямую. Они не превращают произвольные, несвязанные
модули в часть продакшн-приложения. Модуль, перечисленный в аннотации, уже должен принадлежать тестируемому графу. Атрибуты влияют на ограниченный тестовый граф; это не вторая система обнаружения
модулей.
Если тесту нужен широкий осмотр вместо узкого внедрённого компонента, он может внедрить KoraAppGraph. Этот объект умеет находить компоненты по типу и тегу, причём для параметризованных типов
используется TypeRef. Прямой доступ к графу ценен для инфраструктурных утверждений и диагностических тестов, но рутинные поведенческие тесты яснее, когда их граница графа видна в типизированных
полях или параметрах @TestComponent.
Замена происходит внутри графа¶
Сфокусированным тестам нужны контролируемые границы. Kora рассматривает мок как узел-замену графа, а не просто как поле, принадлежащее тестовому классу.
@KoraAppTest(Application.class)
class CheckoutServiceTest {
@Mock
@TestComponent
private PaymentGateway paymentGateway;
@Mock
@TestComponent
private AuditPublisher auditPublisher;
@TestComponent
private CheckoutService checkoutService;
@Test
void declinesAnOrderWhenPaymentIsRejected() {
when(paymentGateway.authorize(any()))
.thenReturn(PaymentResult.declined("insufficient-funds"));
var result = checkoutService.checkout(request());
assertEquals(OrderStatus.DECLINED, result.status());
verify(auditPublisher).paymentDeclined(result.id());
}
}
@KoraAppTest(Application::class)
class CheckoutServiceTest {
@MockK
@TestComponent
lateinit var paymentGateway: PaymentGateway
@MockK
@TestComponent
lateinit var auditPublisher: AuditPublisher
@TestComponent
lateinit var checkoutService: CheckoutService
@Test
fun declinesAnOrderWhenPaymentIsRejected() {
every { paymentGateway.authorize(any()) } returns
PaymentResult.declined("insufficient-funds")
val result = checkoutService.checkout(request())
assertEquals(OrderStatus.DECLINED, result.status)
verify { auditPublisher.paymentDeclined(result.id) }
}
}
Тот же экземпляр мока внедряется и в тестовое поле, и в каждый сохранённый компонент графа, зависящий от PaymentGateway. CheckoutService остаётся реальным продакшн-компонентом, созданным Kora, но
его конструктор получает замену. Зависимости, используемые только для создания реального шлюза, становятся недостижимыми и могут быть опущены. Тест контролирует границу, сохраняя при этом
конструирование тестируемого компонента.
Java-тесты могут использовать @Mock и @Spy из Mockito; Kotlin-тесты могут использовать @MockK и @SpyK из MockK. Расширение Kora владеет созданием, вставкой, сбросом и очисткой этих моков,
поэтому отдельные MockitoExtension или MockKExtension не следует добавлять в тот же тест. Два расширения, управляющих одними и теми же полями, сделали бы идентичность и жизненный цикл моков
неоднозначными. Синхронные контракты сервисов, репозиториев и HTTP в Kora также делают стаббинг прямым: Java возвращает обычные значения, а Kotlin обычно использует every, а не специфичный для
корутин стаббинг.
Моки лучше всего использовать там, где детерминизм и фокус важнее реалистичности протокола. Они хорошо работают для путей отклонения, решений о таймауте, условной публикации, исходов авторизации и другой логики выше внешней границы. Они не могут доказать синтаксис SQL, маппинг строк, совместимость миграций, HTTP-сериализацию, семантику брокера, конфигурацию TLS или упаковку контейнера. Граф делает замену лёгкой; он не делает замену эквивалентной реальной системе.
Не каждая замена должна быть объектом фреймворка мокирования. Детерминированный фейк может выражать поведение яснее и поддерживать состояние, полезное для утверждения. Программная модификация графа поддерживает такой случай.
@KoraAppTest(Application.class)
class CheckoutWithFakeClockTest implements KoraAppTestGraphModifier {
@TestComponent
private CheckoutService checkoutService;
@Override
public KoraGraphModification graph() {
return KoraGraphModification.create()
.replaceComponent(
Clock.class,
() -> Clock.fixed(
Instant.parse("2026-04-12T10:15:30Z"),
ZoneOffset.UTC
)
);
}
}
@KoraAppTest(Application::class)
class CheckoutWithFakeClockTest : KoraAppTestGraphModifier {
@TestComponent
lateinit var checkoutService: CheckoutService
override fun graph(): KoraGraphModification {
return KoraGraphModification.create()
.replaceComponent(Clock::class.java) {
Clock.fixed(
Instant.parse("2026-04-12T10:15:30Z"),
ZoneOffset.UTC
)
}
}
}
KoraAppTestGraphModifier возвращает KoraGraphModification. Он может добавить компонент, заменить существующий компонент или программно создать мок. Тип и тег идентифицируют цель, что важно, когда
несколько узлов графа разделяют интерфейс.
У фабрик замен есть полезное архитектурное следствие. Замена, предоставленная независимо, может позволить обрезать ветвь зависимостей исходного узла. Замена, построенная с Function<KoraAppGraph, T>,
зависит от инициализированных значений графа, поэтому эти значения должны остаться. Такой выбор должен быть намеренным: автономный фейк изолирует границу; граф-осведомлённый декоратор сохраняет часть
реальной границы и меняет поведение вокруг неё. Фабрика замены не должна запрашивать тот же узел, который она заменяет, иначе у инициализации не будет валидного базового случая.
Конфигурация — это зависимость, а не тестовый каркас¶
Конфигурация часто заставляет иначе реалистичные тесты дрейфовать. Продакшн читает HOCON или YAML, маппит секции в типизированные интерфейсы, разрешает подстановки и пропускает получившиеся объекты через граф. Тест, который обходит этот путь, вручную конструируя объект конфигурации, может пропустить неверные пути, отсутствующие значения, ошибки маппинга и взаимодействия между конфигурацией и созданием компонентов.
Kora позволяет тесту изменить конфигурацию до построения графа. Тест реализует KoraAppTestConfigModifier и возвращает KoraConfigModification.
@KoraAppTest(Application.class)
class CheckoutConfigurationTest implements KoraAppTestConfigModifier {
@TestComponent
private CheckoutService checkoutService;
@Override
public KoraConfigModification config() {
return KoraConfigModification.ofString("""
checkout {
maximumOrderValue = 1000
fraudChecksEnabled = true
}
""");
}
}
@KoraAppTest(Application::class)
class CheckoutConfigurationTest : KoraAppTestConfigModifier {
@TestComponent
lateinit var checkoutService: CheckoutService
override fun config(): KoraConfigModification {
return KoraConfigModification.ofString(
"""
checkout {
maximumOrderValue = 1000
fraudChecksEnabled = true
}
""".trimIndent()
)
}
}
Встроенная строка полезна, когда сценарий должен объявить всю свою релевантную конфигурацию рядом с тестом. ofResourceFile(...) лучше для более крупного переиспользуемого тестового профиля.
ofSystemProperty(...), withSystemProperty(...) и withSystemProperties(...) полезны, когда обычный файл приложения должен оставаться в силе или когда значения известны только после запуска
тестовой инфраструктуры. Применённые таким образом системные свойства существуют, пока строится граф, и восстанавливаются после.
Время — решающая часть: config() выполняется до создания компонентов. Типизированные мапперы конфигурации, фабрики клиентов, пулы, миграции и любые другие зависящие от конфигурации компоненты видят
тестовые значения во время обычной инициализации графа. Конфигурация поэтому — один из контролируемых входов графа, а не мутация, применённая после того, как приложение уже запустилось.
Этот паттерн особенно хорошо подходит Testcontainers. Контейнер базы данных выбирает хост-порт во время выполнения. Тест может поместить ${POSTGRES_JDBC_URL}, ${POSTGRES_USER} и ${POSTGRES_PASS}
во встроенный фрагмент HOCON и заполнить их из геттеров контейнера через withSystemProperty(...). Затем граф создаёт реальный пул JDBC и репозиторий против этой одноразовой базы данных.
Продакшн-конфигурация остаётся неизменной, но отрабатываются продакшн-маппинг и путь фабрики.
Переопределения конфигурации должны оставаться достаточно узкими, чтобы вскрывать случайные зависимости. Копирование всей продакшн-конфигурации в каждый тест создаёт ещё одно дерево конфигурации, которое может дрейфовать. Предпочитайте небольшой полный фрагмент для сохранённого среза графа или сохраняйте конфигурацию приложения по умолчанию и переопределяйте только значения времени выполнения. Если якобы сфокусированный компонентный тест вдруг требует несвязанную конфигурацию Kafka, HTTP-сервера или телеметрии, это свидетельство того, что выбранный срез графа или граница компонента заслуживает проверки.
Частичное приложение — это больше, чем меньший контекст¶
Фраза «частичное приложение» может звучать как деталь оптимизации. Полезнее рассматривать её как явную архитектуру тестирования.
Выбранные узлы @TestComponent — тестовые корни. Kora вычисляет их замыкание зависимостей после применения замен. Получившийся граф точно утверждает, каким продакшн-отношениям доверяет сценарий и
какие границы он контролирует. Это даёт тесту содержательный структурный охват.
Для чистого компонентного теста сохранённый граф может содержать реальный сервис, типизированную конфигурацию, валидатор и два замоканных порта. Для межкомпонентного теста он может сохранить контроллер, сервис, маппер и репозиторий в памяти. Для интеграционного теста он может сохранить реальные репозитории, миграции, пул JDBC и сервис, опуская HTTP-сервер и потребителя сообщений. Каждый тест использует один и тот же механизм выбора, но отвечает на другой вопрос.
Этот дизайн избегает двух распространённых крайностей. Первая — изолированный тест, который вручную воссоздаёт каждый конструктор и ничего не доказывает о соединении фреймворка. Вторая — полно-контекстный тест, который запускает каждый сервер, потребителя, планировщик и внешний клиент для каждой маленькой проверки поведения. Срез графа занимает полезную середину: продакшн-топология с намеренной, наблюдаемой границей.
Есть случаи, когда продакшн-графу нужно тестовое расширение, а не замена. Интеграционному тесту может понадобиться репозиторий очистки, загрузчик фикстур или административная операция, которой не
должно быть в продакшн-коде. Тестовый набор исходников может объявить отдельный @KoraApp, который расширяет основное приложение и добавляет эти компоненты. Затем Kora генерирует тестовый граф
приложения.
Этот приём сохраняет продакшн-определение приложения и добавляет возможность в тестовом охвате. Поскольку сам тестовый граф генерируется, процессор аннотаций должен быть подключён к тестовому набору исходников. Основное приложение также должно генерировать подмодуль, необходимый для наследования. Тестовые компоненты, которые никто не потребляет, должны быть достижимы из тестового корня, иначе они будут обрезаны, как любой другой недостижимый узел графа.
Расширенный TestApplication следует использовать для подлинных возможностей графа, а не как свалку для произвольных тестовых помощников. Object mothers, утилиты утверждений и генераторы случайных
данных не должны становиться компонентами приложения. Репозитории очистки, встроенные адаптеры протоколов и фикстуры, чьи собственные зависимости и жизненный цикл важны, — более сильные кандидаты.
Жизненный цикл — часть поверхности утверждений¶
У продакшн-ресурсов есть начала и концы. Пул базы данных открывает и закрывает соединения. Сервер привязывает и освобождает порт. Потребитель вступает в группу и покидает её. Фоновый работник запускается и должен перестать принимать работу до того, как исчезнут его зависимости. Тестирование графа без уважения к жизненному циклу воссоздало бы то же несоответствие, которое граф-основанное тестирование призвано предотвратить.
До выполнения тестового метода расширение Kora строит выбранный граф, применяет модификации конфигурации и графа, создаёт узлы и инициализирует компоненты, осведомлённые о жизненном цикле. Внедрённый компонент готов, а не просто сконструирован. После завершения тестового контекста расширение освобождает управляемые графом ресурсы. Порядок зависимостей управляет инициализацией; обратный порядок зависимостей управляет освобождением. Независимые ветви могут инициализироваться параллельно — именно потому, что отсутствие рёбер зависимостей делает параллельную работу безопасной.
JUnit использует TestInstance.Lifecycle.PER_METHOD по умолчанию. При таком жизненном цикле Kora создаёт и освобождает свежий тестовый граф для каждого тестового метода. Это даёт сильную изоляцию
состояния и делает баги жизненного цикла видимыми, но может быть дорого, когда граф владеет пулом базы данных, движком миграций, клиентом брокера или другим тяжёлым ресурсом.
@TestInstance(TestInstance.Lifecycle.PER_CLASS) создаёт один граф для класса и освобождает его после завершения всех методов. Это может заметно снизить стоимость интеграционного теста, но меняет
обязательства теста. Изменяемое состояние компонентов разделяется. Строки базы данных и кеши нуждаются в явной очистке. Расширение Kora сбрасывает моки и шпионы перед каждым методом, поэтому стаббинг
нужно устанавливать заново для каждого сценария. Внедрение моков на уровне метода ограничено, потому что мок уровня метода не может безопасно населять граф уровня класса.
Выбор жизненного цикла должен следовать владению ресурсами, а не привычке. Используйте PER_METHOD, когда чистое состояние графа дёшево, а изоляция ценна. Используйте PER_CLASS, когда инициализация
доминирует над временем выполнения и тест может обеспечить детерминированную очистку. Статическая база данных Testcontainers в сочетании с графом Kora уровня метода разделяет внешний процесс, но
воссоздаёт соединения, принадлежащие графу; граф уровня класса разделяет и то и другое. Это разные компромиссы изоляции и производительности, и тест должен делать решение видимым.
Тестирование жизненного цикла также выигрывает от реальных, а не замоканных границ. Если корректность компонента зависит от того, что init() регистрирует слушателя или release() дренирует работу,
замена его моком удаляет поведение под проверкой. Держите этот компонент реальным, заменяйте при необходимости только его внешние побочные эффекты и позволяйте графу вызывать его методы жизненного
цикла. И наоборот, сфокусированный сервисный тест должен мокать тяжёлую границу жизненного цикла, когда запуск ресурсов нерелевантен вопросу. Срез графа делает оба выбора возможными без изменения
продакшн-кода.
От уверенности в компоненте к уверенности в продакшне¶
Переиспользование графа не схлопывает все уровни тестирования в один. Оно даёт им общий структурный фундамент.
Компонентный тест спрашивает, корректно ли ведёт себя реальный управляемый графом компонент с контролируемыми зависимостями. Он быстр, детерминирован и хорошо подходит для бизнес-ветвлений. Интеграционный тест спрашивает, работает ли сохранённая часть графа с реальной инфраструктурой. Он может проверить сгенерированные репозитории, SQL, миграции, маппинг, конфигурацию и жизненный цикл ресурсов, не платя за несвязанные точки входа приложения. Чёрно-ящичный тест запускает упакованный сервис и общается через его публичный API. Он проверяет маршрутизацию, сериализацию, полную конфигурацию, флаги процесса, образ контейнера, нативные библиотеки, сеть, готовность и поведение итогового артефакта.
Никакой компонентный граф-тест не может доказать, что образ контейнера содержит правильные файлы или что его entrypoint передаёт намеченные опции JVM. Никакой мок не может доказать диалект базы данных. Никакой чёрно-ящичный набор не должен нести каждую комбинаторную ветвь логики сервиса через дорогую инфраструктуру. Уверенность приходит от размещения каждого вопроса на самой узкой границе, которая может честно на него ответить.
Граф Kora помогает слоям согласовываться. Компонентный тест использует продакшн-определение компонента. Интеграционный тест сохраняет это определение и меняет замоканный порт на реальную реализацию на основе контейнера. Чёрно-ящичный тест запускает собранный артефакт, построенный из того же объявления приложения. Движение наружу добавляет поверхность времени выполнения, а не заменяет одну модель соединения другой.
Стоящие избегания режимы отказа¶
Граф-основанное тестирование наиболее ценно, когда тестовая граница остаётся намеренной. Несколько практик ослабляют её.
Запрос всего графа для каждого теста скрывает случайные зависимости и жертвует скоростью, полученной от достижимости. Запрашивайте компонент, представляющий поведение под тестом, затем добавляйте другие корни только тогда, когда сценарий действительно их наблюдает.
Мокирование каждого параметра конструктора превращает граф в дорогой способ воспроизвести изолированный юнит-тест. Держите дешёвые, детерминированные компоненты реальными. Заменяйте границы, чьи стоимость, недетерминизм или побочные эффекты мешают вопросу.
Использование моков для представления протоколов создаёт ложную уверенность. Замоканный репозиторий может вести ветви сервиса, но не может проверить запрос. Замоканный HTTP-клиент может симулировать обработку статусов, но не может доказать кодирование запроса. Продвигайте такие вопросы в интеграционные тесты с реальной инфраструктурой или реалистичным сервером протокола.
Переопределение конфигурации после инициализации слишком поздно. Используйте KoraAppTestConfigModifier, чтобы зависящие от конфигурации узлы строились из значений сценария. Избегайте внедрения через
конструктор, когда тест реализует модификаторы конфигурации или графа, потому что расширению нужен тестовый экземпляр до того, как оно сможет построить этот граф.
Игнорирование тегов может заменить или запросить не тот компонент, когда несколько реализаций разделяют тип. Рассматривайте тип плюс тег как идентичность компонента. Используйте TypeRef для
параметризованных узлов, а не полагайтесь на стёртый сырой класс.
Разделение графа без сброса всего остального изменяемого состояния делает тесты зависимыми от порядка. PER_CLASS — это инструмент производительности, а не автоматическая изоляция. Явно очищайте
состояние базы данных, кеши, фейковые серверы и буферы, принадлежащие приложению.
Наконец, трактовка «того же графа» как «среды, эквивалентной продакшну» преувеличивает то, что доказывает тест. Срезанный и модифицированный граф валидирует структуру приложения и выбранное поведение времени выполнения. Только чёрно-ящичный запуск готового к продакшну образа может покрыть полную границу упаковки и процесса.
Практическая архитектура тестового набора Kora¶
Продуктивный набор начинается с множества маленьких срезов графа вокруг бизнес-компонентов. Эти тесты запрашивают реальные сервисы, валидаторы, мапперы и объекты политик, заменяя при этом удалённые
системы и дорогую инфраструктуру. Они работают с PER_METHOD, если стоимость жизненного цикла не демонстрирует причину разделять граф.
Вокруг инфраструктурных границ более узкие интеграционные классы сохраняют реальные сгенерированные репозитории, клиенты, миграции и типизированную конфигурацию. Testcontainers поставляет одноразовые
PostgreSQL, Kafka, Redis или совместимые сервисы. Адреса времени выполнения попадают в граф через KoraConfigModification, поэтому фабрики и жизненный цикл работают нормально. Тестовые расширения
графа предоставляют возможности очистки или фикстур, не загрязняя продакшн-интерфейсы.
Меньший чёрно-ящичный набор запускает построенный образ приложения, ждёт готовности и отрабатывает публичные контракты. Эти тесты сосредотачиваются на репрезентативных критических путях, конфигурации запуска, управляющих эндпоинтах и допущениях развёртывания, а не дублируют каждую ветвь сервиса.
Эта архитектура сохраняет обратную связь быстрой, потому что большинство тестов инициализируют лишь небольшое транзитивное замыкание. Она сохраняет соединение честным, потому что эти замыкания выводятся из сгенерированного графа приложения. Она сохраняет интеграцию реалистичной, потому что выбранные внешние границы остаются реальными. Она сохраняет продакшн-утверждения ограниченными, потому что итоговый образ всё ещё получает собственную независимую проверку.
Более глубокая отдача¶
Модель тестирования Kora следует из более широкого дизайнерского выбора: структура приложения достаточно явна, чтобы её можно было генерировать, проверять, валидировать, нарезать, модифицировать, инициализировать и освобождать как граф. Тестирование — не отдельная подсистема, наслоённая поверх внедрения зависимостей времени выполнения. Это ещё один способ выполнения той же архитектурной модели.
Это сокращает дрейф в нескольких направлениях. Изменения конструктора затрагивают продакшн и тесты вместе. Новые обязательные зависимости появляются в срезе графа, а не забываются в написанной вручную фикстуре. Замены занимают тот же узел, который потребляют реальные зависимые. Типизированная конфигурация достигает фабрик до конструирования. Жизненный цикл принадлежит сохранённым компонентам и очищается тем же порядком зависимостей, который управляет продакшн-завершением.
Результат — не устранение моков, тестовой конфигурации или специальных тестовых компонентов. Это дисциплинированное место для каждого из них. Мок — это замена графа. Тестовая конфигурация — это вход, применённый до построения графа. Частичное приложение — это достижимый продакшн-срез. Тестовое приложение — это явное расширение, скомпилированное в тестовом охвате. Жизненный цикл — часть установки и демонтажа, а не запоздалая мысль.
Вот почему простое и быстрое тестирование может быть основным принципом фреймворка, а не набором вспомогательных API. Как только продакшн-приложение становится явным исполняемым графом, тесту больше не нужно выбирать между скоростью и верным соединением. Он может сохранить соединение, срезать граф на честной границе и инициализировать только то приложение, которое нужно, чтобы ответить на один точный вопрос.