This is an automated email from the ASF dual-hosted git repository.

veithen pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/ws-axiom.git


The following commit(s) were added to refs/heads/master by this push:
     new e64e560c6 Refine MatrixTestNode design to use Guice injector hierarchy
e64e560c6 is described below

commit e64e560c622d69d2ae4bc4438767232c6cb80f5a
Author: Andreas Veithen-Knowles <[email protected]>
AuthorDate: Mon Feb 23 13:14:33 2026 +0000

    Refine MatrixTestNode design to use Guice injector hierarchy
    
    - MatrixTestNode.toDynamicNodes() now receives a Guice Injector parameter
    - MatrixTestContainer creates child injectors binding each Dimension 
instance
    - MatrixTestCase uses the injector to instantiate test classes and runs them
      via TestCase.runBare()
    - Test cases use @Inject fields instead of constructor parameters
    - Added examples showing the injector hierarchy, migrated test classes, and
      builder usage for both small (SAAJ) and large (OM) test suites
    - Removed the hybrid JUnit 5 adapter section (no longer needed since test
      classes keep the existing TestCase structure)
---
 docs/design/test-suite-pattern.md | 365 +++++++++++++++++++++++++++++++-------
 1 file changed, 296 insertions(+), 69 deletions(-)

diff --git a/docs/design/test-suite-pattern.md 
b/docs/design/test-suite-pattern.md
index 5578d00f7..c19a688af 100644
--- a/docs/design/test-suite-pattern.md
+++ b/docs/design/test-suite-pattern.md
@@ -217,22 +217,35 @@ The larger suites present additional considerations:
     output into `DynamicTest` instances would allow consuming modules to 
migrate to
     JUnit 5 runners without rewriting test case classes.
 
-### Replacement for MatrixTestSuiteBuilder: MatrixTestNode tree
+### Replacement for MatrixTestSuiteBuilder: MatrixTestNode tree with Guice
 
 Since `DynamicContainer` and `DynamicTest` are `final` in JUnit 5, they cannot 
be
 subclassed to attach test parameters for LDAP-style filtering. Instead, a 
parallel
-class hierarchy can act as a factory for `DynamicNode` instances while 
carrying the
+class hierarchy acts as a factory for `DynamicNode` instances while carrying 
the
 parameters needed for exclusion filtering.
 
+Tests continue to be structured as one test case per class extending
+`junit.framework.TestCase`, with each class overriding `runTest()`. Rather than
+receiving test parameters via constructor arguments, test cases declare their
+dependencies using `@Inject` annotations and are instantiated by Google Guice.
+`MatrixTestContainer` builds a Guice injector hierarchy — one child injector 
per
+dimension value — so that by the time a leaf `MatrixTestCase` is reached, the
+accumulated injector can satisfy all `@Inject` dependencies for the test case 
class.
+
 #### Class hierarchy
 
 ```java
 /**
  * Base class mirroring {@link DynamicNode}. Represents a node in the test tree
  * that can be filtered before conversion to JUnit 5's dynamic test API.
+ *
+ * <p>The {@code parentInjector} parameter threads through the tree: each
+ * {@link MatrixTestContainer} creates child injectors from it, and each
+ * {@link MatrixTestCase} uses it to instantiate the test class.
  */
 abstract class MatrixTestNode {
-    abstract Stream<DynamicNode> toDynamicNodes(Dictionary<String, String> 
inheritedParameters,
+    abstract Stream<DynamicNode> toDynamicNodes(Injector parentInjector,
+            Dictionary<String, String> inheritedParameters,
             List<Filter> excludes);
 }
 ```
