Repository: weld/weld-junit
Pull request #134 (2022-04-11 08:13:56)

Introduce recursive search for WeldInitiator in nested test classes
* add recursive search for WeldInitiator in nested test classes
* clarify the behaviour for class hierarchies and nested tests
* add unit test for nested classes that inherit a WeldInitiator

3.x backport of #129 


Patch:
diff --git a/.github/workflows/ci-actions.yml b/.github/workflows/ci-actions.yml
index 24d27c0..f037a25 100644
--- a/.github/workflows/ci-actions.yml
+++ b/.github/workflows/ci-actions.yml
@@ -2,7 +2,7 @@ name: Weld-JUnit CI
 
 on:
   pull_request:
-    branches: [ master ]
+    branches: [ 3.0 ]
 
 jobs:
   build-weld-junit:
diff --git a/junit5/README.md b/junit5/README.md
index f304eca..9a9696f 100644
--- a/junit5/README.md
+++ b/junit5/README.md
@@ -21,6 +21,8 @@ Requirements are JUnit 5 and Java 8.
       * [Adding mock beans](#adding-mock-beans)
       * [Adding mock interceptors](#adding-mock-interceptors)
       * [Mock injection services](#mock-injection-services)
+    * [Inheritance](#inheritance-of-test-classes)
+    * [Nested test classes](#nested-test-classes)
 * [WeldJunit5AutoExtension](#weldjunit5autoextension)
   * [`@ActivateScopes`](#activatescopes)
   * [`@AddBeanClasses`](#addbeanclasses)
@@ -383,6 +385,103 @@ class MyTest {
 }
 ```
 
+#### Inheritance of test classes
+
+The `@WeldSetup` field can be defined in a superclass, but there can only be one `@WeldSetup` field in the class
+hierarchy.
+
+```java
+abstract class GenericTest {
+  @WeldSetup
+  protected WeldInitiator weldInitiator = WeldInitiator.of(Foo.class);
+}
+
+@EnableWeld
+class SpecializedTest extends GenericTest {
+
+  @Inject
+  Foo foo; // injected by the inherited Weld container
+
+  // This would throw an exception, because there would be two @WeldSetup fields in the class hierarchy
+  //
+  // @WeldSetup
+  // WeldInitiator secondWeldInitiator = WeldInitiator.of(Foo.class, Bar.class);
+}
+```
+
+#### Nested test classes
+
+A `@Nested` test class can have its own `WeldInitiator` field to configure a Weld container for the test methods of the
+nested class. If there is no `WeldInitiator` field in the class hierarchy of the nested class, the enclosing class (and
+its class hierarchy) is queried for *its* `WeldInitiator`
+field and so on until the outermost test class is reached.
+
+```java
+
+@EnableWeld
+class OuterTest {
+
+  @WeldSetup
+  WeldInitiator weld = WeldInitiator.of(Foo.class);
+
+  @Test
+  void test(Foo myFoo) {
+    assertNotNull(myFoo);
+    assertThrows(UnsatisfiedResolutionException.class, () -> outerWeld.select(Bar.class).get());
+  }
+
+  @Nested
+  class InnerTestWithoutItsOwnInitiator {
+
+    @Test
+    void testInnerMethodWithOuterWeldContainer(Foo myFoo) {
+      assertNotNull(myFoo);
+    }
+  }
+
+  @Nested
+  class InnerTestWithItsOwnInitiator {
+
+    @WeldSetup
+    WeldInitiator weld = WeldInitiator.of(Foo.class, Bar.class);
+
+    @Test
+    void testInnerMethodWithInnerWeldContainer(Foo myFoo, Bar myBar) {
+      assertNotNull(myFoo);
+      assertNotNull(myBar);
+    }
+  }
+}
+```
+
+**Attention**: There is only one Weld container for every test so no matter if the inner or the outer `WeldInitiator` field is chosen to initialize that container, it is used to fill the injection points in the nested class *and* its enclosing class(es). Therefore, you have to make sure that all beans that the outer class(es) requires are also present in the inner `WeldInitiator`!
+
+```java
+class OuterTest {
+
+  @WeldSetup
+  WeldInitiator outerWeld = WeldInitiator.of(Foo.class);
+
+  @Inject
+  Foo foo;
+
+  @Nested
+  class InnerTest {
+
+    @WeldSetup
+    WeldInitiator innerWeld = WeldInitiator.of(Bar.class);
+
+    // This test fails with an exception, because OuterTest.this.foo
+    // cannot be injected by the inner Weld container.
+    //
+    // @Test
+    // void testAnything() {
+    //   Assertions.assertTrue(true);
+    // }
+  }
+}
+```
+
 ## WeldJunit5AutoExtension
 
 To use this approach, annotate your test class with `@ExtendWith(WeldJunit5AutoExtension.class)` or just `@EnableAutoWeld`.
diff --git a/junit5/src/main/java/org/jboss/weld/junit5/WeldJunit5Extension.java b/junit5/src/main/java/org/jboss/weld/junit5/WeldJunit5Extension.java
index 2dc2065..d3ea5a1 100644
--- a/junit5/src/main/java/org/jboss/weld/junit5/WeldJunit5Extension.java
+++ b/junit5/src/main/java/org/jboss/weld/junit5/WeldJunit5Extension.java
@@ -35,9 +35,11 @@ import java.lang.reflect.Method;
 import java.security.AccessController;
 import java.security.PrivilegedAction;
 import java.util.ArrayList;
+import java.util.HashSet;
 import java.util.Collections;
 import java.util.List;
 import java.util.ServiceLoader;
+import java.util.Objects;
 import java.util.stream.Collectors;
 
 import static org.jboss.weld.junit5.ExtensionContextUtils.getContainerFromStore;
@@ -89,7 +91,7 @@ public class WeldJunit5Extension implements AfterAllCallback, BeforeAllCallback,
 
     private static void storeExplicitParamResolutionInformation(ExtensionContext ec) {
         // check system property which may have set the global explicit param injection
-        Boolean globalSettings = Boolean.valueOf(System.getProperty(GLOBAL_EXPLICIT_PARAM_INJECTION, "false"));
+        boolean globalSettings = Boolean.parseBoolean(System.getProperty(GLOBAL_EXPLICIT_PARAM_INJECTION, "false"));
         if (globalSettings) {
             setExplicitInjectionInfoToStore(ec, true);
             return;
@@ -105,14 +107,14 @@ public class WeldJunit5Extension implements AfterAllCallback, BeforeAllCallback,
     }
 
     @Override
-    public void afterAll(ExtensionContext context) throws Exception {
+    public void afterAll(ExtensionContext context) {
         if (determineTestLifecycle(context).equals(PER_CLASS)) {
             getInitiatorFromStore(context).shutdownWeld();
         }
     }
 
     @Override
-    public void beforeAll(ExtensionContext context) throws Exception {
+    public void beforeAll(ExtensionContext context) {
         // we are storing them into root context, hence only needs to be done once per test suite
         if (getEnrichersFromStore(context) == null) {
             ImmutableList.Builder<WeldJunitEnricher> enrichers = ImmutableList.builder();
@@ -124,7 +126,7 @@ public class WeldJunit5Extension implements AfterAllCallback, BeforeAllCallback,
     }
 
     @Override
-    public void afterEach(ExtensionContext context) throws Exception {
+    public void afterEach(ExtensionContext context) {
         if (determineTestLifecycle(context).equals(PER_METHOD)) {
             getInitiatorFromStore(context).shutdownWeld();
         }
@@ -196,85 +198,106 @@ public class WeldJunit5Extension implements AfterAllCallback, BeforeAllCallback,
     }
 
     @Override
-    public void beforeEach(ExtensionContext extensionContext) throws Exception {
+    public void beforeEach(ExtensionContext extensionContext) {
         startWeldContainerIfAppropriate(PER_METHOD, extensionContext);
     }
 
-    private void startWeldContainerIfAppropriate(TestInstance.Lifecycle expectedLifecycle, ExtensionContext context) throws Exception {
-        // is the lifecycle is what we expect it to be, start Weld container
+    private void startWeldContainerIfAppropriate(TestInstance.Lifecycle expectedLifecycle, ExtensionContext context) {
+        // if the lifecycle is what we expect it to be, start Weld container
         if (determineTestLifecycle(context).equals(expectedLifecycle)) {
-            Object testInstance = context.getTestInstance().orElseGet(null);
-            if (testInstance == null) {
-                throw new IllegalStateException("ExtensionContext.getTestInstance() returned empty Optional!");
-            }
+            Object testInstance = context.getRequiredTestInstance();
 
             // store info about explicit param injection, either from global settings or from annotation on the test class
             storeExplicitParamResolutionInformation(context);
 
-            // all found fields which are WeldInitiator and have @WeldSetup annotation
-            List<Field> foundInitiatorFields = new ArrayList<>();
-            WeldInitiator initiator = null;
-            // We will go through class hierarchy in search of @WeldSetup field (even private)
-            for (Class<?> clazz = testInstance.getClass(); clazz != null; clazz = clazz.getSuperclass()) {
-                // Find @WeldSetup field using getDeclaredFields() - this allows even for private fields
-                for (Field field : clazz.getDeclaredFields()) {
-                    if (field.isAnnotationPresent(WeldSetup.class)) {
-                        Object fieldInstance;
-                        try {
-                            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<Object>) () -> {
-                                field.setAccessible(true);
-                                return null;
-                            });
-                            fieldInstance = field.get(testInstance);
-                        }
-                        if (fieldInstance != null && fieldInstance instanceof WeldInitiator) {
-                            initiator = (WeldInitiator) fieldInstance;
-                            foundInitiatorFields.add(field);
-                        } else {
-                            // Field with other type than WeldInitiator was annotated with @WeldSetup
-                            throw new IllegalStateException("@WeldSetup annotation should only be used on a field of type"
-                                    + " WeldInitiator but was found on a field of type " + field.getType() + " which is declared "
-                                    + "in class " + field.getDeclaringClass());
-                        }
-                    }
-                }
-            }
-            // Multiple occurrences of @WeldSetup in the hierarchy will lead to an exception
-            if (foundInitiatorFields.size() > 1) {
-                throw new IllegalStateException(foundInitiatorFields.stream().map(f -> "Field type - " + f.getType() + " which is "
-                        + "in " + f.getDeclaringClass()).collect(Collectors.joining("\n", "Multiple @WeldSetup annotated fields found, "
-                        + "only one is allowed! Fields found:\n", "")));
-            }
+            // iterate through the testInstance, the enclosing instance (in case of nested tests),
+            // the enclosing instance of the enclosing instance (in cases of twice nested tests) and so on
+            // until we find a WeldInitiator
+            final List<Object> allTestInstances = new ArrayList<>(context.getRequiredTestInstances().getAllInstances());
+            Collections.reverse(allTestInstances); // so we can iterate from inner-most to outer-most
+            WeldInitiator initiator = allTestInstances.stream()
+                    .map(this::findInitiatorInInstance)
+                    .filter(Objects::nonNull)
+                    .findFirst()
+                    .orElseGet(() -> getDefaultInitiator(context, testInstance));
+            setInitiatorToStore(context, initiator);
 
-            // at this point we can be sure that either no or exactly one WeldInitiator was found
-            if (initiator == null) {
-                Weld weld = WeldInitiator.createWeld();
-                WeldInitiator.Builder builder = WeldInitiator.from(weld);
+            // this ensures the test class is injected into
+            // in case of nested tests, this also injects into any outer classes
+            initiator.addObjectsToInjectInto(new HashSet<>(allTestInstances));
 
-                weldInit(context, weld, builder);
+            // and finally, init Weld
+            setContainerToStore(context, initiator.initWeld(testInstance));
+        }
+    }
 
-                // Apply discovered enrichers
-                for (WeldJunitEnricher enricher : getEnrichersFromStore(context)) {
-                    String property = System.getProperty(enricher.getClass().getName());
-                    if (property == null || Boolean.parseBoolean(property)) {
-                        enricher.enrich(testInstance, context, weld, builder);
+    private WeldInitiator findInitiatorInInstance(Object testInstance) {
+        // all found fields which are WeldInitiator and have @WeldSetup annotation
+        List<Field> foundInitiatorFields = new ArrayList<>();
+        // We will go through class hierarchy in search of @WeldSetup field (even private)
+        for (Class<?> clazz = testInstance.getClass(); clazz != null; clazz = clazz.getSuperclass()) {
+            // Find @WeldSetup field using getDeclaredFields() - this allows even for private fields
+            for (Field field : clazz.getDeclaredFields()) {
+                if (field.isAnnotationPresent(WeldSetup.class)) {
+                    if (WeldInitiator.class.isAssignableFrom(field.getType())) {
+                        foundInitiatorFields.add(field);
+                    } else {
+                        // Field with other type than WeldInitiator was annotated with @WeldSetup
+                        throw new IllegalStateException("@WeldSetup annotation should only be used on a field of type"
+                                                        + " WeldInitiator but was found on a field of type "
+                                                        + field.getType() + " which is declared in class "
+                                                        + field.getDeclaringClass());
                     }
                 }
+            }
+        }
+        if (foundInitiatorFields.isEmpty()) {
+            return null;
+        }
+        // Multiple occurrences of @WeldSetup in the hierarchy will lead to an exception
+        if (foundInitiatorFields.size() > 1) {
+            final String msg = foundInitiatorFields.stream()
+                    .map(f -> "Field type of type" + f.getType() + " which is declared in " + f.getDeclaringClass())
+                    .collect(Collectors.joining("\n", "Multiple @WeldSetup annotated fields found, "
+                                                      + "only one is allowed! Fields found:\n", ""));
+            throw new IllegalStateException(msg);
+        }
 
-                initiator = builder.build();
+        Field field = foundInitiatorFields.get(0);
+        try {
+            return (WeldInitiator) field.get(testInstance);
+        } catch (IllegalAccessException e) {
+            // In case we cannot get to the field, we need to set accessibility as well
+            AccessController.doPrivileged((PrivilegedAction<Object>) () -> {
+                field.setAccessible(true);
+                return null;
+            });
+            try {
+                return (WeldInitiator) field.get(testInstance);
+            } catch (IllegalAccessException e2) {
+                // we should never get to this point, because setAccessible would have thrown earlier if access could
+                // not be granted.
             }
-            setInitiatorToStore(context, initiator);
+        }
+        return null;
+    }
 
-            // this ensures the test class is injected into
-            // in case of nested tests, this also injects into any outer classes
-            initiator.addObjectsToInjectInto(context.getRequiredTestInstances().getAllInstances().stream().collect(Collectors.toSet()));
+    private WeldInitiator getDefaultInitiator(ExtensionContext context, Object testInstance) {
 
-            // and finally, init Weld
-            setContainerToStore(context, initiator.initWeld(testInstance));
+        Weld weld = WeldInitiator.createWeld();
+        WeldInitiator.Builder builder = WeldInitiator.from(weld);
+
+        weldInit(context, weld, builder);
+
+        // Apply discovered enrichers
+        for (WeldJunitEnricher enricher : getEnrichersFromStore(context)) {
+            String property = System.getProperty(enricher.getClass().getName());
+            if (property == null || Boolean.parseBoolean(property)) {
+                enricher.enrich(testInstance, context, weld, builder);
+            }
         }
+
+        return builder.build();
     }
 
 }


Test patch:
diff --git a/junit5/src/test/java/org/jboss/weld/junit5/initiator/bean/Bar.java b/junit5/src/test/java/org/jboss/weld/junit5/initiator/bean/Bar.java
new file mode 100644
index 0000000..410a815
--- /dev/null
+++ b/junit5/src/test/java/org/jboss/weld/junit5/initiator/bean/Bar.java
@@ -0,0 +1,3 @@
+package org.jboss.weld.junit5.initiator.bean;
+
+public class Bar {}
diff --git a/junit5/src/test/java/org/jboss/weld/junit5/initiator/discovery/NestedClassesWeldInitiatorTest.java b/junit5/src/test/java/org/jboss/weld/junit5/initiator/discovery/NestedClassesWeldInitiatorTest.java
new file mode 100644
index 0000000..7e9f439
--- /dev/null
+++ b/junit5/src/test/java/org/jboss/weld/junit5/initiator/discovery/NestedClassesWeldInitiatorTest.java
@@ -0,0 +1,119 @@
+package org.jboss.weld.junit5.initiator.discovery;
+
+import jakarta.inject.Inject;
+import org.jboss.weld.exceptions.UnsatisfiedResolutionException;
+import org.jboss.weld.junit5.EnableWeld;
+import org.jboss.weld.junit5.WeldInitiator;
+import org.jboss.weld.junit5.WeldSetup;
+import org.jboss.weld.junit5.initiator.bean.Bar;
+import org.jboss.weld.junit5.initiator.bean.Foo;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Nested;
+import org.junit.jupiter.api.Test;
+
+@EnableWeld
+public class NestedClassesWeldInitiatorTest {
+
+    @WeldSetup
+    private WeldInitiator outerWeld = WeldInitiator.of(Foo.class);
+
+    @Inject
+    Foo outerFoo;
+
+    @Test
+    void testWeldWorksInOuterClass() {
+        Assertions.assertNotNull(outerFoo);
+        Assertions.assertThrows(UnsatisfiedResolutionException.class, () -> outerWeld.select(Bar.class).get());
+    }
+
+    @Nested
+    class NestedWithoutInitiatorTest {
+
+        @Inject
+        Foo innerFoo;
+
+        @Test
+        void testWeldWorksInInnerClass() {
+            Assertions.assertNotNull(outerFoo);
+            Assertions.assertNotNull(innerFoo);
+            Assertions.assertThrows(UnsatisfiedResolutionException.class, () -> outerWeld.select(Bar.class).get());
+        }
+
+        @Nested
+        class TwiceNestedWithoutInitiatorTest {
+
+            @Inject
+            Foo innerInnerFoo;
+
+            @Test
+            void testWeldWorksInSecondInnerClass() {
+                Assertions.assertNotNull(outerFoo);
+                Assertions.assertNotNull(innerFoo);
+                Assertions.assertNotNull(innerInnerFoo);
+                Assertions.assertThrows(UnsatisfiedResolutionException.class, () -> outerWeld.select(Bar.class).get());
+            }
+        }
+    }
+
+    @Nested
+    class NestedWithInitiatorTest {
+
+        @WeldSetup
+        private WeldInitiator innerWeld = WeldInitiator.of(Foo.class, Bar.class);
+
+        @Inject
+        Foo innerFoo;
+
+        @Inject
+        Bar bar;
+
+        @Test
+        void testWeldWorksInInnerClass() {
+            Assertions.assertNotNull(outerFoo);
+            Assertions.assertNotNull(bar);
+
+            // verify outer weld container is not running
+            Assertions.assertThrows(IllegalStateException.class, () -> outerWeld.select(Foo.class));
+        }
+
+        @Nested
+        class TwiceNestedWithoutInitiatorTest {
+
+            @Inject
+            Foo innerInnerFoo;
+
+            @Inject
+            Bar innerBar;
+
+            @Test
+            void testWeldWorksInSecondInnerClass() {
+                Assertions.assertNotNull(outerFoo);
+                Assertions.assertNotNull(innerFoo);
+                Assertions.assertNotNull(innerInnerFoo);
+
+                Assertions.assertNotNull(bar);
+                Assertions.assertNotNull(innerBar);
+
+                // verify outer weld container is not running
+                Assertions.assertThrows(IllegalStateException.class, () -> outerWeld.select(Foo.class));
+            }
+        }
+    }
+
+    @Nested
+    class NestedWithInheritedWeldInitiatorTest extends SuperclassWithProtectedWeldInitiator {
+
+        @Inject
+        Foo innerFoo;
+
+        @Test
+        void testInheritedTakesPrecedenceOverEnclosingWeldInitiator() {
+            Assertions.assertNotNull(outerFoo);
+            Assertions.assertNotNull(innerFoo);
+            Assertions.assertThrows(UnsatisfiedResolutionException.class, () -> weld.select(Bar.class).get());
+
+            // verify outer weld container is not running
+            Assertions.assertThrows(IllegalStateException.class, () -> outerWeld.select(Foo.class));
+        }
+    }
+}