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 5be18c92f Add matrix-testsuite docs and remove design document
5be18c92f is described below

commit 5be18c92f814640ea92082957d300043a5eb90b0
Author: Andreas Veithen-Knowles <[email protected]>
AuthorDate: Sun Mar 1 07:30:45 2026 +0000

    Add matrix-testsuite docs and remove design document
    
    Add README.md with an overview of the new MatrixTestSuite framework
    and migration.md with step-by-step instructions for migrating from
    MatrixTestSuiteBuilder. Delete the design document that preceded the
    implementation.
---
 docs/design/README.md                 |   1 -
 docs/design/test-suite-pattern.md     | 612 ----------------------------------
 testing/matrix-testsuite/README.md    | 199 +++++++++++
 testing/matrix-testsuite/migration.md | 290 ++++++++++++++++
 4 files changed, 489 insertions(+), 613 deletions(-)

diff --git a/docs/design/README.md b/docs/design/README.md
index dedcaefe7..4308e0f3c 100644
--- a/docs/design/README.md
+++ b/docs/design/README.md
@@ -23,4 +23,3 @@ Design documents
 | Title/link | Status |
 | ---------- | ------ |
 | [OSGi integration and separation between API and 
implementation](osgi-integration.md) | Implemented |
-| [Reusable test suites and parameterization](test-suite-pattern.md) | In 
review |
diff --git a/docs/design/test-suite-pattern.md 
b/docs/design/test-suite-pattern.md
deleted file mode 100644
index f239b1778..000000000
--- a/docs/design/test-suite-pattern.md
+++ /dev/null
@@ -1,612 +0,0 @@
-<!--
-  ~ Licensed to the Apache Software Foundation (ASF) under one
-  ~ or more contributor license agreements. See the NOTICE file
-  ~ distributed with this work for additional information
-  ~ regarding copyright ownership. The ASF licenses this file
-  ~ to you under the Apache License, Version 2.0 (the
-  ~ "License"); you may not use this file except in compliance
-  ~ with the License. You may obtain a copy of the License at
-  ~
-  ~ http://www.apache.org/licenses/LICENSE-2.0
-  ~
-  ~ Unless required by applicable law or agreed to in writing,
-  ~ software distributed under the License is distributed on an
-  ~ "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
-  ~ KIND, either express or implied. See the License for the
-  ~ specific language governing permissions and limitations
-  ~ under the License.
-  -->
-
-Reusable test suites and parameterization
-=========================================
-
-## Introduction
-
-The Axiom project provides reusable API test suites that can be applied to 
different
-implementations of the same API. For example, `saaj-testsuite` defines tests 
for the
-SAAJ API that can be executed against any `SAAJMetaFactory`.
-
-Test suites also execute tests across multiple dimensions. For instance, SAAJ 
tests
-run against both SOAP 1.1 and SOAP 1.2.
-
-This document examines the current pattern used to implement these test suites 
and
-evaluates whether JUnit 5's `@TestFactory` mechanism offers a better approach.
-
-## Current pattern: MatrixTestSuiteBuilder (JUnit 3)
-
-### Infrastructure classes
-
-The current pattern is built on the following custom infrastructure in the 
`testutils`
-module:
-
-*   **`MatrixTestCase`** — extends `junit.framework.TestCase`. Each test case 
is a
-    separate class that overrides `runTest()`. Test parameters (e.g. SOAP 
version) are
-    added via `addTestParameter(name, value)`, which appends `[name=value]` to 
the test
-    name for display purposes.
-
-*   **`MatrixTestSuiteBuilder`** — builds a `junit.framework.TestSuite` by 
collecting
-    `MatrixTestCase` instances via `addTest()` calls in the abstract 
`addTests()` method.
-    Supports exclusions using LDAP-style filters on test parameters.
-
-### How it works in saaj-testsuite
-
-The saaj-testsuite uses this pattern as follows:
-
-1.  Each test case is a separate class extending `SAAJTestCase` (which extends
-    `MatrixTestCase`). For example, `TestAddChildElementReification`,
-    `TestExamineMustUnderstandHeaderElements`, etc.
-
-2.  `SAAJTestSuiteBuilder` extends `MatrixTestSuiteBuilder`. Its `addTests()` 
method
-    calls a private `addTests(SOAPSpec)` helper for both `SOAPSpec.SOAP11` and
-    `SOAPSpec.SOAP12`, instantiating each test case class with the SAAJ 
implementation
-    and the SOAP spec.
-
-3.  `SAAJTestCase` provides convenience methods `newMessageFactory()` and
-    `newSOAPFactory()` that create the appropriate factory for the current 
SOAP version.
-
-4.  Consumers create a JUnit 3 runner class with a `static suite()` method:
-
-    ```java
-    public class SAAJRITest extends TestCase {
-        public static TestSuite suite() throws Exception {
-            return new SAAJTestSuiteBuilder(new SAAJMetaFactoryImpl()).build();
-        }
-    }
-    ```
-
-### File inventory for saaj-testsuite
-
-For 6 test cases × 2 SOAP versions = 12 test instances, the main files are:
-
-| File | Role |
-|------|------|
-| `SAAJTestCase.java` | Abstract base class for all SAAJ tests |
-| `SAAJTestSuiteBuilder.java` | Suite builder; registers all tests × SOAP 
versions |
-| `SAAJImplementation.java` | Wraps `SAAJMetaFactory` with reflective access |
-| `TestAddChildElementReification.java` | Test case class |
-| `TestAddChildElementLocalName.java` | Test case class |
-| `TestAddChildElementLocalNamePrefixAndURI.java` | Test case class |
-| `TestSetParentElement.java` | Test case class |
-| `TestGetOwnerDocument.java` | Test case class |
-| `TestExamineMustUnderstandHeaderElements.java` | Test case class |
-| `SAAJRITest.java` | JUnit 3 runner for the reference implementation |
-
-## Alternative: JUnit 5 @TestFactory + DynamicTest
-
-JUnit 5 provides a built-in mechanism for dynamic test generation that directly
-addresses the same use case.
-
-### Key JUnit 5 features
-
-*   **`@TestFactory`** — a method that returns a `Stream<DynamicNode>` (or 
`Collection`,
-    `Iterable`, etc.). Each `DynamicNode` becomes a test in the test tree.
-
-*   **`DynamicContainer`** — groups `DynamicNode` instances under a named 
container,
-    enabling hierarchical test organization (e.g. grouping by SOAP version).
-
-*   **`DynamicTest`** — a named test with an `Executable` body. Replaces the 
need for
-    a separate class per test case.
-
-*   **`@ParameterizedTest`** + `@MethodSource` — an alternative for simpler
-    parameterization where each test method receives parameters directly.
-
-### What saaj-testsuite would look like
-
-The reusable test suite module would define an abstract class:
-
-```java
-public abstract class SAAJTests {
-    private final SAAJImplementation impl;
-
-    protected SAAJTests(SAAJMetaFactory metaFactory) {
-        this.impl = new SAAJImplementation(metaFactory);
-    }
-
-    @TestFactory
-    Stream<DynamicContainer> saajTests() {
-        return Multiton.getInstances(SOAPSpec.class).stream().map(spec ->
-            DynamicContainer.dynamicContainer(spec.getName(), Stream.of(
-                testAddChildElementReification(spec),
-                testExamineMustUnderstandHeaderElements(spec),
-                testAddChildElementLocalName(spec),
-                testAddChildElementLocalNamePrefixAndURI(spec),
-                testSetParentElement(spec),
-                testGetOwnerDocument(spec)
-            ))
-        );
-    }
-
-    private DynamicTest testAddChildElementReification(SOAPSpec spec) {
-        return DynamicTest.dynamicTest("addChildElementReification", () -> {
-            MessageFactory mf = spec.getAdapter(FactorySelector.class)
-                    .newMessageFactory(impl);
-            SOAPBody body = mf.createMessage().getSOAPBody();
-            SOAPElement child = body.addChildElement(
-                    (SOAPElement) 
body.getOwnerDocument().createElementNS("urn:test", "p:test"));
-            assertThat(child).isInstanceOf(SOAPBodyElement.class);
-        });
-    }
-
-    // ... other test methods ...
-}
-```
-
-Consumers would subclass per implementation:
-
-```java
-class SAAJRITests extends SAAJTests {
-    SAAJRITests() {
-        super(new SAAJMetaFactoryImpl());
-    }
-}
-```
-
-### Comparison
-
-| Concern | MatrixTestSuiteBuilder (JUnit 3) | JUnit 5 @TestFactory |
-|---------|----------------------------------|----------------------|
-| Framework version | JUnit 3 | JUnit 5 (Jupiter) |
-| Test registration | Explicit `addTest()` in builder | Return 
`Stream<DynamicNode>` |
-| One class per test case | Required | Not required — tests are methods 
returning `DynamicTest` |
-| Boilerplate for saaj-testsuite | 10 files | 2–3 files |
-| Test tree in IDE | Flat list with `[spec=SOAP11]` in name | Nested: SOAP11 > 
testName, SOAP12 > testName |
-| Exclusion mechanism | LDAP filter on parameter dictionary | LDAP filter on 
`MatrixTestNode` tree (see below) |
-| Reusability across implementations | Subclass `TestCase` + pass factory to 
builder | Subclass base test class + pass factory to constructor |
-| Custom infrastructure needed | `MatrixTestSuiteBuilder`, `MatrixTestCase` | 
None (built into JUnit 5) |
-
-## Considerations for migration
-
-### saaj-testsuite
-
-For the saaj-testsuite, migrating to JUnit 5 `@TestFactory` would:
-
-*   Collapse 6 test case classes into methods within a single class.
-*   Remove the need for `SAAJTestSuiteBuilder` entirely.
-*   Replace the `SAAJTestCase` base class with a simpler abstract class.
-*   Reduce the file count from 10 to approximately 3 (`SAAJImplementation`, 
`SAAJTests`,
-    `SAAJRITests`).
-
-The `SAAJImplementation` class (which uses reflection to access protected 
methods on
-`SAAJMetaFactory`) would be retained as-is.
-
-### 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 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 `MatrixTest` 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
- * fan-out node ({@link DimensionFanOutNode}, {@link ParameterFanOutNode}) 
creates
- * child injectors from it, and each {@link MatrixTest} uses it to instantiate
- * the test class.
- */
-public abstract class MatrixTestNode {
-    abstract Stream<DynamicNode> toDynamicNodes(Injector parentInjector,
-            Dictionary<String, String> inheritedParameters,
-            MatrixTestFilters excludes);
-}
-```
-
-```java
-/**
- * Abstract base class for fan-out nodes that iterate over a list of values,
- * creating one {@link DynamicContainer} per value. For each value, a child
- * Guice injector is created that binds the value type to the specific 
instance.
- *
- * <p>Subclasses define how test parameters (used for display names and LDAP
- * filter matching) are extracted from each value:
- * <ul>
- *   <li>{@link DimensionFanOutNode} — for types that implement {@link 
Dimension},
- *       using {@link Dimension#addTestParameters}.
- *   <li>{@link ParameterFanOutNode} — for arbitrary types, using a 
caller-supplied
- *       parameter name and {@link Function}.
- * </ul>
- *
- * @param <T> the value type
- */
-public abstract class AbstractFanOutNode<T> extends MatrixTestNode {
-    private final Class<T> type;
-    private final List<T> values;
-    private final List<MatrixTestNode> children = new ArrayList<>();
-
-    protected AbstractFanOutNode(Class<T> type, List<T> values) {
-        this.type = type;
-        this.values = values;
-    }
-
-    public void addChild(MatrixTestNode child) {
-        children.add(child);
-    }
-
-    /**
-     * Extracts test parameters from the given value. The returned map entries
-     * are used for the display name and for LDAP filter matching.
-     */
-    protected abstract Map<String, String> extractParameters(T value);
-
-    @Override
-    Stream<DynamicNode> toDynamicNodes(Injector parentInjector,
-            Dictionary<String, String> inheritedParameters,
-            MatrixTestFilters excludes) {
-        return values.stream().map(value -> {
-            Injector childInjector = parentInjector.createChildInjector(new 
AbstractModule() {
-                @Override
-                protected void configure() {
-                    bind(type).toInstance(value);
-                }
-            });
-
-            Map<String, String> parameters = extractParameters(value);
-            Hashtable<String, String> params = new Hashtable<>();
-            for (Enumeration<String> e = inheritedParameters.keys(); 
e.hasMoreElements(); ) {
-                String key = e.nextElement();
-                params.put(key, inheritedParameters.get(key));
-            }
-            parameters.forEach(params::put);
-            String displayName = parameters.entrySet().stream()
-                    .map(e -> e.getKey() + "=" + e.getValue())
-                    .collect(Collectors.joining(", "));
-            return DynamicContainer.dynamicContainer(displayName,
-                    children.stream()
-                            .flatMap(child -> 
child.toDynamicNodes(childInjector, params, excludes)));
-        });
-    }
-}
-```
-
-```java
-/**
- * Fan-out node for types that implement {@link Dimension}. Parameters are
- * extracted via {@link Dimension#addTestParameters}.
- *
- * <p>For types that do <em>not</em> implement {@code Dimension}, use
- * {@link ParameterFanOutNode} instead.
- *
- * @param <D> the dimension type
- */
-public class DimensionFanOutNode<D extends Dimension> extends 
AbstractFanOutNode<D> {
-    public DimensionFanOutNode(Class<D> dimensionType, List<D> dimensions) {
-        super(dimensionType, dimensions);
-    }
-
-    @Override
-    protected Map<String, String> extractParameters(D dimension) {
-        Map<String, String> parameters = new LinkedHashMap<>();
-        dimension.addTestParameters(new TestParameterTarget() {
-            @Override
-            public void addTestParameter(String name, String value) {
-                parameters.put(name, value);
-            }
-
-            @Override
-            public void addTestParameter(String name, boolean value) {
-                addTestParameter(name, String.valueOf(value));
-            }
-
-            @Override
-            public void addTestParameter(String name, int value) {
-                addTestParameter(name, String.valueOf(value));
-            }
-        });
-        return parameters;
-    }
-}
-```
-
-```java
-/**
- * Fan-out node for arbitrary value types that do not implement {@link 
Dimension}.
- * The caller supplies a parameter name and a {@link Function} that maps each
- * value to its parameter value (used for display names and LDAP filter 
matching).
- *
- * <p>For example, {@code SOAPSpec} does not implement {@code Dimension}, so
- * it is handled with:
- *
- * <pre>
- * new ParameterFanOutNode&lt;&gt;(SOAPSpec.class,
- *         Multiton.getInstances(SOAPSpec.class),
- *         "spec", SOAPSpec::getName)
- * </pre>
- *
- * @param <T> the value type
- */
-public class ParameterFanOutNode<T> extends AbstractFanOutNode<T> {
-    private final String parameterName;
-    private final Function<T, String> parameterValueFunction;
-
-    public ParameterFanOutNode(Class<T> type, List<T> values,
-            String parameterName, Function<T, String> parameterValueFunction) {
-        super(type, values);
-        this.parameterName = parameterName;
-        this.parameterValueFunction = parameterValueFunction;
-    }
-
-    @Override
-    protected Map<String, String> extractParameters(T value) {
-        return Map.of(parameterName, parameterValueFunction.apply(value));
-    }
-}
-```
-
-```java
-/**
- * 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 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.
- */
-public class MatrixTest extends MatrixTestNode {
-    private final Class<? extends TestCase> testClass;
-
-    public MatrixTest(Class<? extends TestCase> testClass) {
-        this.testClass = testClass;
-    }
-
-    @Override
-    Stream<DynamicNode> toDynamicNodes(Injector injector,
-            Dictionary<String, String> inheritedParameters,
-            MatrixTestFilters excludes) {
-        if (excludes.test(testClass, inheritedParameters)) {
-            return Stream.empty(); // Excluded
-        }
-        return Stream.of(DynamicTest.dynamicTest(testClass.getSimpleName(), () 
-> {
-            TestCase testInstance = injector.getInstance(testClass);
-            testInstance.setName(testClass.getSimpleName());
-            testInstance.runBare();
-        }));
-    }
-}
-```
-
-```java
-/**
- * Root of a test suite. Owns the Guice root injector and the tree of
- * {@link MatrixTestNode} instances. Provides a {@link 
#toDynamicNodes(MatrixTestFilters)}
- * method that converts the tree to JUnit 5 dynamic nodes, applying the
- * supplied exclusion filters.
- *
- * <p>Exclusion filters are <em>not</em> owned by the suite itself because
- * they are specific to each consumer (implementation under test), whereas
- * the suite structure and bindings are defined by the test suite author.
- */
-public class MatrixTestSuite {
-    private final Injector rootInjector;
-    private final List<MatrixTestNode> children = new ArrayList<>();
-
-    public MatrixTestSuite(Module... modules) {
-        this.rootInjector = Guice.createInjector(modules);
-    }
-
-    public void addChild(MatrixTestNode child) {
-        children.add(child);
-    }
-
-    public Stream<DynamicNode> toDynamicNodes(MatrixTestFilters excludes) {
-        return children.stream()
-                .flatMap(child -> child.toDynamicNodes(
-                        rootInjector, new Hashtable<>(), excludes));
-    }
-}
-```
-
-#### Guice injector hierarchy
-
-The injector hierarchy mirrors the fan-out node nesting. The root injector is 
created
-by the consumer and binds implementation-level objects. Each 
`DimensionFanOutNode` or
-`ParameterFanOutNode` level creates one child injector per value, binding the 
value type.
-By the time a leaf `MatrixTest` is reached, the injector can satisfy all 
`@Inject`
-dependencies.
-
-```
-Root Injector
-  binds: SAAJImplementation → instance
-  │
-  ├─ Child Injector (SOAPSpec → SOAP11)
-  │    │
-  │    ├─ MatrixTest → 
injector.getInstance(TestAddChildElementReification.class)
-  │    │               → testInstance.runBare()
-  │    └─ MatrixTest → injector.getInstance(TestGetOwnerDocument.class)
-  │                    → testInstance.runBare()
-  │
-  └─ Child Injector (SOAPSpec → SOAP12)
-       │
-       ├─ MatrixTest → 
injector.getInstance(TestAddChildElementReification.class)
-       │               → testInstance.runBare()
-       └─ MatrixTest → injector.getInstance(TestGetOwnerDocument.class)
-                           → testInstance.runBare()
-```
-
-#### 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);
-    }
-}
-```
-
-Note that the existing `Dimension.addTestParameters()` mechanism is **not** 
used by test
-case classes at all. Parameters are extracted only by `DimensionFanOutNode` 
(via
-`Dimension.addTestParameters()`) or `ParameterFanOutNode` (via the supplied 
function) for
-display names and filter matching. Test cases interact with values purely as 
typed
-objects obtained through injection.
-
-#### How filtering works
-
-Each fan-out node level holds a list of values. When `toDynamicNodes()` is 
called,
-each node produces one `DynamicContainer` per value, and parameters accumulate 
from
-the root down. For types that implement `Dimension`, use 
`DimensionFanOutNode`; for
-arbitrary types (like `SOAPSpec`), use `ParameterFanOutNode`:
-
-```
-ParameterFanOutNode(SOAPSpec.class, [SOAPSpec.SOAP11, SOAPSpec.SOAP12], 
"spec", SOAPSpec::getName)
-  MatrixTest(TestAddChildElementReification.class)
-  MatrixTest(TestGetOwnerDocument.class)
-
-→ spec=soap11                          [child injector binds SOAPSpec]
-    → TestAddChildElementReification   (filtered against {spec=soap11})
-    → TestGetOwnerDocument             (filtered against {spec=soap11})
-  spec=soap12
-    → TestAddChildElementReification   (filtered against {spec=soap12})
-    → TestGetOwnerDocument             (filtered against {spec=soap12})
-```
-
-The **test suite author** (in `saaj-testsuite`) defines the suite structure — 
which
-test classes to include, which dimensions to iterate over, and what Guice 
bindings
-are needed — accepting only the implementation-specific factory as a parameter:
-
-```java
-public class SAAJTestSuite {
-    public static MatrixTestSuite create(SAAJMetaFactory metaFactory) {
-        SAAJImplementation impl = new SAAJImplementation(metaFactory);
-        MatrixTestSuite suite = new MatrixTestSuite(new AbstractModule() {
-            @Override
-            protected void configure() {
-                bind(SAAJImplementation.class).toInstance(impl);
-            }
-        });
-
-        ParameterFanOutNode<SOAPSpec> specs = new ParameterFanOutNode<>(
-            SOAPSpec.class, Multiton.getInstances(SOAPSpec.class),
-            "spec", SOAPSpec::getName);
-        specs.addChild(new MatrixTest(TestAddChildElementReification.class));
-        specs.addChild(new 
MatrixTest(TestExamineMustUnderstandHeaderElements.class));
-        specs.addChild(new MatrixTest(TestAddChildElementLocalName.class));
-        specs.addChild(new 
MatrixTest(TestAddChildElementLocalNamePrefixAndURI.class));
-        specs.addChild(new MatrixTest(TestSetParentElement.class));
-        specs.addChild(new MatrixTest(TestGetOwnerDocument.class));
-        suite.addChild(specs);
-
-        return suite;
-    }
-}
-```
-
-The **consumer** (in the implementation module) supplies the concrete factory 
and
-any implementation-specific exclusion filters:
-
-```java
-class SAAJRITests {
-    @TestFactory
-    Stream<DynamicNode> saajTests() {
-        MatrixTestSuite suite = SAAJTestSuite.create(new 
SAAJMetaFactoryImpl());
-        MatrixTestFilters excludes = MatrixTestFilters.builder()
-                .add(TestGetOwnerDocument.class, "(spec=soap12)")
-                .build();
-        return suite.toDynamicNodes(excludes);
-    }
-}
-```
-
-#### Benefits over MatrixTestSuiteBuilder
-
-*   Produces a hierarchical test tree in the IDE (grouped by dimension) 
instead of a
-    flat list with parameter suffixes in the test name.
-*   Parameters are distributed across the tree (one `Dimension` per container 
level,
-    possibly contributing multiple parameters) rather than accumulated on each 
leaf
-    test case, making the structure explicit.
-*   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.
-*   **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, `MatrixTest` only needs the test class; the injector supplies
-    everything.
-*   **Test case base classes become simpler.** `SAAJTestCase` no longer needs
-    constructor parameters or chains of `super(...)` calls — it simply declares
-    `@Inject` fields.
diff --git a/testing/matrix-testsuite/README.md 
b/testing/matrix-testsuite/README.md
new file mode 100644
index 000000000..c8d6599d6
--- /dev/null
+++ b/testing/matrix-testsuite/README.md
@@ -0,0 +1,199 @@
+<!--
+  ~ Licensed to the Apache Software Foundation (ASF) under one
+  ~ or more contributor license agreements. See the NOTICE file
+  ~ distributed with this work for additional information
+  ~ regarding copyright ownership. The ASF licenses this file
+  ~ to you under the Apache License, Version 2.0 (the
+  ~ "License"); you may not use this file except in compliance
+  ~ with the License. You may obtain a copy of the License at
+  ~
+  ~ http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing,
+  ~ software distributed under the License is distributed on an
+  ~ "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+  ~ KIND, either express or implied. See the License for the
+  ~ specific language governing permissions and limitations
+  ~ under the License.
+  -->
+
+# Matrix Test Suite Framework
+
+## Overview
+
+The matrix-testsuite module provides infrastructure for building reusable,
+parameterized test suites that can be applied to different implementations of 
the
+same API. It combines JUnit 5's `@TestFactory` / `DynamicNode` mechanism with
+Google Guice dependency injection to produce hierarchical, filterable test 
trees.
+
+## Key concepts
+
+### Test tree
+
+A test suite is a tree of `MatrixTestNode` instances. Interior nodes are
+**fan-out nodes** that iterate over a list of dimension values, creating one
+`DynamicContainer` per value. Leaf nodes are **`MatrixTest`** instances that
+instantiate and run a `junit.framework.TestCase` subclass.
+
+### Guice injector hierarchy
+
+Each fan-out level creates a child Guice injector that binds its value type to 
the
+current value. By the time a leaf `MatrixTest` is reached, the accumulated 
injector
+can satisfy all `@Inject` dependencies declared by the test case class.
+
+```
+Root Injector
+  binds: implementation-level objects
+  │
+  ├─ Child Injector (DimensionA → value1)
+  │    ├─ MatrixTest → injector.getInstance(SomeTestCase.class) → runBare()
+  │    └─ MatrixTest → injector.getInstance(AnotherTestCase.class) → runBare()
+  │
+  └─ Child Injector (DimensionA → value2)
+       ├─ MatrixTest → injector.getInstance(SomeTestCase.class) → runBare()
+       └─ MatrixTest → injector.getInstance(AnotherTestCase.class) → runBare()
+```
+
+### Filtering
+
+Parameters accumulate from the root down through the tree. At each leaf, the
+accumulated parameter dictionary is checked against `MatrixTestFilters` — an
+immutable set of LDAP-style filter expressions optionally scoped to a test 
class.
+Excluded tests produce an empty `Stream<DynamicNode>` and do not appear in the
+test tree.
+
+## Classes
+
+### `MatrixTestNode`
+
+Abstract base class for all nodes in the test tree. Defines a single method:
+
+```java
+abstract Stream<DynamicNode> toDynamicNodes(
+        Injector parentInjector,
+        Dictionary<String, String> inheritedParameters,
+        MatrixTestFilters excludes);
+```
+
+### `AbstractFanOutNode<T>`
+
+Abstract fan-out node that iterates over a list of values of type `T`. For each
+value, it:
+
+1. Creates a child Guice injector binding `T` to the value.
+2. Extracts test parameters (via the abstract `extractParameters` method).
+3. Produces a `DynamicContainer` containing the results of recursing into its
+   child nodes.
+
+Subclasses:
+
+- **`DimensionFanOutNode<D extends Dimension>`** — for types that implement the
+  `Dimension` interface. Parameters are extracted via
+  `Dimension.addTestParameters()`.
+
+- **`ParameterFanOutNode<T>`** — for arbitrary types. The caller supplies a
+  parameter name and a `Function<T, String>` to extract the parameter value.
+
+### `MatrixTest`
+
+Leaf node. Instantiates a `junit.framework.TestCase` subclass via Guice and
+executes it through `TestCase.runBare()` (which runs the full `setUp()` →
+`runTest()` → `tearDown()` lifecycle). The test is skipped if matched by the
+exclusion filters.
+
+### `MatrixTestSuite`
+
+Root of a test suite. Owns the Guice root injector (created from 
caller-supplied
+modules) and a list of top-level `MatrixTestNode` children. Provides:
+
+```java
+public Stream<DynamicNode> toDynamicNodes(MatrixTestFilters excludes)
+```
+
+### `MatrixTestFilters`
+
+Immutable set of exclusion filters. Each filter entry optionally constrains by
+test class and/or an LDAP filter expression on the parameter dictionary (using
+OSGi's `FrameworkUtil.createFilter()`). Built via 
`MatrixTestFilters.builder()`.
+
+## Writing a test case
+
+Test cases extend `junit.framework.TestCase` (or a domain-specific subclass) 
and
+override `runTest()`. Dependencies are declared with `@Inject` — either on 
fields
+or via constructor. The test case does **not** receive parameters through its
+constructor and does **not** call `addTestParameter()`.
+
+```java
+public abstract class MyTestCase extends TestCase {
+    @Inject protected SomeImplementation impl;
+    @Inject protected SomeDimension dimension;
+
+    // convenience methods using impl and dimension ...
+}
+```
+
+```java
+public class TestSomeBehavior extends MyTestCase {
+    @Override
+    protected void runTest() throws Throwable {
+        // test logic using inherited injected fields
+    }
+}
+```
+
+## Defining a test suite
+
+The test suite author creates a factory method that builds a `MatrixTestSuite`,
+adds fan-out nodes for each dimension, and registers test classes as 
`MatrixTest`
+leaf nodes:
+
+```java
+public class MyTestSuite {
+    public static MatrixTestSuite create(SomeFactory factory) {
+        SomeImplementation impl = new SomeImplementation(factory);
+        MatrixTestSuite suite = new MatrixTestSuite(new AbstractModule() {
+            @Override
+            protected void configure() {
+                bind(SomeImplementation.class).toInstance(impl);
+            }
+        });
+
+        ParameterFanOutNode<SomeDimension> dimensions = new 
ParameterFanOutNode<>(
+                SomeDimension.class,
+                Multiton.getInstances(SomeDimension.class),
+                "dimension",
+                SomeDimension::getName);
+        dimensions.addChild(new MatrixTest(TestSomeBehavior.class));
+        dimensions.addChild(new MatrixTest(TestOtherBehavior.class));
+        suite.addChild(dimensions);
+
+        return suite;
+    }
+}
+```
+
+## Consuming a test suite
+
+Consumers create a JUnit 5 test class with a `@TestFactory` method:
+
+```java
+class MyImplTest {
+    @TestFactory
+    Stream<DynamicNode> tests() {
+        MatrixTestSuite suite = MyTestSuite.create(new MyFactoryImpl());
+        MatrixTestFilters excludes = MatrixTestFilters.builder()
+                .add(TestSomeBehavior.class, "(dimension=problematicValue)")
+                .build();
+        return suite.toDynamicNodes(excludes);
+    }
+}
+```
+
+## Legacy classes
+
+The following classes from the old JUnit 3 based framework still exist in this
+package but are deprecated and will be removed once all test suites have been
+migrated:
+
+- `MatrixTestCase`
+- `MatrixTestSuiteBuilder`
diff --git a/testing/matrix-testsuite/migration.md 
b/testing/matrix-testsuite/migration.md
new file mode 100644
index 000000000..a04d10c96
--- /dev/null
+++ b/testing/matrix-testsuite/migration.md
@@ -0,0 +1,290 @@
+<!--
+  ~ Licensed to the Apache Software Foundation (ASF) under one
+  ~ or more contributor license agreements. See the NOTICE file
+  ~ distributed with this work for additional information
+  ~ regarding copyright ownership. The ASF licenses this file
+  ~ to you under the Apache License, Version 2.0 (the
+  ~ "License"); you may not use this file except in compliance
+  ~ with the License. You may obtain a copy of the License at
+  ~
+  ~ http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing,
+  ~ software distributed under the License is distributed on an
+  ~ "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+  ~ KIND, either express or implied. See the License for the
+  ~ specific language governing permissions and limitations
+  ~ under the License.
+  -->
+
+# Migration guide: MatrixTestSuiteBuilder → MatrixTestSuite
+
+This document describes how to migrate a test suite from the old
+`MatrixTestSuiteBuilder` / `MatrixTestCase` pattern (JUnit 3) to the new
+`MatrixTestSuite` / `MatrixTestNode` pattern (JUnit 5 + Guice).
+
+For a completed example of this migration, see the `saaj-testsuite` module.
+
+## Prerequisites
+
+The module being migrated must depend on:
+
+```xml
+<dependency>
+    <groupId>org.junit.jupiter</groupId>
+    <artifactId>junit-jupiter</artifactId>
+</dependency>
+<dependency>
+    <groupId>com.google.inject</groupId>
+    <artifactId>guice</artifactId>
+</dependency>
+```
+
+These are already declared in the root POM's `<dependencyManagement>`.
+
+## Step-by-step migration
+
+### 1. Update the test case base class
+
+The domain-specific base class (e.g. `SAAJTestCase`) currently:
+
+- Extends `MatrixTestCase`
+- Accepts all dimension values and implementation objects via constructor
+  parameters
+- Calls `addTestParameter(name, value)` in the constructor
+
+**Change it to:**
+
+- Extend `junit.framework.TestCase` directly
+- Declare dependencies as `@Inject` fields (using `com.google.inject.Inject`)
+- Remove the constructor entirely (or make it no-arg)
+- Remove all `addTestParameter()` calls
+
+**Before:**
+
+```java
+public abstract class SAAJTestCase extends MatrixTestCase {
+    protected final SAAJImplementation saajImplementation;
+    protected final SOAPSpec spec;
+
+    public SAAJTestCase(SAAJImplementation saajImplementation, SOAPSpec spec) {
+        this.saajImplementation = saajImplementation;
+        this.spec = spec;
+        addTestParameter("spec", spec.getName());
+    }
+
+    protected final MessageFactory newMessageFactory() throws SOAPException {
+        return 
spec.getAdapter(FactorySelector.class).newMessageFactory(saajImplementation);
+    }
+}
+```
+
+**After:**
+
+```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);
+    }
+}
+```
+
+### 2. Update each test case class
+
+Each test case class currently accepts constructor parameters and forwards them
+to the base class.
+
+**Remove the constructor.** The `runTest()` method stays unchanged. Any imports
+of the implementation class and dimension types that were only used in the
+constructor can be removed.
+
+**Before:**
+
+```java
+public class TestAddChildElementReification extends SAAJTestCase {
+    public TestAddChildElementReification(SAAJImplementation 
saajImplementation, SOAPSpec spec) {
+        super(saajImplementation, spec);
+    }
+
+    @Override
+    protected void runTest() throws Throwable {
+        // ... test logic unchanged ...
+    }
+}
+```
+
+**After:**
+
+```java
+public class TestAddChildElementReification extends SAAJTestCase {
+    @Override
+    protected void runTest() throws Throwable {
+        // ... test logic unchanged ...
+    }
+}
+```
+
+### 3. Replace the suite builder with a suite factory
+
+The old `*TestSuiteBuilder` class extends `MatrixTestSuiteBuilder` and 
overrides
+`addTests()` to register test instances for each dimension combination.
+
+**Replace it** with a class that has a static factory method returning a
+`MatrixTestSuite`. The factory method:
+
+1. Creates a `MatrixTestSuite` with a Guice module that binds
+   implementation-level objects.
+2. Creates fan-out nodes for each dimension.
+3. Adds `MatrixTest` leaf nodes for each test case class.
+
+Use `ParameterFanOutNode` for types that don't implement `Dimension` 
(supplying a
+parameter name and a function to extract the display value). Use
+`DimensionFanOutNode` for types that implement `Dimension`.
+
+**Before:**
+
+```java
+public class SAAJTestSuiteBuilder extends MatrixTestSuiteBuilder {
+    private final SAAJImplementation saajImplementation;
+
+    public SAAJTestSuiteBuilder(SAAJMetaFactory metaFactory) {
+        saajImplementation = new SAAJImplementation(metaFactory);
+    }
+
+    @Override
+    protected void addTests() {
+        addTests(SOAPSpec.SOAP11);
+        addTests(SOAPSpec.SOAP12);
+    }
+
+    private void addTests(SOAPSpec spec) {
+        addTest(new TestAddChildElementReification(saajImplementation, spec));
+        addTest(new TestGetOwnerDocument(saajImplementation, spec));
+        // ...
+    }
+}
+```
+
+**After:**
+
+```java
+public class SAAJTestSuite {
+    public static MatrixTestSuite create(SAAJMetaFactory metaFactory) {
+        SAAJImplementation impl = new SAAJImplementation(metaFactory);
+        MatrixTestSuite suite = new MatrixTestSuite(new AbstractModule() {
+            @Override
+            protected void configure() {
+                bind(SAAJImplementation.class).toInstance(impl);
+            }
+        });
+
+        ParameterFanOutNode<SOAPSpec> specs = new ParameterFanOutNode<>(
+                SOAPSpec.class,
+                Multiton.getInstances(SOAPSpec.class),
+                "spec",
+                SOAPSpec::getName);
+        specs.addChild(new MatrixTest(TestAddChildElementReification.class));
+        specs.addChild(new MatrixTest(TestGetOwnerDocument.class));
+        // ...
+        suite.addChild(specs);
+
+        return suite;
+    }
+}
+```
+
+Key differences:
+
+- Test classes are registered **once** as `MatrixTest` instances under the
+  appropriate fan-out node, rather than once per dimension combination.
+- Dimension values are listed via `Multiton.getInstances()` (or an explicit 
list)
+  in the fan-out node, not iterated manually.
+- No constructor arguments are passed to test classes.
+
+### 4. Replace the consumer test class
+
+The old consumer class uses JUnit 3's `static suite()` method.
+
+**Replace it** with a JUnit 5 class that has a `@TestFactory` method returning
+`Stream<DynamicNode>`.
+
+**Before:**
+
+```java
+public class SAAJRITest extends TestCase {
+    public static TestSuite suite() throws Exception {
+        return new SAAJTestSuiteBuilder(new SAAJMetaFactoryImpl()).build();
+    }
+}
+```
+
+**After:**
+
+```java
+public class SAAJRITest {
+    @TestFactory
+    public Stream<DynamicNode> saajTests() {
+        return SAAJTestSuite.create(new SAAJMetaFactoryImpl())
+                .toDynamicNodes(MatrixTestFilters.builder().build());
+    }
+}
+```
+
+### 5. Migrate exclusions
+
+If the old consumer called `exclude()` on the builder, convert those calls to
+`MatrixTestFilters.builder().add(...)` entries.
+
+**Before:**
+
+```java
+SAAJTestSuiteBuilder builder = new SAAJTestSuiteBuilder(factory);
+builder.exclude(TestGetOwnerDocument.class, "(spec=soap12)");
+builder.exclude(TestSomething.class);
+builder.exclude("(parser=StAX)");
+return builder.build();
+```
+
+**After:**
+
+```java
+MatrixTestFilters excludes = MatrixTestFilters.builder()
+        .add(TestGetOwnerDocument.class, "(spec=soap12)")
+        .add(TestSomething.class)
+        .add("(parser=StAX)")
+        .build();
+return MyTestSuite.create(factory).toDynamicNodes(excludes);
+```
+
+The filter syntax and semantics are identical.
+
+### 6. Update dependencies in pom.xml
+
+Add `junit-jupiter` and `guice` to the module's `<dependencies>`. If the module
+uses `Multiton.getInstances()`, also add a dependency on the `multiton` module.
+
+Remove any dependency on `junit:junit` if no code in the module still uses 
JUnit 3
+or 4 APIs directly. (Note: test case classes still extend
+`junit.framework.TestCase`, which comes from `junit:junit` transitively through
+`matrix-testsuite`.)
+
+### 7. Delete the old builder class
+
+The old `*TestSuiteBuilder` class can be deleted once the new `*TestSuite` 
factory
+is in place and all consumers have been updated.
+
+## Checklist
+
+- [ ] Base test case class: extends `TestCase`, uses `@Inject` fields, no
+      constructor
+- [ ] All test case classes: constructor removed, `runTest()` unchanged
+- [ ] Suite factory class: creates `MatrixTestSuite` with Guice module, builds
+      fan-out tree with `MatrixTest` leaves
+- [ ] Consumer test class: uses `@TestFactory` returning `Stream<DynamicNode>`
+- [ ] Exclusions: converted to `MatrixTestFilters.builder()` calls
+- [ ] `pom.xml`: `junit-jupiter`, `guice`, and (if needed) `multiton` added
+- [ ] Old builder class deleted
+- [ ] Tests pass: `mvn clean test -pl <module> -am`


Reply via email to