@@ -245,23 +258,28 @@ abstract class MatrixTestNode {
  * <p>A {@code MatrixTestContainer} holds a list of {@link Dimension} instances
  * of a given type (e.g. all {@code SOAPSpec} values, or all
  * {@code SerializationStrategy} values). When {@link #toDynamicNodes} is 
called,
- * it produces one {@link DynamicContainer} per dimension instance. Each
- * dimension's test parameters are extracted at that point to determine the
- * display name and to merge into the inherited parameter dictionary. Because a
- * {@code Dimension} may contribute multiple test parameters (for example,
- * {@code SerializeToWriter} adds both {@code serializationStrategy=Writer} and
- * {@code cache=true}), these parameters also determine the display name. The
- * full parameter dictionary for any leaf {@code MatrixTestCase} is the
- * accumulation of parameters from its ancestor {@code MatrixTestContainer}
- * chain.
+ * it produces one {@link DynamicContainer} per dimension instance. For each
+ * dimension instance, a <em>child Guice injector</em> is created from the
+ * parent injector, binding the dimension type to that specific instance. This
+ * child injector is then propagated to all children, forming a hierarchy where
+ * each path from root to leaf accumulates all dimension bindings.
+ *
+ * <p>Because a {@code Dimension} may contribute multiple test parameters (for
+ * example, {@code SerializeToWriter} adds both {@code 
serializationStrategy=Writer}
+ * and {@code cache=true}), the test parameters extracted from each dimension
+ * determine both the display name and the filter dictionary. The full 
parameter
+ * dictionary for any leaf {@code MatrixTestCase} is the accumulation of
+ * parameters from its ancestor {@code MatrixTestContainer} chain.
  *
  * @param <D> the dimension type
  */
 class MatrixTestContainer<D extends Dimension> extends MatrixTestNode {
+    private final Class<D> dimensionType;
     private final List<D> dimensions;
     private final List<MatrixTestNode> children = new ArrayList<>();
 
-    MatrixTestContainer(List<D> dimensions) {
+    MatrixTestContainer(Class<D> dimensionType, List<D> dimensions) {
+        this.dimensionType = dimensionType;
         this.dimensions = dimensions;
     }
 
@@ -270,9 +288,19 @@ class MatrixTestContainer<D extends Dimension> extends 
MatrixTestNode {
     }
 
     @Override
-    Stream<DynamicNode> toDynamicNodes(Dictionary<String, String> 
inheritedParameters,
+    Stream<DynamicNode> toDynamicNodes(Injector parentInjector,
+            Dictionary<String, String> inheritedParameters,
             List<Filter> excludes) {
         return dimensions.stream().map(dimension -> {
+            // Create a child injector that binds this dimension value.
+            Injector childInjector = parentInjector.createChildInjector(new 
AbstractModule() {
+                @Override
+                protected void configure() {
+                    bind(dimensionType).toInstance(dimension);
+                }
+            });
+
+            // Extract test parameters from the dimension for display and 
filtering.
             Map<String, String> parameters = new LinkedHashMap<>();
             dimension.addTestParameters(new TestParameterTarget() {
                 @Override
@@ -302,7 +330,7 @@ class MatrixTestContainer<D extends Dimension> extends 
MatrixTestNode {
                     .collect(Collectors.joining(", "));
             return DynamicContainer.dynamicContainer(displayName,
                     children.stream()
-                            .flatMap(child -> child.toDynamicNodes(params, 
excludes)));
+                            .flatMap(child -> 
child.toDynamicNodes(childInjector, params, excludes)));
         });
     }
 }
@@ -310,32 +338,184 @@ class MatrixTestContainer<D extends Dimension> extends 
MatrixTestNode {
 
 ```java
 /**
- * Mirrors {@link DynamicTest}. A leaf test case with an executable body.
- * Filtering is applied based on the accumulated parameters from ancestor
- * containers plus the test class name.
+ * Mirrors {@link DynamicTest}. A leaf node that instantiates a
+ * {@link junit.framework.TestCase} subclass via Guice and executes it.
+ *
+ * <p>The test class must have an injectable constructor (either a no-arg
+ * constructor or one annotated with {@code @Inject}). Field injection is
+ * also supported. The injector received from the ancestor
+ * {@code MatrixTestContainer} chain will have bindings for all dimension
+ * types, plus any implementation-level bindings from the root injector
+ * (e.g. {@code OMMetaFactory}, {@code SAAJImplementation}).
+ *
+ * <p>Once the instance is created, it is executed via {@link 
TestCase#runBare()},
+ * which invokes the full {@code setUp()} → {@code runTest()} → {@code 
tearDown()}
+ * lifecycle.
  */
 class MatrixTestCase extends MatrixTestNode {
-    private final Class<?> testClass;
-    private final Executable executable;
+    private final Class<? extends TestCase> testClass;
 
-    MatrixTestCase(Class<?> testClass, Executable executable) {
+    MatrixTestCase(Class<? extends TestCase> testClass) {
         this.testClass = testClass;
-        this.executable = executable;
     }
 
     @Override
-    Stream<DynamicNode> toDynamicNodes(Dictionary<String, String> 
inheritedParameters,
+    Stream<DynamicNode> toDynamicNodes(Injector injector,
+            Dictionary<String, String> inheritedParameters,
             List<Filter> excludes) {
         for (Filter exclude : excludes) {
             if (exclude.matches(testClass, inheritedParameters)) {
                 return Stream.empty(); // Excluded
             }
         }
-        return Stream.of(DynamicTest.dynamicTest(testClass.getSimpleName(), 
executable));
+        return Stream.of(DynamicTest.dynamicTest(testClass.getSimpleName(), () 
-> {
+            TestCase testInstance = injector.getInstance(testClass);
+            testInstance.setName(testClass.getSimpleName());
+            testInstance.runBare();
+        }));
+    }
+}
+```
+
+#### Guice injector hierarchy
+
+The injector hierarchy mirrors the `MatrixTestContainer` nesting. The root 
injector
+is created by the consumer and binds implementation-level objects. Each
+`MatrixTestContainer` level creates one child injector per dimension value, 
binding
+the dimension type. By the time a leaf `MatrixTestCase` is reached, the 
injector
+can satisfy all `@Inject` dependencies.
+
+```
+Root Injector
+  binds: SAAJImplementation → instance
+  │
+  ├─ Child Injector (SOAPSpec → SOAP11)
+  │    │
+  │    ├─ MatrixTestCase → 
injector.getInstance(TestAddChildElementReification.class)
+  │    │                   → testInstance.runBare()
+  │    └─ MatrixTestCase → injector.getInstance(TestGetOwnerDocument.class)
+  │                        → testInstance.runBare()
+  │
+  └─ Child Injector (SOAPSpec → SOAP12)
+       │
+       ├─ MatrixTestCase → 
injector.getInstance(TestAddChildElementReification.class)
+       │                   → testInstance.runBare()
+       └─ MatrixTestCase → injector.getInstance(TestGetOwnerDocument.class)
+                           → testInstance.runBare()
+```
+
+For suites with multiple dimensions the nesting deepens:
+
+```
+Root Injector
+  binds: OMMetaFactory → instance
+  │
+  ├─ Child Injector (SOAPSpec → SOAP11)
+  │    │
+  │    ├─ Child Injector (SerializationStrategy → SerializeToWriter)
+  │    │    └─ MatrixTestCase → injector has {OMMetaFactory, SOAPSpec, 
SerializationStrategy}
+  │    │
+  │    └─ Child Injector (SerializationStrategy → SerializeToOutputStream)
+  │         └─ MatrixTestCase → injector has {OMMetaFactory, SOAPSpec, 
SerializationStrategy}
+  │
+  └─ Child Injector (SOAPSpec → SOAP12)
+       └─ ...
+```
+
+#### What test case classes look like
+
+Test case classes continue to extend `junit.framework.TestCase` (or a 
domain-specific
+subclass) and override `runTest()`. The key difference is that dependencies 
are injected
+by Guice rather than passed via constructor arguments.
+
+**Before (constructor parameters):**
+
+```java
+public class TestAddChildElementReification extends SAAJTestCase {
+    public TestAddChildElementReification(SAAJImplementation 
saajImplementation, SOAPSpec spec) {
+        super(saajImplementation, spec);
+    }
+
+    @Override
+    protected void runTest() throws Throwable {
+        SOAPBody body = newMessageFactory().createMessage().getSOAPBody();
+        SOAPElement child = body.addChildElement(
+                (SOAPElement) 
body.getOwnerDocument().createElementNS("urn:test", "p:test"));
+        assertThat(child).isInstanceOf(SOAPBodyElement.class);
+    }
+}
+```
+
+**After (Guice injection):**
+
+```java
+public class TestAddChildElementReification extends SAAJTestCase {
+    @Override
+    protected void runTest() throws Throwable {
+        SOAPBody body = newMessageFactory().createMessage().getSOAPBody();
+        SOAPElement child = body.addChildElement(
+                (SOAPElement) 
body.getOwnerDocument().createElementNS("urn:test", "p:test"));
+        assertThat(child).isInstanceOf(SOAPBodyElement.class);
+    }
+}
+```
+
+The intermediate base class `SAAJTestCase` uses `@Inject` for its dependencies:
+
+```java
+public abstract class SAAJTestCase extends TestCase {
+    @Inject protected SAAJImplementation saajImplementation;
+    @Inject protected SOAPSpec spec;
+
+    protected final MessageFactory newMessageFactory() throws SOAPException {
+        return 
spec.getAdapter(FactorySelector.class).newMessageFactory(saajImplementation);
+    }
+
+    protected final SOAPFactory newSOAPFactory() throws SOAPException {
+        return 
spec.getAdapter(FactorySelector.class).newSOAPFactory(saajImplementation);
+    }
+}
+```
+
+For the larger axiom-testsuite, intermediate classes follow the same pattern:
+
+```java
+public abstract class AxiomTestCase extends TestCase {
+    @Inject protected OMMetaFactory metaFactory;
+}
+
+public abstract class SOAPTestCase extends AxiomTestCase {
+    @Inject protected SOAPSpec spec;
+    protected SOAPFactory soapFactory;
+
+    @Override
+    protected void setUp() throws Exception {
+        super.setUp();
+        soapFactory = 
spec.getAdapter(FactorySelector.class).getFactory(metaFactory);
     }
 }
 ```
 
+Test cases with multiple dimensions simply inject all of them:
+
+```java
+public class TestSerializeWithXSITypeAttribute extends SOAPTestCase {
+    @Inject private SerializationStrategy serializationStrategy;
+
+    @Override
+    protected void runTest() throws Throwable {
+        XML xml = serializationStrategy.serialize(
+                soapFactory.getDefaultSOAPEnvelope().getBody());
+        // ... assertions ...
+    }
+}
+```
+
+Note that the existing `Dimension.addTestParameters()` mechanism is **not** 
used by test
+case classes at all. Parameters are extracted only by `MatrixTestContainer` 
for display
+names and filter matching. Test cases interact with dimensions purely as typed 
objects
+obtained through injection.
+
 #### How filtering works
 
 Each `MatrixTestContainer` level holds a list of `Dimension` instances (e.g. 
all
@@ -344,34 +524,99 @@ called, each container produces one `DynamicContainer` 
per dimension instance, a
 parameters accumulate from the root down:
 
 ```
-MatrixTestContainer([SOAPSpec.SOAP11, SOAPSpec.SOAP12])
-  MatrixTestContainer([SerializeToWriter(cache=true), ...])
-    MatrixTestCase(SerializeElement.class, ...)
-    MatrixTestCase(SerializeDocument.class, ...)
-
-→ spec=soap11
-    → serializationStrategy=Writer, cache=true
-        → SerializeElement  (filtered against {spec=soap11, 
serializationStrategy=Writer, cache=true})
-        → SerializeDocument (filtered against {spec=soap11, 
serializationStrategy=Writer, cache=true})
+MatrixTestContainer(SOAPSpec.class, [SOAPSpec.SOAP11, SOAPSpec.SOAP12])
+  MatrixTestContainer(SerializationStrategy.class, [SerializeToWriter, ...])
+    MatrixTestCase(TestSerializeElement.class)
+    MatrixTestCase(TestSerializeDocument.class)
+
+→ spec=soap11                                      [child injector binds 
SOAPSpec]
+    → serializationStrategy=Writer, cache=true      [child injector binds 
SerializationStrategy]
+        → TestSerializeElement  (filtered against {spec=soap11, 
serializationStrategy=Writer, cache=true})
+        → TestSerializeDocument (filtered against {spec=soap11, 
serializationStrategy=Writer, cache=true})
   spec=soap12
     → serializationStrategy=Writer, cache=true
         → ...
 ```
 
-Note that `SerializeToWriter` contributes two parameters 
(`serializationStrategy` and
-`cache`) to a single container level, matching the existing `Dimension` 
contract where
-`addTestParameters()` may call `addTestParameter()` multiple times.
+Consumers apply exclusions and create the root injector:
+
+```java
+class SAAJRITests {
+    @TestFactory
+    Stream<DynamicNode> saajTests() {
+        SAAJImplementation impl = new SAAJImplementation(new 
SAAJMetaFactoryImpl());
+        Injector rootInjector = Guice.createInjector(new AbstractModule() {
+            @Override
+            protected void configure() {
+                bind(SAAJImplementation.class).toInstance(impl);
+            }
+        });
+
+        MatrixTestContainer<SOAPSpec> root = new MatrixTestContainer<>(
+                SOAPSpec.class, Multiton.getInstances(SOAPSpec.class));
+        root.addChild(new 
MatrixTestCase(TestAddChildElementReification.class));
+        root.addChild(new 
MatrixTestCase(TestExamineMustUnderstandHeaderElements.class));
+        root.addChild(new MatrixTestCase(TestAddChildElementLocalName.class));
+        root.addChild(new 
MatrixTestCase(TestAddChildElementLocalNamePrefixAndURI.class));
+        root.addChild(new MatrixTestCase(TestSetParentElement.class));
+        root.addChild(new MatrixTestCase(TestGetOwnerDocument.class));
+
+        List<Filter> excludes = new ArrayList<>();
+        return root.toDynamicNodes(rootInjector, new Hashtable<>(), excludes);
+    }
+}
+```
 
-Consumers apply exclusions exactly as they do today:
+For large suites, a builder class constructs the tree:
+
+```java
+class OMTestTreeBuilder {
+    private final OMMetaFactory metaFactory;
+
+    OMTestTreeBuilder(OMMetaFactory metaFactory) {
+        this.metaFactory = metaFactory;
+    }
+
+    Injector createRootInjector() {
+        return Guice.createInjector(new AbstractModule() {
+            @Override
+            protected void configure() {
+                bind(OMMetaFactory.class).toInstance(metaFactory);
+            }
+        });
+    }
+
+    MatrixTestNode build() {
+        // Top-level container groups by SOAPSpec
+        MatrixTestContainer<SOAPSpec> bySpec = new MatrixTestContainer<>(
+                SOAPSpec.class, Multiton.getInstances(SOAPSpec.class));
+
+        // Nested container groups by SerializationStrategy
+        MatrixTestContainer<SerializationStrategy> bySerialization = new 
MatrixTestContainer<>(
+                SerializationStrategy.class,
+                Multiton.getInstances(SerializationStrategy.class));
+        bySerialization.addChild(new 
MatrixTestCase(TestSerializeWithXSITypeAttribute.class));
+        bySerialization.addChild(new 
MatrixTestCase(TestSerializeDocument.class));
+        bySpec.addChild(bySerialization);
+
+        // Tests that only vary by SOAPSpec (no serialization dimension)
+        bySpec.addChild(new MatrixTestCase(TestGetSOAPVersion.class));
+
+        return bySpec;
+    }
+}
+```
 
 ```java
 class OMImplementationTests {
     @TestFactory
     Stream<DynamicNode> omTests() {
-        MatrixTestContainer<?> root = new 
OMTestTreeBuilder(metaFactory).build();
+        OMTestTreeBuilder builder = new OMTestTreeBuilder(myMetaFactory);
+        Injector rootInjector = builder.createRootInjector();
+        MatrixTestNode root = builder.build();
         List<Filter> excludes = new ArrayList<>();
-        excludes.add(Filter.forClass(TestSerialize.class, "(spec=soap12)"));
-        return root.toDynamicNodes(new Hashtable<>(), excludes);
+        excludes.add(Filter.forClass(TestSerializeDocument.class, 
"(spec=soap12)"));
+        return root.toDynamicNodes(rootInjector, new Hashtable<>(), excludes);
     }
 }
 ```
@@ -386,32 +631,14 @@ class OMImplementationTests {
 *   Uses standard JUnit 5 `DynamicNode` for execution while keeping the 
filtering
     infrastructure in the intermediate `MatrixTestNode` layer.
 *   The LDAP-style filter mechanism is preserved unchanged.
-
-### Hybrid approach: JUnit 5 adapter for MatrixTestSuiteBuilder
-
-A pragmatic intermediate step would be to create a JUnit 5 adapter that 
converts a
-`MatrixTestSuiteBuilder` into a `@TestFactory` method, allowing consumers to 
use JUnit 5
-without rewriting the test suites themselves:
-
-```java
-public abstract class MatrixTestFactory {
-    protected abstract MatrixTestSuiteBuilder createBuilder();
-
-    @TestFactory
-    Stream<DynamicTest> tests() {
-        TestSuite suite = createBuilder().build();
-        return Collections.list(suite.tests()).stream()
-                .map(test -> DynamicTest.dynamicTest(test.toString(), () -> {
-                    TestResult result = new TestResult();
-                    test.run(result);
-                    if (result.failureCount() > 0) {
-                        throw (Throwable) result.failures().nextElement()
-                                .thrownException();
-                    }
-                }));
-    }
-}
-```
-
-This would allow the project to migrate runners (consuming modules) to JUnit 5
-incrementally while preserving the existing test suite infrastructure.
+*   **Guice injection decouples test cases from the tree structure.** Test 
cases declare
+    what they need (`@Inject SOAPSpec spec`) without knowing how or where in 
the tree
+    hierarchy that binding is provided. Adding a new dimension to the tree 
does not
+    require changing test case constructors.
+*   **No boilerplate parameter passing in builders.** The current pattern 
requires each
+    `addTest()` call to manually pass all dimension values to the test 
constructor.
+    With Guice, `MatrixTestCase` only needs the test class; the injector 
supplies
+    everything.
+*   **Test case base classes become simpler.** Intermediate classes like 
`SAAJTestCase`,
+    `SOAPTestCase`, and `AxiomTestCase` no longer need constructor parameters 
or chains
+    of `super(...)` calls — they simply declare `@Inject` fields.

Reply via email to