Porting to Quarkus/ArC

How we ported the auto-mocking CDI extension from CDI SE (Weld/OWB) to Quarkus/ArC using only ArC's processor SPIs — no @QuarkusTest required.

ArC 64 tests passing JVM mode April 2026

The Challenge

The addon uses six CDI lifecycle events — BeforeBeanDiscovery, ProcessAnnotatedType, AfterTypeDiscovery, ProcessInjectionPoint, ProcessBean, and AfterBeanDiscovery — to discover unsatisfied injection points and register Mockito mocks. ArC fires none of these at runtime. It processes everything at build time through a BeanProcessor pipeline.

CDI SE vs ArC: API Mapping

FeatureCDI SEQuarkus/ArC
Bean discoveryRuntime classpath scanningJandex index at build time
Synthetic beansAfterBeanDiscovery.addBean()BeanRegistrar + BeanConfigurator
AlternativesAfterTypeDiscovery.getAlternatives()AnnotationTransformation — add @Priority
Vetoing beansProcessAnnotatedType.veto()BeanProcessor.addExcludeType()
Container bootSeContainerInitializer.initialize()BeanProcessor.process() + Arc.initialize()
Mock creationCustom Bean<T> implBeanCreator<T>Mockito.mock()
Test injectionInjectionTarget.inject()ArcContainer.select().get()
Request scopeRequestContextControllerArc.container().requestContext()

The Approach

We use ArC's processor APIs directly — the same way ArC's own ArcTestContainer and Quarkus's QuarkusComponentTestExtension do it. No @BuildStep processors, no multi-module deployment/runtime split. @QuarkusTest is not required — but if present, the addon detects it and skips container management, letting the Quarkus test framework handle the lifecycle.

The result is a single JUnit 5 extension that performs a full ArC build pass inside @BeforeAll:

@EnableTestBeans
@TestBean(bean = CustomGreeting.class)
class MyTest {

    @Inject
    GreetingConsumer consumer;

    @Test
    void greetingIsReplaced() {
        assertEquals("Hello, world!", consumer.getGreeting().greet("world"));
    }
}

Same annotations. Same test structure. Same developer experience.

How It Works

  1. Discover classes — scans test-classes directory and classpath JARs, filters out other test classes
  2. Build Jandex index — indexes every bean class and its type hierarchy
  3. Transform annotations — adds @Priority(MAX_VALUE) to @TestBean-selected alternatives
  4. Exclude clashing beans — unselected alternatives with type overlap are excluded
  5. Register mock beans — a BeanRegistrar iterates all injection points, unsatisfied ones get a MockBeanCreator
  6. Register inline fields@TestBean static fields get an InlineFieldBeanCreator
  7. Process and bootBeanProcessor.process() generates bytecode, Arc.initialize() starts the container

@Nonbinding — The Subtle One

CDI qualifiers can have @Nonbinding members — meaning those values are ignored during bean resolution. If two injection points differ only in a @Nonbinding member, they must share the same mock bean. The ArC version queries the BeanDeployment for nonbinding members, strips those values from qualifier annotations before registration, and deduplicates using a key that ignores nonbinding member values.

What Was Eliminated

CDI SE ComponentQuarkus Status
DynamicTestBeanExtensionReplaced by BeanRegistrar + AnnotationTransformation
DynamicTestBeanContextEliminated — no static volatile bridge needed
META-INF/services/...ExtensionEliminated — @ExtendWith is the entry point
Weld / OWB profilesEliminated — ArC only
manageContainer flagIgnored — container managed by the extension (or by @QuarkusTest if present)

Using with @QuarkusTest

The addon works standalone — it boots and manages its own ArC container. But in a full Quarkus application you may already use @QuarkusTest, which manages the container itself. Using both is safe: when @QuarkusTest (or @QuarkusComponentTest) is detected on the test class, the addon automatically skips all container lifecycle management. Only the @TestBean / @EnableTestBeans annotations are processed — the Quarkus test framework handles the rest.

Standalone (addon manages the container)

@EnableTestBeans
@TestBean(bean = CustomGreeting.class)
class StandaloneTest {

    @Inject
    GreetingConsumer consumer;

    @Test
    void test() {
        assertEquals("Hello, world!", consumer.getGreeting().greet("world"));
    }
}

Combined with @QuarkusTest (Quarkus manages the container)

@QuarkusTest            // Quarkus manages the container
@EnableTestBeans       // addon skips container, provides @TestBean support
@TestBean(bean = CustomGreeting.class)
class QuarkusIntegrationTest {

    @Inject
    GreetingConsumer consumer;

    @Test
    void test() {
        assertEquals("Hello, world!", consumer.getGreeting().greet("world"));
    }
}
Detection is automatic. The addon checks for @QuarkusTest and @QuarkusComponentTest by annotation name — no compile-time dependency on the Quarkus test framework is required. If neither annotation is present, the addon manages the ArC container itself.

The Result

64 / 64
tests passing
All 64 original tests ported with Quarkus equivalents

Maven Dependency

<dependency>
    <groupId>org.os890.cdi.addon</groupId>
    <artifactId>dynamic-cdi-test-bean-addon-quarkus</artifactId>
    <version>1.0.0-SNAPSHOT</version>
    <scope>test</scope>
</dependency>
That's it. The API (@EnableTestBeans, @TestBean) is included transitively. The dependency footprint is just ArC (arc-processor + arc-runtime), plus JUnit and Mockito as provided scope. No quarkus-core, no quarkus-bootstrap, no SmallRye Config.
← first post Modularization →