From 922296203a79141905a9e7588d91d649d246ff7a Mon Sep 17 00:00:00 2001 From: Matej Novotny Date: Fri, 25 Sep 2026 16:22:41 +0200 Subject: [PATCH 1/9] Update Weld to 7.0.0.Final and CDI to 5.0.0 --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 26f3050..c353f07 100644 --- a/pom.xml +++ b/pom.xml @@ -52,7 +52,7 @@ 6.1.3 2.4-groovy-4.0 4.0.30 - 7.0.0.CR1 + 7.0.0.Final 5.23.0 4.0.1 4.0.0-M7 From 7f5a4dd0f4a30ed1baf65d58a2c07fe40d6ae3a8 Mon Sep 17 00:00:00 2001 From: Matej Novotny Date: Fri, 25 Sep 2026 16:42:32 +0200 Subject: [PATCH 2/9] Update Weld Parent, Groovy and Mockito dependencies --- pom.xml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pom.xml b/pom.xml index c353f07..5b8e30b 100644 --- a/pom.xml +++ b/pom.xml @@ -10,7 +10,7 @@ org.jboss.weld weld-parent - 58 + 60 http://weld.cdi-spec.org @@ -51,9 +51,9 @@ 4.13.2 6.1.3 2.4-groovy-4.0 - 4.0.30 + 4.0.33 7.0.0.Final - 5.23.0 + 5.24.0 4.0.1 4.0.0-M7 2.26.1 From 389eb30929610f1860b047bab273af1f49dd66fd Mon Sep 17 00:00:00 2001 From: Matej Novotny Date: Fri, 25 Sep 2026 16:47:01 +0200 Subject: [PATCH 3/9] Refresh release documentation and document missing configuration options --- README.md | 22 +++++++------- .../weld/junit/AbstractWeldInitiator.java | 2 +- junit-jupiter/README.md | 29 ++++++++++++++++--- .../weld/junit/jupiter/WeldInitiator.java | 2 +- junit4/README.md | 1 + spock/README.md | 1 + 6 files changed, 41 insertions(+), 16 deletions(-) diff --git a/README.md b/README.md index fc63b5d..4703fda 100644 --- a/README.md +++ b/README.md @@ -1,16 +1,18 @@ # Weld Testing Extensions -[![Maven Central](http://img.shields.io/maven-central/v/org.jboss.weld/weld-junit4.svg)](http://search.maven.org/#search%7Cga%7C1%7Ca%3A%22weld-junit4%22) -[![License](https://img.shields.io/badge/license-Apache%20License%202.0-yellow.svg)](http://www.apache.org/licenses/LICENSE-2.0.html) +[![Maven Central](https://img.shields.io/maven-central/v/org.jboss.weld/weld-junit4.svg)](https://central.sonatype.com/artifact/org.jboss.weld/weld-junit4) +[![License](https://img.shields.io/badge/license-Apache%20License%202.0-yellow.svg)](https://www.apache.org/licenses/LICENSE-2.0.html) The primary goal of this project is to provide simple and fast tools for CDI *unit/component* testing. -The tools are implemented as JUnit 4, JUnit 5 and Spock extensions. +The tools are implemented as JUnit 4, JUnit Jupiter and Spock extensions. -:warning: **Version 6.x of this extension supports Weld 7.0 (CDI 5.0). The JUnit 5 extension artifact has been renamed from `weld-junit5` to `weld-junit-jupiter` — see the [migration guide](junit-jupiter/README.md#migrating-from-weld-junit5) for details.** +:warning: **Version 6.x of this extension supports Weld 7.0 (CDI 5.0). The JUnit Jupiter extension artifact has been renamed from `weld-junit5` to `weld-junit-jupiter` — see the [migration guide](junit-jupiter/README.md#migrating-from-weld-junit5) for details.** + +Java 17 or newer is required. ## The What -Extensions in this repository allow you to write unit tests for your beans regardless of the target environment (Java SE, Java EE, etc.). +Extensions in this repository allow you to write unit tests for your beans regardless of the target environment (Java SE, Jakarta EE, etc.). They start a real CDI container with minimal configuration meaning you can leverage all bean capabilities - injection, interception, events, etc. - without the need for mocking. These extensions boot up CDI container before each test container run and shut it down afterwards. You have the power to customize what beans, extensions, interceptors (and so on) are going to be in the container. @@ -21,21 +23,21 @@ Furthermore, you can `@Inject` directly in your test class - and the list goes o As a matter of fact, one could have a unit test for CDI bean without running a container, but that would present quite a few drawbacks. Simulating field injection to start with, then interceptors and/or decorators - all in all, quite a challenge. -There are frameworks to make this easier such as [Mockito](http://site.mockito.org/); but use too many mocks and things get tangled real quick. +There are frameworks to make this easier such as [Mockito](https://site.mockito.org/); but use too many mocks and things get tangled real quick. So we came with JUnit/Spock extensions which allow you to use actual CDI container instead of complex simulations. There is no need to change the way you develop your CDI components if you have a real container to test it with. Besides, it's easy to combine this approach with mocking frameworks (see for instance [Adding mock beans](junit-jupiter/README.md#adding-mock-beans)). ## How To Use Each Extension -This project consists of three modules. +This project consists of four modules. Below is a list with links to detailed README of each extension: * JUnit 4 extension * [JUnit 4 extension using `@Rule` and `@ClassRule` mechanism](junit4/README.md) -* JUnit 5 - * [JUnit 5 extension using the `@ExtendWith` mechanism](junit-jupiter/README.md) +* JUnit Jupiter + * [JUnit Jupiter extension using the `@ExtendWith` mechanism](junit-jupiter/README.md) * JUnit-common - * Houses the parts of code shared by both Junit extensions + * Houses the code shared by the JUnit and Spock extensions * Spock * [Spock framework extension](spock/README.md) diff --git a/junit-common/src/main/java/org/jboss/weld/junit/AbstractWeldInitiator.java b/junit-common/src/main/java/org/jboss/weld/junit/AbstractWeldInitiator.java index 3407c1c..3e29f00 100644 --- a/junit-common/src/main/java/org/jboss/weld/junit/AbstractWeldInitiator.java +++ b/junit-common/src/main/java/org/jboss/weld/junit/AbstractWeldInitiator.java @@ -493,7 +493,7 @@ public T bindResource(String name, Object resource) { * Makes it possible to mock {@code @EJB} injection points. * *

- * Note that for Weld 3 {@code org.jboss.weld.module:weld-ejb} dependency is also required. + * The {@code org.jboss.weld.module:weld-ejb} dependency is also required. *

