Repository: weld/weld-junit (java)

Issue:
Better support for @Nested classes
Currently, if one has a JUnit5 test class with a `@WeldSetup WeldInitiator` field and a `@Nested` subclass, the Setup-Field of the enclosing class is not picked up by the nested class. Every nested class seems to need its own Setup-Field.

Example application:
```
@ApplicationScoped
public class HelloService {

    @Inject
    Helper helper;

    public String sayHello() {
        return "Hello " + helper.getName();
    }
}

@Dependent
public class Helper {

    public String getName() {
        return "World";
    }
}
```
Example test:
```
@ExtendWith(WeldJunit5Extension.class)
class HelloServiceTest {

    @WeldSetup
    WeldInitiator weldInitiator = configureWeld();

    private WeldInitiator configureWeld() {
        return WeldInitiator.from(HelloService.class)
                .addBeans(MockBean.of(Mockito.mock(Helper.class), Helper.class))
                .build();
    }

    @Inject
    Helper helper;

    @Inject
    HelloService service;

    @Test
    void testSayHello() {
        when(helper.getName()).thenReturn("Mock");
        final String actual = service.sayHello();
        assertThat(actual).isEqualTo("Hello Mock");
    }

    @Nested
    class NestedTest{

        @WeldSetup
        WeldInitiator weldInitiator = configureWeld(); // (!)

        @Test
        void testSayHelloAgain() {
            when(helper.getName()).thenReturn("nested Mock");
            final String actual = service.sayHello();
            assertThat(actual).isEqualTo("Hello nested Mock");
        }
    }
}
```

Not including the field marked (!) will lead to a `DeploymentException`:
```
WELD-001408: Unsatisfied dependencies for type Helper with qualifiers @Default
  at injection point [BackedAnnotatedField] @Inject org.example.HelloServiceTest.helper
  at org.example.HelloServiceTest.helper(HelloServiceTest.java:0)
```
which is (at least for me) confusing, because there obviously is a CDI-Container running, because it is throwing the DeploymentException. I just assumed that if a container is running, then the `@ExtendWith` on the enclosing class was picked up, and therefore the `@WeldSetup` field should be picked up too. Obviously that was not the case.

In hindsight, I can guess what happens under the hood: The extension only looks for fields in the test class and `HelloServiceTest.weldInitiator` is not a field of `HelloServiceTest$NestedTest`.

But it would be nice to have this behaviour at least documented. Better yet, the error message could suggest to define an appropriate field for every nested test class. Even better yet: The extension could detect that a test class is nested and check the instance of the enclosing itself.

Pull request description:
add recursive search for WeldInitiator in nested test classes
Will resolve #129 by modifying the search for `@WeldSetup WeldInitiator` fields so that `@Nested` test instances are searched first, then their enclosing instance, then *their* enclosing instance etc. until an Initiator is found. This way, nested test classes can re-use their parent's configuration by doing nothing and overwrite their parent's configuration by defining their own `@WeldSetup WeldInitiator` field.

Patch:
diff --git a/junit5/README.md b/junit5/README.md
index a208394..751316f 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));
+        }
+    }
+}