diff --git a/README.md b/README.md index fc63b5d5..4703fdab 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 3407c1c0..7a998aaf 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 @@ -493,7 +483,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-common/src/main/java/org/jboss/weld/junit/MockBean.java b/junit-common/src/main/java/org/jboss/weld/junit/MockBean.java index 0eee1a43..f2288a4e 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,27 +25,31 @@ 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.BeanManager; import jakarta.enterprise.inject.spi.InjectionPoint; import jakarta.enterprise.inject.spi.PassivationCapable; 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; 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; @@ -64,6 +68,8 @@ */ public class MockBean implements Bean, PassivationCapable { + private static final Logger LOGGER = Logger.getLogger(MockBean.class.getName()); + /** * By default, the bean: *
    @@ -73,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
  • *
* *

@@ -124,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; @@ -146,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; @@ -167,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(); } } @@ -212,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 @@ -283,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()) { @@ -329,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; @@ -349,7 +416,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); @@ -456,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 @@ -554,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; } @@ -638,14 +735,76 @@ 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); + } + } + + } + + // 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 { @@ -670,28 +829,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() { - } - - } - } 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 8b01042a..d784db9d 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-common/src/main/java/org/jboss/weld/junit/MockInterceptor.java b/junit-common/src/main/java/org/jboss/weld/junit/MockInterceptor.java index 23729f1c..fa4f6453 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 { 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 ee065e2b..4f52f219 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/README.md b/junit-jupiter/README.md index 278ba413..52fbff8b 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 9eff1a07..375931ea 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/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 4af5f880..b6808f7a 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) { 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 00000000..4893406f --- /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 00000000..7c387f7b --- /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 00000000..368e468d --- /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 00000000..d32b327c --- /dev/null +++ b/junit-jupiter/src/test/java/org/jboss/weld/junit/jupiter/bean/MockBeanReadTest.java @@ -0,0 +1,144 @@ +/* + * 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.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +@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 00000000..6e02729a --- /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"; + } + } +} diff --git a/junit4/README.md b/junit4/README.md index 94cbbdbc..0ddeed22 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/pom.xml b/pom.xml index 26f30503..5b8e30b7 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 - 7.0.0.CR1 - 5.23.0 + 4.0.33 + 7.0.0.Final + 5.24.0 4.0.1 4.0.0-M7 2.26.1 diff --git a/spock/README.md b/spock/README.md index bb34a444..ebdf706b 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