* * @param ejbFactory diff --git a/junit-jupiter/README.md b/junit-jupiter/README.md index 278ba41..52fbff8 100644 --- a/junit-jupiter/README.md +++ b/junit-jupiter/README.md @@ -1,11 +1,11 @@ -# Weld JUnit 5 (Jupiter) Extensions +# Weld JUnit Jupiter Extensions -There are two extension here, both of which follow the extension mechanism introduced in JUnit 5. +There are two extensions here, both of which follow the extension mechanism introduced in JUnit 5. Therefore, in order to use this extension in your test, you have to annotate your test class with `@ExtendWith(WeldJunit5Extension.class)` or `@ExtendWith(WeldJunit5AutoExtension.class)` respectively. In their default behaviour, the extensions will automatically start/stop Weld SE container and inject into all your `@Inject` fields and method parameters in the given test instance. Furthermore, you can provide configuration and modify Weld bootstrapping process in various ways - extensions, scope activation, interception, ... -These JUnit extensions supports both test lifecycles [as described by JUnit 5](https://junit.org/junit5/docs/current/user-guide/#writing-tests-test-instance-lifecycle) - per method and per class. +These extensions support both test lifecycles [as described by JUnit](https://docs.junit.org/current/writing-tests/test-instance-lifecycle.html) - per method and per class. These extensions fully support parallel execution mode of JUnit Jupiter, except for if `WeldContainer.current()` is used which does not work if multiple containers are running from the same class loader. This includes the usage of @@ -14,7 +14,7 @@ internally, as soon as the according beans are resolved and thus created. If eit get a `WELD-ENV-002016` error if another container is running at the same time, so you should use `@Isolated` for these features to make sure they are running individually. -Requirements are JUnit 5 and Java 17. +This extension requires Java 17 or newer. ## Table of contents @@ -41,6 +41,7 @@ Requirements are JUnit 5 and Java 17. * [`@EnableAlternatives`](#enablealternatives) * [`@ExcludeBean`](#excludebean) * [`@ExcludeBeanClasses`](#excludebeanclasses) + * [`@SetBeanDiscoveryMode`](#setbeandiscoverymode) * [Additional Configuration](#additional-configuration) * [Explicit Parameter Injection](#explicit-parameter-injection) * [Flat Deployment](#flat-deployment) @@ -370,6 +371,7 @@ If a bean under the test declares a non-CDI injection point (such as `@Resource` * `setEjbFactory()` - to handle `@EJB` * `setPersistenceUnitFactory()` - to handle `@PersistenceUnit` * `setPersistenceContextFactory()` - to handle `@PersistenceContext` +* `setPersistenceAgentFactory()` - to handle `@PersistenceAgent` (Jakarta Persistence 4.0) ```java class Baz { @@ -640,6 +642,25 @@ This annotation works as an inverse of [`@AddBeanClasses`](#addbeanclasses) henc NOTE: This annotation will only exclude beans defined by class annotations. It will not exclude beans of the specified type that are defined by `Produces` producer methods/fields or synthetic beans. +### `@SetBeanDiscoveryMode` + +Sets the discovery mode of the synthetic bean archive used by the auto extension. +The default is `BeanDiscoveryMode.ANNOTATED`, which discovers classes with bean defining annotations. +Use `BeanDiscoveryMode.ALL` to include classes without such annotations, for example when adding a package: + +```java +@EnableAutoWeld +@AddPackages(MyService.class) +@SetBeanDiscoveryMode(BeanDiscoveryMode.ALL) +class MyServiceTest { + // Tests can inject classes from MyService's package without bean defining annotations. +} +``` + +Import `org.jboss.weld.junit.jupiter.auto.SetBeanDiscoveryMode` and +`org.jboss.weld.bootstrap.spi.BeanDiscoveryMode` to use this configuration. +This changes the synthetic archive's discovery mode; it does not enable classpath discovery. + ## Additional Configuration This section describes any additional configuration options this extension offers. diff --git a/junit-jupiter/src/main/java/org/jboss/weld/junit/jupiter/WeldInitiator.java b/junit-jupiter/src/main/java/org/jboss/weld/junit/jupiter/WeldInitiator.java index 9eff1a0..375931e 100644 --- a/junit-jupiter/src/main/java/org/jboss/weld/junit/jupiter/WeldInitiator.java +++ b/junit-jupiter/src/main/java/org/jboss/weld/junit/jupiter/WeldInitiator.java @@ -31,7 +31,7 @@ import org.jboss.weld.junit.AbstractWeldInitiator; /** - * JUnit 5 initiator - can be used to customize the Weld SE container started by {@link WeldJunit5Extension}. + * JUnit Jupiter initiator - can be used to customize the Weld SE container started by {@link WeldJunit5Extension}. * *

* Example: diff --git a/junit4/README.md b/junit4/README.md index 94cbbdb..0ddeed2 100644 --- a/junit4/README.md +++ b/junit4/README.md @@ -293,6 +293,7 @@ If a bean under the test declares a non-CDI injection point (such as `@Resource` * `setEjbFactory()` - to handle `@EJB` * `setPersistenceUnitFactory()` - to handle `@PersistenceUnit` * `setPersistenceContextFactory()` - to handle `@PersistenceContext` +* `setPersistenceAgentFactory()` - to handle `@PersistenceAgent` (Jakarta Persistence 4.0) ```java class Baz { diff --git a/spock/README.md b/spock/README.md index bb34a44..ebdf706 100644 --- a/spock/README.md +++ b/spock/README.md @@ -490,6 +490,7 @@ installed. `WeldInitiator.Builder` comes with several convenient methods which a * `setEjbFactory()` - to handle `@EJB` * `setPersistenceUnitFactory()` - to handle `@PersistenceUnit` * `setPersistenceContextFactory()` - to handle `@PersistenceContext` +* `setPersistenceAgentFactory()` - to handle `@PersistenceAgent` (Jakarta Persistence 4.0) _**Example:**_ ```groovy From 8823d92e159fbf38e5eaa4cb756f018208349b5a Mon Sep 17 00:00:00 2001 From: Matej Novotny Date: Fri, 25 Sep 2026 16:54:15 +0200 Subject: [PATCH 4/9] Remove obsolete privileged access for WeldSetup fields --- .../org/jboss/weld/junit/jupiter/WeldJunit5Extension.java | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/junit-jupiter/src/main/java/org/jboss/weld/junit/jupiter/WeldJunit5Extension.java b/junit-jupiter/src/main/java/org/jboss/weld/junit/jupiter/WeldJunit5Extension.java index 4af5f88..b6808f7 100644 --- a/junit-jupiter/src/main/java/org/jboss/weld/junit/jupiter/WeldJunit5Extension.java +++ b/junit-jupiter/src/main/java/org/jboss/weld/junit/jupiter/WeldJunit5Extension.java @@ -31,8 +31,6 @@ import java.lang.annotation.Annotation; import java.lang.reflect.Field; import java.lang.reflect.Method; -import java.security.AccessController; -import java.security.PrivilegedAction; import java.util.ArrayList; import java.util.Collections; import java.util.HashSet; @@ -324,10 +322,7 @@ private WeldInitiator findInitiatorInInstance(Object testInstance) { fieldInstance = field.get(testInstance); } catch (IllegalAccessException e) { // In case we cannot get to the field, we need to set accessibility as well - AccessController.doPrivileged((PrivilegedAction) () -> { - field.setAccessible(true); - return null; - }); + field.setAccessible(true); try { fieldInstance = field.get(testInstance); } catch (IllegalAccessException e2) { From 664a6b449dfacc6b2068388a3276c92b228146cd Mon Sep 17 00:00:00 2001 From: Matej Novotny Date: Fri, 25 Sep 2026 16:55:54 +0200 Subject: [PATCH 5/9] Remove legacy Weld compatibility from mock interceptors --- .../org/jboss/weld/junit/MockInterceptor.java | 32 ++++--------------- 1 file changed, 7 insertions(+), 25 deletions(-) diff --git a/junit-common/src/main/java/org/jboss/weld/junit/MockInterceptor.java b/junit-common/src/main/java/org/jboss/weld/junit/MockInterceptor.java index 23729f1..fa4f645 100644 --- a/junit-common/src/main/java/org/jboss/weld/junit/MockInterceptor.java +++ b/junit-common/src/main/java/org/jboss/weld/junit/MockInterceptor.java @@ -18,7 +18,6 @@ import java.io.Serializable; import java.lang.annotation.Annotation; -import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Type; import java.util.Collections; import java.util.HashSet; @@ -35,6 +34,7 @@ import jakarta.interceptor.InvocationContext; import org.jboss.weld.bean.builtin.BeanManagerProxy; +import org.jboss.weld.contexts.CreationalContextImpl; import org.jboss.weld.junit.MockInterceptor.MockInterceptorInstance; import org.jboss.weld.util.bean.SerializableForwardingBean; import org.jboss.weld.util.collections.ImmutableSet; @@ -165,34 +165,16 @@ boolean hasDefaultBeanClass() { } private Bean getInterceptedBean(CreationalContext ctx) { - if (!ctx.getClass().getName().startsWith("org.jboss.weld")) { - return null; - } - Bean interceptedBean = null; - try { - // Note that we need to support both 2.x and 3.x - Class ctxImplClazz; - if (ctx.getClass().getName().startsWith("org.jboss.weld.contexts")) { - // 3.x - ctxImplClazz = MockInterceptor.class.getClassLoader() - .loadClass("org.jboss.weld.contexts.CreationalContextImpl"); - } else { - // 2.x - ctxImplClazz = MockInterceptor.class.getClassLoader().loadClass("org.jboss.weld.context.CreationalContextImpl"); - } - Object parentContext = ctxImplClazz.getMethod("getParentCreationalContext").invoke(ctx); + if (ctx instanceof CreationalContextImpl weldContext) { + CreationalContextImpl parentContext = weldContext.getParentCreationalContext(); if (parentContext != null) { - Contextual interceptedContextual = (Contextual) ctxImplClazz.getMethod("getContextual") - .invoke(parentContext); - if (interceptedContextual instanceof Bean) { - interceptedBean = (Bean) interceptedContextual; + Contextual interceptedContextual = parentContext.getContextual(); + if (interceptedContextual instanceof Bean bean) { + return bean; } } - } catch (ClassNotFoundException | IllegalAccessException | IllegalArgumentException | NoSuchMethodException - | SecurityException | InvocationTargetException e) { - throw new IllegalStateException(e); } - return interceptedBean; + return null; } public static class MockInterceptorInstance implements Serializable { From 76459193a367748f93a8a237e9531e8219bfd949 Mon Sep 17 00:00:00 2001 From: Matej Novotny Date: Fri, 25 Sep 2026 17:07:26 +0200 Subject: [PATCH 6/9] Call WeldContainer event API directly --- .../org/jboss/weld/junit/AbstractWeldInitiator.java | 12 +----------- 1 file changed, 1 insertion(+), 11 deletions(-) diff --git a/junit-common/src/main/java/org/jboss/weld/junit/AbstractWeldInitiator.java b/junit-common/src/main/java/org/jboss/weld/junit/AbstractWeldInitiator.java index 3e29f00..7a998aa 100644 --- a/junit-common/src/main/java/org/jboss/weld/junit/AbstractWeldInitiator.java +++ b/junit-common/src/main/java/org/jboss/weld/junit/AbstractWeldInitiator.java @@ -17,8 +17,6 @@ package org.jboss.weld.junit; import java.lang.annotation.Annotation; -import java.lang.reflect.InvocationTargetException; -import java.lang.reflect.Method; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; @@ -234,17 +232,9 @@ public Iterable> handles() { * * @return an event object */ - @SuppressWarnings("unchecked") public Event event() { checkContainer(); - try { - // We need to use reflection due to some compatibility issues - Method eventMethod = container.getClass().getMethod("event"); - return (Event) eventMethod.invoke(container); - } catch (NoSuchMethodException | SecurityException | IllegalAccessException | IllegalArgumentException - | InvocationTargetException e) { - throw new IllegalStateException("Cannot invoke WeldContainer.event() method using reflection", e); - } + return container.event(); } @Override From 09da381df501c52a6dc18cf022d308b698e06313 Mon Sep 17 00:00:00 2001 From: Matej Novotny Date: Fri, 25 Sep 2026 17:08:14 +0200 Subject: [PATCH 7/9] Use CDI built-in qualifier literals in MockBean --- .../java/org/jboss/weld/junit/MockBean.java | 27 +------------------ 1 file changed, 1 insertion(+), 26 deletions(-) diff --git a/junit-common/src/main/java/org/jboss/weld/junit/MockBean.java b/junit-common/src/main/java/org/jboss/weld/junit/MockBean.java index 0eee1a4..1efb0bd 100644 --- a/junit-common/src/main/java/org/jboss/weld/junit/MockBean.java +++ b/junit-common/src/main/java/org/jboss/weld/junit/MockBean.java @@ -39,7 +39,6 @@ import jakarta.enterprise.inject.spi.Prioritized; import jakarta.enterprise.inject.spi.Unmanaged; import jakarta.enterprise.inject.spi.Unmanaged.UnmanagedInstance; -import jakarta.enterprise.util.AnnotationLiteral; import jakarta.inject.Named; import jakarta.inject.Qualifier; import jakarta.inject.Scope; @@ -349,7 +348,7 @@ private Builder() { this.stereotypes = new HashSet<>(); this.alternative = false; this.qualifiers = new HashSet<>(); - this.qualifiers.add(AnyLiteral.INSTANCE); + this.qualifiers.add(Any.Literal.INSTANCE); this.scope = Dependent.class; this.types = new HashSet<>(); this.types.add(Object.class); @@ -670,28 +669,4 @@ public interface DestroyFunction { } - @SuppressWarnings("all") - static class AnyLiteral extends AnnotationLiteral implements Any { - - private static final long serialVersionUID = -1366513826361712883L; - - public static final Any INSTANCE = new AnyLiteral(); - - private AnyLiteral() { - } - - } - - @SuppressWarnings("all") - static class DefaultLiteral extends AnnotationLiteral implements Default { - - private static final long serialVersionUID = -1395539980812895226L; - - public static final Default INSTANCE = new DefaultLiteral(); - - private DefaultLiteral() { - } - - } - } From a09d0b47dfe771fbf60217e5c42a5737e83a293f Mon Sep 17 00:00:00 2001 From: Matej Novotny Date: Fri, 25 Sep 2026 17:14:08 +0200 Subject: [PATCH 8/9] Support CDI 5 reserve, eager and auto-close mock beans --- .../java/org/jboss/weld/junit/MockBean.java | 123 ++++++++++++++- .../weld/junit/MockBeanWithPriority.java | 8 +- .../jupiter/bean/MockBeanAutoCloseTest.java | 70 +++++++++ .../jupiter/bean/MockBeanDestructionTest.java | 72 +++++++++ .../junit/jupiter/bean/MockBeanEagerTest.java | 87 +++++++++++ .../junit/jupiter/bean/MockBeanReadTest.java | 147 ++++++++++++++++++ .../jupiter/bean/MockBeanReserveTest.java | 107 +++++++++++++ 7 files changed, 606 insertions(+), 8 deletions(-) create mode 100644 junit-jupiter/src/test/java/org/jboss/weld/junit/jupiter/bean/MockBeanAutoCloseTest.java create mode 100644 junit-jupiter/src/test/java/org/jboss/weld/junit/jupiter/bean/MockBeanDestructionTest.java create mode 100644 junit-jupiter/src/test/java/org/jboss/weld/junit/jupiter/bean/MockBeanEagerTest.java create mode 100644 junit-jupiter/src/test/java/org/jboss/weld/junit/jupiter/bean/MockBeanReadTest.java create mode 100644 junit-jupiter/src/test/java/org/jboss/weld/junit/jupiter/bean/MockBeanReserveTest.java diff --git a/junit-common/src/main/java/org/jboss/weld/junit/MockBean.java b/junit-common/src/main/java/org/jboss/weld/junit/MockBean.java index 1efb0bd..8a55b25 100644 --- a/junit-common/src/main/java/org/jboss/weld/junit/MockBean.java +++ b/junit-common/src/main/java/org/jboss/weld/junit/MockBean.java @@ -25,13 +25,18 @@ import java.util.Set; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.atomic.AtomicInteger; +import java.util.logging.Level; +import java.util.logging.Logger; +import jakarta.enterprise.context.AutoClose; import jakarta.enterprise.context.Dependent; +import jakarta.enterprise.context.Eager; import jakarta.enterprise.context.NormalScope; import jakarta.enterprise.context.spi.CreationalContext; import jakarta.enterprise.inject.Alternative; import jakarta.enterprise.inject.Any; import jakarta.enterprise.inject.Default; +import jakarta.enterprise.inject.Reserve; import jakarta.enterprise.inject.Stereotype; import jakarta.enterprise.inject.spi.Bean; import jakarta.enterprise.inject.spi.InjectionPoint; @@ -63,6 +68,8 @@ */ public class MockBean implements Bean, PassivationCapable { + private static final Logger LOGGER = Logger.getLogger(MockBean.class.getName()); + /** * By default, the bean: *
    @@ -72,6 +79,7 @@ public class MockBean implements Bean, PassivationCapable { *
  • has {@link Object} bean type
  • *
  • has no stereotypes
  • *
  • is not an alternative
  • + *
  • is not a reserve, eagerly initialized, or auto-closeable
  • *
* *

@@ -123,6 +131,12 @@ public static Builder read(Class beanClass) { private final boolean alternative; + private final boolean reserve; + + private final boolean eager; + + private final boolean autoClose; + private final boolean selectForSyntheticBeanArchive; private final String name; @@ -145,9 +159,20 @@ protected MockBean(Class beanClass, Set> stereoty boolean selectForSyntheticBeanArchive, String name, Set qualifiers, Set types, Class scope, CreateFunction createCallback, DestroyFunction destroyCallback) { + this(beanClass, stereotypes, alternative, selectForSyntheticBeanArchive, name, qualifiers, types, scope, + createCallback, destroyCallback, false, false, false); + } + + protected MockBean(Class beanClass, Set> stereotypes, boolean alternative, + boolean selectForSyntheticBeanArchive, String name, + Set qualifiers, Set types, Class scope, CreateFunction createCallback, + DestroyFunction destroyCallback, boolean reserve, boolean eager, boolean autoClose) { this.beanClass = beanClass; this.stereotypes = stereotypes; this.alternative = alternative; + this.reserve = reserve; + this.eager = eager; + this.autoClose = autoClose; this.selectForSyntheticBeanArchive = selectForSyntheticBeanArchive; this.name = name; this.qualifiers = qualifiers; @@ -166,8 +191,21 @@ public T create(CreationalContext creationalContext) { @Override public void destroy(T instance, CreationalContext creationalContext) { - if (destroyCallback != null) { - destroyCallback.destroy(instance, creationalContext); + try { + if (destroyCallback != null) { + destroyCallback.destroy(instance, creationalContext); + } + } catch (Exception e) { + LOGGER.log(Level.WARNING, "Unable to destroy mock bean " + id, e); + } + try { + if (isAutoClose() && instance instanceof AutoCloseable closeable) { + closeable.close(); + } + } catch (Exception e) { + LOGGER.log(Level.WARNING, "Unable to auto-close mock bean " + id, e); + } finally { + creationalContext.release(); } } @@ -211,6 +249,21 @@ public boolean isAlternative() { return alternative; } + @Override + public boolean isReserve() { + return reserve; + } + + @Override + public boolean isEager() { + return eager; + } + + @Override + public boolean isAutoClose() { + return autoClose; + } + /** * Initiator should use {@link #getBeanClass()} to select the alternative. Also the alternative should only be selected if * {@link #isAlternative()} also @@ -282,9 +335,18 @@ private static Builder readInternal(Class beanClass) { } } } + builder.reserve(hasAnnotation(beanClass, stereotypes, Reserve.class)); + builder.eager(hasAnnotation(beanClass, stereotypes, Eager.class)); + builder.autoClose(hasAnnotation(beanClass, stereotypes, AutoClose.class)); return builder; } + private static boolean hasAnnotation(Class beanClass, Set stereotypes, + Class annotation) { + return beanClass.isAnnotationPresent(annotation) + || stereotypes.stream().anyMatch(stereotype -> stereotype.annotationType().isAnnotationPresent(annotation)); + } + private static Set getStereotypes(AnnotatedElement element) { Set stereotypes = new HashSet<>(); for (Annotation annotation : element.getAnnotations()) { @@ -328,6 +390,12 @@ public static class Builder { private boolean alternative; + private boolean reserve; + + private boolean eager; + + private boolean autoClose; + private boolean selectForSyntheticBeanArchive; private Integer priority; @@ -455,9 +523,54 @@ public Builder alternative(boolean value) { return this; } + /** + * Marks the bean as a reserve. Select it with {@link #priority(int)} to make it available as a fallback + * when no non-reserve bean matches. A bean cannot be both a reserve and an alternative. + * + * @param value whether the bean is a reserve + * @return self + * @see Bean#isReserve() + * @since 6.0 + */ + public Builder reserve(boolean value) { + this.reserve = value; + return this; + } + + /** + * Requests initialization during container startup. Eager beans must have + * {@link jakarta.enterprise.context.ApplicationScoped} scope. + * + * @param value whether the bean is eagerly initialized + * @return self + * @see Bean#isEager() + * @since 6.0 + */ + public Builder eager(boolean value) { + this.eager = value; + return this; + } + + /** + * Calls {@link AutoCloseable#close()} on an auto-closeable instance after the destroy callback and + * before releasing the creational context. Exceptions from closing are logged and do not prevent cleanup. + * Has no effect on instances that do not implement {@link AutoCloseable}. + * + * @param value whether the bean is auto-closeable + * @return self + * @see Bean#isAutoClose() + * @since 6.0 + */ + public Builder autoClose(boolean value) { + this.autoClose = value; + return this; + } + /** * Programmatic equivalent to to putting {@link jakarta.annotation.Priority} annotation on a bean class. - * Allows for globally enabled alternatives. + * Allows for globally enabled alternatives and reserves. + * Use distinct {@link #beanClass(Class)} values for beans with different priorities: Weld associates + * their enablement priorities with the bean class, and mock beans share a default bean class. * * @param priority * @return self @@ -637,11 +750,11 @@ public MockBean build() { if (priority != null) { return new MockBeanWithPriority<>(beanClass, stereotypes, alternative, selectForSyntheticBeanArchive, priority, name, normalizedQualfiers, types, scope, createCallback, - destroyCallback); + destroyCallback, reserve, eager, autoClose); } else { return new MockBean<>(beanClass, stereotypes, alternative, selectForSyntheticBeanArchive, name, normalizedQualfiers, types, scope, createCallback, - destroyCallback); + destroyCallback, reserve, eager, autoClose); } } diff --git a/junit-common/src/main/java/org/jboss/weld/junit/MockBeanWithPriority.java b/junit-common/src/main/java/org/jboss/weld/junit/MockBeanWithPriority.java index 8b01042..d784db9 100644 --- a/junit-common/src/main/java/org/jboss/weld/junit/MockBeanWithPriority.java +++ b/junit-common/src/main/java/org/jboss/weld/junit/MockBeanWithPriority.java @@ -9,7 +9,8 @@ import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; /** - * A subclass of {@link MockBean} implementing {@link Prioritized} hence allowing for globally enabled alternatives. + * A subclass of {@link MockBean} implementing {@link Prioritized} hence allowing for globally enabled alternatives and + * reserves. * Used instead of {@link MockBean} if user specified {@link MockBean.Builder#priority(int)}. * * @author Matej Novotny @@ -22,9 +23,10 @@ class MockBeanWithPriority extends MockBean implements Prioritized { MockBeanWithPriority(Class beanClass, Set> stereotypes, boolean alternative, boolean selectForSyntheticBeanArchive, int priority, String name, Set qualifiers, Set types, Class scope, - CreateFunction createCallback, DestroyFunction destroyCallback) { + CreateFunction createCallback, DestroyFunction destroyCallback, boolean reserve, boolean eager, + boolean autoClose) { super(beanClass, stereotypes, alternative, selectForSyntheticBeanArchive, name, qualifiers, types, scope, - createCallback, destroyCallback); + createCallback, destroyCallback, reserve, eager, autoClose); if (priority <= 0) { throw new IllegalArgumentException("MockBean cannot have priority equal or lower than 0!"); } diff --git a/junit-jupiter/src/test/java/org/jboss/weld/junit/jupiter/bean/MockBeanAutoCloseTest.java b/junit-jupiter/src/test/java/org/jboss/weld/junit/jupiter/bean/MockBeanAutoCloseTest.java new file mode 100644 index 0000000..4893406 --- /dev/null +++ b/junit-jupiter/src/test/java/org/jboss/weld/junit/jupiter/bean/MockBeanAutoCloseTest.java @@ -0,0 +1,70 @@ +/* + * Copyright The Weld Authors + * SPDX-License-Identifier: Apache-2.0 + */ +package org.jboss.weld.junit.jupiter.bean; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.ArrayList; +import java.util.List; + +import jakarta.enterprise.inject.Any; +import jakarta.enterprise.inject.Instance; +import jakarta.enterprise.inject.Instance.Handle; +import jakarta.enterprise.inject.literal.NamedLiteral; +import jakarta.inject.Inject; + +import org.jboss.weld.junit.MockBean; +import org.jboss.weld.junit.jupiter.EnableWeld; +import org.jboss.weld.junit.jupiter.WeldInitiator; +import org.jboss.weld.junit.jupiter.WeldSetup; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; + +@EnableWeld +class MockBeanAutoCloseTest { + @WeldSetup + WeldInitiator weld = WeldInitiator.from(ResourceConsumer.class) + .addBeans(resource("close", true, false), resource("prioritized-close", true, true), + resource("no-close", false, false), resource("prioritized-no-close", false, true)) + .build(); + + @ParameterizedTest + @CsvSource({ "close,true", "prioritized-close,true", "no-close,false", "prioritized-no-close,false" }) + void destroyingDependentInstanceHonorsAutoClose(String name, boolean autoClose) { + Handle handle = weld.select(ResourceConsumer.class).get().resources.select(NamedLiteral.of(name)).getHandle(); + Resource instance = handle.get(); + assertTrue(instance.events.isEmpty()); + + handle.destroy(); + + assertEquals(autoClose ? List.of("destroy", "close") : List.of("destroy"), instance.events); + } + + private static MockBean resource(String name, boolean autoClose, boolean prioritized) { + MockBean.Builder builder = MockBean. builder().types(Resource.class) + .qualifiers(NamedLiteral.of(name)).autoClose(autoClose).create(ctx -> new Resource()) + .destroy((instance, ctx) -> instance.events.add("destroy")); + if (prioritized) { + builder.priority(10); + } + return builder.build(); + } + + public static class ResourceConsumer { + @Inject + @Any + Instance resources; + } + + public static class Resource implements AutoCloseable { + private final List events = new ArrayList<>(); + + @Override + public void close() { + events.add("close"); + } + } +} diff --git a/junit-jupiter/src/test/java/org/jboss/weld/junit/jupiter/bean/MockBeanDestructionTest.java b/junit-jupiter/src/test/java/org/jboss/weld/junit/jupiter/bean/MockBeanDestructionTest.java new file mode 100644 index 0000000..7c387f7 --- /dev/null +++ b/junit-jupiter/src/test/java/org/jboss/weld/junit/jupiter/bean/MockBeanDestructionTest.java @@ -0,0 +1,72 @@ +/* + * Copyright The Weld Authors + * SPDX-License-Identifier: Apache-2.0 + */ +package org.jboss.weld.junit.jupiter.bean; + +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.atomic.AtomicInteger; + +import jakarta.enterprise.context.spi.CreationalContext; + +import org.jboss.weld.junit.MockBean; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +class MockBeanDestructionTest { + + @ParameterizedTest + @ValueSource(booleans = { false, true }) + void failedDestructionStillClosesAndReleasesContext(boolean failingCallback) { + List events = new ArrayList<>(); + AutoCloseable service = () -> { + events.add("close"); + throw new Exception("Expected close failure"); + }; + MockBean bean = MockBean. builder().autoClose(true).creating(service) + .destroy((instance, ctx) -> { + events.add("destroy"); + if (failingCallback) { + throw new IllegalStateException("Expected destroy failure"); + } + }).build(); + CreationalContext context = new CreationalContext<>() { + @Override + public void push(AutoCloseable incompleteInstance) { + } + + @Override + public void release() { + events.add("release"); + } + }; + + assertDoesNotThrow(() -> bean.destroy(service, context)); + assertEquals(List.of("destroy", "close", "release"), events); + } + + @Test + void autoCloseIgnoresInstancesThatAreNotAutoCloseable() { + AtomicInteger releases = new AtomicInteger(); + Object instance = new Object(); + MockBean bean = MockBean.builder().autoClose(true).creating(instance).build(); + CreationalContext context = new CreationalContext<>() { + @Override + public void push(Object incompleteInstance) { + } + + @Override + public void release() { + releases.incrementAndGet(); + } + }; + assertDoesNotThrow(() -> bean.destroy(instance, context)); + assertEquals(1, releases.get()); + } + +} diff --git a/junit-jupiter/src/test/java/org/jboss/weld/junit/jupiter/bean/MockBeanEagerTest.java b/junit-jupiter/src/test/java/org/jboss/weld/junit/jupiter/bean/MockBeanEagerTest.java new file mode 100644 index 0000000..368e468 --- /dev/null +++ b/junit-jupiter/src/test/java/org/jboss/weld/junit/jupiter/bean/MockBeanEagerTest.java @@ -0,0 +1,87 @@ +/* + * Copyright The Weld Authors + * SPDX-License-Identifier: Apache-2.0 + */ +package org.jboss.weld.junit.jupiter.bean; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.util.concurrent.atomic.AtomicInteger; + +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.enterprise.inject.Any; +import jakarta.enterprise.inject.Instance; +import jakarta.enterprise.inject.literal.NamedLiteral; +import jakarta.inject.Inject; + +import org.jboss.weld.junit.MockBean; +import org.jboss.weld.junit.jupiter.EnableWeld; +import org.jboss.weld.junit.jupiter.WeldInitiator; +import org.jboss.weld.junit.jupiter.WeldSetup; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +@EnableWeld +class MockBeanEagerTest { + private final AtomicInteger eagerCreations = new AtomicInteger(); + private final AtomicInteger prioritizedEagerCreations = new AtomicInteger(); + private final AtomicInteger lazyCreations = new AtomicInteger(); + private final AtomicInteger prioritizedLazyCreations = new AtomicInteger(); + + @WeldSetup + WeldInitiator weld = WeldInitiator.from(ServiceConsumer.class) + .addBeans(service("eager", true, false, eagerCreations), + service("prioritized-eager", true, true, prioritizedEagerCreations), + service("lazy", false, false, lazyCreations), + service("prioritized-lazy", false, true, prioritizedLazyCreations)) + .build(); + + @ParameterizedTest + @ValueSource(booleans = { false, true }) + void eagerBeanIsCreatedBeforeTestBegins(boolean prioritized) { + AtomicInteger creations = prioritized ? prioritizedEagerCreations : eagerCreations; + String name = prioritized ? "prioritized-eager" : "eager"; + assertEquals(1, creations.get()); + assertEquals("ready", weld.select(ServiceConsumer.class).get().value(name)); + assertEquals(1, creations.get()); + } + + @ParameterizedTest + @ValueSource(booleans = { false, true }) + void lazyBeanIsCreatedOnFirstUse(boolean prioritized) { + AtomicInteger creations = prioritized ? prioritizedLazyCreations : lazyCreations; + String name = prioritized ? "prioritized-lazy" : "lazy"; + assertEquals(0, creations.get()); + assertEquals("ready", weld.select(ServiceConsumer.class).get().value(name)); + assertEquals(1, creations.get()); + } + + private static MockBean service(String name, boolean eager, boolean prioritized, AtomicInteger creations) { + MockBean.Builder builder = MockBean. builder() + .types(Service.class).qualifiers(NamedLiteral.of(name)).scope(ApplicationScoped.class).eager(eager) + .create(ctx -> { + creations.incrementAndGet(); + return new Service(); + }); + if (prioritized) { + builder.priority(10); + } + return builder.build(); + } + + public static class ServiceConsumer { + @Inject + @Any + Instance services; + + public String value(String name) { + return services.select(NamedLiteral.of(name)).get().value(); + } + } + + public static class Service { + public String value() { + return "ready"; + } + } +} diff --git a/junit-jupiter/src/test/java/org/jboss/weld/junit/jupiter/bean/MockBeanReadTest.java b/junit-jupiter/src/test/java/org/jboss/weld/junit/jupiter/bean/MockBeanReadTest.java new file mode 100644 index 0000000..3fb4ea8 --- /dev/null +++ b/junit-jupiter/src/test/java/org/jboss/weld/junit/jupiter/bean/MockBeanReadTest.java @@ -0,0 +1,147 @@ +/* + * Copyright The Weld Authors + * SPDX-License-Identifier: Apache-2.0 + */ +package org.jboss.weld.junit.jupiter.bean; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import jakarta.annotation.PostConstruct; +import jakarta.annotation.PreDestroy; +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.enterprise.context.AutoClose; +import jakarta.enterprise.context.Eager; +import jakarta.enterprise.inject.Instance.Handle; +import jakarta.enterprise.inject.Reserve; +import jakarta.enterprise.inject.Stereotype; +import jakarta.inject.Inject; + +import org.jboss.weld.junit.MockBean; +import org.jboss.weld.junit.jupiter.EnableWeld; +import org.jboss.weld.junit.jupiter.WeldInitiator; +import org.jboss.weld.junit.jupiter.WeldSetup; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.parallel.Isolated; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +// MockBean.read() uses Unmanaged and WeldContainer.current(), which requires a single running container. +@Isolated +@EnableWeld +class MockBeanReadTest { + @Inject + LifecycleTracker tracker; + private final MockBean overridden = MockBean.read(OverriddenService.class) + .reserve(false).eager(false).autoClose(false).build(); + + @WeldSetup + WeldInitiator weld = WeldInitiator.from(LifecycleTracker.class) + .addBeans(MockBean.read(AnnotatedService.class).priority(10).build(), + MockBean.read(StereotypedService.class).priority(10).build(), overridden) + .inject(this).build(); + + @ParameterizedTest + @ValueSource(classes = { AnnotatedService.class, StereotypedService.class }) + void readsAttributesAndClosesUnmanagedInstanceOnce(Class type) { + Handle handle = weld.select(type).getHandle(); + assertTrue(handle.getBean().isReserve()); + assertTrue(handle.getBean().isEager()); + assertTrue(handle.getBean().isAutoClose()); + assertEquals(List.of("postConstruct"), tracker.eventsFor(type)); + + handle.get(); + handle.destroy(); + + assertEquals(List.of("postConstruct", "preDestroy", "close"), tracker.eventsFor(type)); + } + + @Test + void explicitSettingsOverrideReadAttributes() { + assertFalse(overridden.isReserve()); + assertFalse(overridden.isEager()); + assertFalse(overridden.isAutoClose()); + assertTrue(tracker.eventsFor(OverriddenService.class).isEmpty()); + + Handle handle = weld.select(OverriddenService.class).getHandle(); + assertEquals("ready", handle.get().value()); + handle.destroy(); + + assertEquals(List.of("postConstruct", "preDestroy"), tracker.eventsFor(OverriddenService.class)); + } + + @ApplicationScoped + public static class LifecycleTracker { + private final Map, List> events = new HashMap<>(); + + public List eventsFor(Class type) { + return events.computeIfAbsent(type, ignored -> new ArrayList<>()); + } + } + + public static class LifecycleService implements AutoCloseable { + @Inject + LifecycleTracker tracker; + + public String value() { + return "ready"; + } + + @PostConstruct + public void postConstruct() { + tracker.eventsFor(getClass()).add("postConstruct"); + } + + @PreDestroy + public void preDestroy() { + tracker.eventsFor(getClass()).add("preDestroy"); + } + + @Override + public void close() { + tracker.eventsFor(getClass()).add("close"); + } + } + + @ApplicationScoped + @Reserve + @Eager + @AutoClose + public static class AnnotatedService extends LifecycleService { + } + + @NestedResourceStereotype + public static class StereotypedService extends LifecycleService { + } + + @ResourceStereotype + public static class OverriddenService extends LifecycleService { + } + + @Stereotype + @Retention(RetentionPolicy.RUNTIME) + @Target({ ElementType.TYPE, ElementType.ANNOTATION_TYPE }) + @ApplicationScoped + @Reserve + @Eager + @AutoClose + @interface ResourceStereotype { + } + + @Stereotype + @Retention(RetentionPolicy.RUNTIME) + @Target(ElementType.TYPE) + @ResourceStereotype + @interface NestedResourceStereotype { + } +} diff --git a/junit-jupiter/src/test/java/org/jboss/weld/junit/jupiter/bean/MockBeanReserveTest.java b/junit-jupiter/src/test/java/org/jboss/weld/junit/jupiter/bean/MockBeanReserveTest.java new file mode 100644 index 0000000..6e02729 --- /dev/null +++ b/junit-jupiter/src/test/java/org/jboss/weld/junit/jupiter/bean/MockBeanReserveTest.java @@ -0,0 +1,107 @@ +/* + * Copyright The Weld Authors + * SPDX-License-Identifier: Apache-2.0 + */ +package org.jboss.weld.junit.jupiter.bean; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import jakarta.enterprise.inject.Instance; +import jakarta.inject.Inject; + +import org.jboss.weld.junit.MockBean; +import org.jboss.weld.junit.jupiter.EnableWeld; +import org.jboss.weld.junit.jupiter.WeldInitiator; +import org.jboss.weld.junit.jupiter.WeldSetup; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; + +class MockBeanReserveTest { + + @Nested + @EnableWeld + class WithoutRegularBean { + @WeldSetup + WeldInitiator weld = WeldInitiator.from(ServiceConsumer.class) + .addBeans(MockBean. builder().types(Service.class) + .reserve(true).priority(10).creating(() -> "fallback").build()) + .build(); + + @Test + void selectsReserve() { + assertEquals("fallback", weld.select(ServiceConsumer.class).get().services.get().value()); + } + } + + @Nested + @EnableWeld + class WithRegularBean { + @WeldSetup + WeldInitiator weld = WeldInitiator.from(ServiceConsumer.class) + .addBeans(MockBean. builder().types(Service.class) + .reserve(true).priority(10).creating(() -> "fallback").build(), + MockBean. of(() -> "regular", Service.class)) + .build(); + + @Test + void selectsRegularBean() { + assertEquals("regular", weld.select(ServiceConsumer.class).get().services.get().value()); + } + } + + @Nested + @EnableWeld + class CompetingReserves { + @WeldSetup + WeldInitiator weld = WeldInitiator.from(ServiceConsumer.class) + .addBeans(MockBean. builder().types(Service.class).beanClass(FirstService.class) + .reserve(true).priority(10).creating(new FirstService()).build(), + MockBean. builder().types(Service.class).beanClass(SecondService.class) + .reserve(true).priority(20).creating(new SecondService()).build()) + .build(); + + @Test + void selectsHigherPriorityReserve() { + assertEquals("second", weld.select(ServiceConsumer.class).get().services.get().value()); + } + } + + @Nested + @EnableWeld + class WithoutPriority { + @WeldSetup + WeldInitiator weld = WeldInitiator.from(ServiceConsumer.class) + .addBeans(MockBean. builder().types(Service.class) + .reserve(true).creating(() -> "fallback").build()) + .build(); + + @Test + void doesNotSelectReserve() { + assertTrue(weld.select(ServiceConsumer.class).get().services.isUnsatisfied()); + } + } + + public static class ServiceConsumer { + @Inject + Instance services; + } + + interface Service { + String value(); + } + + static class FirstService implements Service { + @Override + public String value() { + return "first"; + } + } + + static class SecondService implements Service { + @Override + public String value() { + return "second"; + } + } +} From c5a71ac6cc536e67b431f5d50794e53f13a84795 Mon Sep 17 00:00:00 2001 From: Matej Novotny Date: Tue, 29 Sep 2026 13:34:17 +0200 Subject: [PATCH 9/9] Bind unmanaged mock lifecycles to their owning container --- .../java/org/jboss/weld/junit/MockBean.java | 91 ++++++++++++++----- .../jboss/weld/junit/WeldCDIExtension.java | 2 +- .../junit/jupiter/bean/MockBeanReadTest.java | 3 - 3 files changed, 70 insertions(+), 26 deletions(-) diff --git a/junit-common/src/main/java/org/jboss/weld/junit/MockBean.java b/junit-common/src/main/java/org/jboss/weld/junit/MockBean.java index 8a55b25..f2288a4 100644 --- a/junit-common/src/main/java/org/jboss/weld/junit/MockBean.java +++ b/junit-common/src/main/java/org/jboss/weld/junit/MockBean.java @@ -39,6 +39,7 @@ import jakarta.enterprise.inject.Reserve; import jakarta.enterprise.inject.Stereotype; import jakarta.enterprise.inject.spi.Bean; +import jakarta.enterprise.inject.spi.BeanManager; import jakarta.enterprise.inject.spi.InjectionPoint; import jakarta.enterprise.inject.spi.PassivationCapable; import jakarta.enterprise.inject.spi.Prioritized; @@ -49,7 +50,6 @@ import jakarta.inject.Scope; import org.jboss.weld.environment.se.Weld; -import org.jboss.weld.environment.se.WeldContainer; import org.jboss.weld.util.collections.ImmutableSet; import org.jboss.weld.util.reflection.HierarchyDiscovery; @@ -666,36 +666,21 @@ public Builder creating(final T instance) { /** * Use {@link Unmanaged} to create/destroy the bean instance. + * The mock must be registered through {@code WeldInitiator.Builder.addBeans(Bean...)} so that its unmanaged + * lifecycle can be bound to the owning container's {@link BeanManager}. * *

* NOTE: {@code CreationalContext#toString()} is used as a key in a map and therefore must be unique for the lifetime of - * a bean instance. Weld - * implementation fulfills this requirement. + * a bean instance. Weld implementation fulfills this requirement. *

* * @return self * @see UnmanagedInstance */ - @SuppressWarnings("unchecked") public Builder useUnmanaged(Class beanClass) { - Map> ctxToUnmanaged = new ConcurrentHashMap<>(); - create(ctx -> { - Unmanaged unmanaged = new Unmanaged<>(WeldContainer.current().getBeanManager(), beanClass); - UnmanagedInstance unmanagedInstance = unmanaged.newInstance(); - ctxToUnmanaged.put(ctx.toString(), unmanagedInstance); - return (T) unmanagedInstance.produce().inject().postConstruct().get(); - }); - destroy((o, ctx) -> { - UnmanagedInstance unmanagedInstance = ctxToUnmanaged.remove(ctx.toString()); - if (unmanagedInstance != null) { - if (!unmanagedInstance.get().equals(o)) { - throw new IllegalStateException( - "Unmanaged instance [" + unmanagedInstance.get() - + "] is not equal to the bean instance to be destroyed: " + o); - } - unmanagedInstance.preDestroy().dispose(); - } - }); + UnmanagedLifecycle lifecycle = new UnmanagedLifecycle<>(beanClass, null); + create(lifecycle); + destroy(lifecycle); return this; } @@ -760,6 +745,68 @@ public MockBean build() { } + // The same MockBean may be shared by multiple initiators, including in parallel tests. Setting its BeanManager + // in place would let one container overwrite another's manager. Instead, copy mocks with unmanaged callbacks + // so each deployment has its own BeanManager binding and unmanaged instance tracking. + MockBean forBeanManager(BeanManager beanManager) { + CreateFunction creation = createCallback; + DestroyFunction destruction = destroyCallback; + if (creation instanceof UnmanagedLifecycle lifecycle) { + UnmanagedLifecycle bound = new UnmanagedLifecycle<>(lifecycle.beanClass, beanManager); + creation = bound; + if (destruction == lifecycle) { + destruction = bound; + } + } + if (destruction == destroyCallback && destruction instanceof UnmanagedLifecycle lifecycle) { + destruction = new UnmanagedLifecycle<>(lifecycle.beanClass, beanManager); + } + if (creation == createCallback && destruction == destroyCallback) { + return this; + } + if (this instanceof Prioritized prioritized) { + return new MockBeanWithPriority<>(beanClass, stereotypes, alternative, selectForSyntheticBeanArchive, + prioritized.getPriority(), name, qualifiers, types, scope, creation, destruction, reserve, eager, + autoClose); + } + return new MockBean<>(beanClass, stereotypes, alternative, selectForSyntheticBeanArchive, name, + qualifiers, types, scope, creation, destruction, reserve, eager, autoClose); + } + + private static class UnmanagedLifecycle implements CreateFunction, DestroyFunction { + private final Class beanClass; + private final BeanManager beanManager; + private final Map> instances = new ConcurrentHashMap<>(); + + private UnmanagedLifecycle(Class beanClass, BeanManager beanManager) { + this.beanClass = beanClass; + this.beanManager = beanManager; + } + + @Override + public T create(CreationalContext context) { + if (beanManager == null) { + throw new IllegalStateException( + "Unmanaged mock creation requires a BeanManager bound by WeldInitiator.addBeans()"); + } + UnmanagedInstance instance = new Unmanaged<>(beanManager, beanClass).newInstance(); + instances.put(context.toString(), instance); + return instance.produce().inject().postConstruct().get(); + } + + @Override + public void destroy(T instance, CreationalContext context) { + UnmanagedInstance unmanaged = instances.remove(context.toString()); + if (unmanaged != null) { + if (!unmanaged.get().equals(instance)) { + throw new IllegalStateException("Unmanaged instance [" + unmanaged.get() + + "] is not equal to the bean instance to be destroyed: " + instance); + } + unmanaged.preDestroy().dispose(); + } + } + } + public interface CreateFunction { /** diff --git a/junit-common/src/main/java/org/jboss/weld/junit/WeldCDIExtension.java b/junit-common/src/main/java/org/jboss/weld/junit/WeldCDIExtension.java index ee065e2..4f52f21 100644 --- a/junit-common/src/main/java/org/jboss/weld/junit/WeldCDIExtension.java +++ b/junit-common/src/main/java/org/jboss/weld/junit/WeldCDIExtension.java @@ -56,7 +56,7 @@ void afterBeandiscovery(@Observes AfterBeanDiscovery event, BeanManager beanMana } if (beans != null) { for (Bean bean : beans) { - event.addBean(bean); + event.addBean(bean instanceof MockBean mockBean ? mockBean.forBeanManager(beanManager) : bean); } } } diff --git a/junit-jupiter/src/test/java/org/jboss/weld/junit/jupiter/bean/MockBeanReadTest.java b/junit-jupiter/src/test/java/org/jboss/weld/junit/jupiter/bean/MockBeanReadTest.java index 3fb4ea8..d32b327 100644 --- a/junit-jupiter/src/test/java/org/jboss/weld/junit/jupiter/bean/MockBeanReadTest.java +++ b/junit-jupiter/src/test/java/org/jboss/weld/junit/jupiter/bean/MockBeanReadTest.java @@ -32,12 +32,9 @@ import org.jboss.weld.junit.jupiter.WeldInitiator; import org.jboss.weld.junit.jupiter.WeldSetup; import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.parallel.Isolated; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.ValueSource; -// MockBean.read() uses Unmanaged and WeldContainer.current(), which requires a single running container. -@Isolated @EnableWeld class MockBeanReadTest { @Inject