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 37d1c04b5 Add design document for reusable test suite parameterization 
pattern
37d1c04b5 is described below

commit 37d1c04b5cb4d1d33e68d108b8b000ad718e01a2
Author: Andreas Veithen-Knowles <[email protected]>
AuthorDate: Sun Feb 22 10:44:50 2026 +0000

    Add design document for reusable test suite parameterization pattern
---
 docs/design/README.md             |   1 +
 docs/design/test-suite-pattern.md | 268 ++++++++++++++++++++++++++++++++++++++
 2 files changed, 269 insertions(+)

diff --git a/docs/design/README.md b/docs/design/README.md
index 4308e0f3c..dedcaefe7 100644
--- a/docs/design/README.md
+++ b/docs/design/README.md
@@ -23,3 +23,4 @@ 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
new file mode 100644
index 000000000..ad90d7b28
--- /dev/null
+++ b/docs/design/test-suite-pattern.md
@@ -0,0 +1,268 @@
+<!--
+  ~ 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`, and 
`axiom-testsuite`
+defines tests for the Axiom API that can be executed against any 
`OMMetaFactory`.
+
+Most test suites also execute tests across multiple dimensions. For instance, 
SAAJ and
+SOAP tests run against both SOAP 1.1 and SOAP 1.2, while `OMTestSuiteBuilder` 
multiplies
+tests across XML samples, serialization strategies, builder factories, and 
more.
+
+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.
+
+*   **Multiton / Adaptable / AdapterFactory** — a custom SPI framework in the 
`multiton`
+    module. `SOAPSpec` is a `Multiton` with instances `SOAP11` and `SOAP12`. 
Adapters
+    (such as `FactorySelector`) are registered via `AdapterFactory` 
implementations
+    discovered through `ServiceLoader` (using `@AutoService`).
+
+### 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.  `SOAPSpecAdapterFactory` implements `AdapterFactory<SOAPSpec>` and 
registers a
+    `FactorySelector` adapter for each SOAP version, mapping `SOAPSpec.SOAP11` 
to
+    `SOAPConstants.SOAP_1_1_PROTOCOL` and `SOAPSpec.SOAP12` to
+    `SOAPConstants.SOAP_1_2_PROTOCOL`.
+
+4.  `SAAJTestCase` provides convenience methods `newMessageFactory()` and
+    `newSOAPFactory()` that look up the `FactorySelector` adapter for the 
current
+    `SOAPSpec` and delegate to it.
+
+5.  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 current pattern 
requires:
+
+| 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 |
+| `FactorySelector.java` | Adapter interface (`@AdapterType`) |
+| `SOAPSpecAdapterFactory.java` | Adapter factory registering 
`FactorySelector` per SOAP version |
+| `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 |
+
+### Usage across the project
+
+The same pattern is used at much larger scale in other modules:
+
+| Module | Builder | `addTest()` calls | Estimated runtime tests |
+|--------|---------|-------------------|------------------------|
+| axiom-testsuite | `OMTestSuiteBuilder` | ~452 | Thousands (combinatorial) |
+| axiom-testsuite | `SOAPTestSuiteBuilder` | ~197 | Hundreds |
+| dom-testsuite | `DOMTestSuiteBuilder` | Many | Hundreds |
+| saaj-testsuite | `SAAJTestSuiteBuilder` | 6 | 12 |
+
+## 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 Stream.of(SOAPSpec.SOAP11, SOAPSpec.SOAP12).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", () -> {
+            SOAPBody body = 
newMessageFactory(spec).createMessage().getSOAPBody();
+            SOAPElement child = body.addChildElement(
+                    (SOAPElement) 
body.getOwnerDocument().createElementNS("urn:test", "p:test"));
+            assertThat(child).isInstanceOf(SOAPBodyElement.class);
+        });
+    }
+
+    // ... other test methods ...
+
+    private MessageFactory newMessageFactory(SOAPSpec spec) throws 
SOAPException {
+        String protocol = spec == SOAPSpec.SOAP11
+                ? SOAPConstants.SOAP_1_1_PROTOCOL
+                : SOAPConstants.SOAP_1_2_PROTOCOL;
+        return impl.newMessageFactory(protocol);
+    }
+}
+```
+
+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>` |
+| SOAP version parameterization | Multiton adapter SPI (`FactorySelector`, 
`SOAPSpecAdapterFactory`, `@AdapterType`, `@AutoService`) | 
`Stream.of(SOAPSpec.SOAP11, SOAPSpec.SOAP12)` or a simple `if`/`switch` |
+| One class per test case | Required | Not required — tests are methods 
returning `DynamicTest` |
+| Boilerplate for saaj-testsuite | 12 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 | Conditional 
logic, `@DisabledIf`, or `Assumptions.assumeThat()` |
+| Reusability across implementations | Subclass `TestCase` + pass factory to 
builder | Subclass base test class + pass factory to constructor |
+| Custom infrastructure needed | `MatrixTestSuiteBuilder`, `MatrixTestCase`, 
`Multiton`, `Adaptable`, `AdapterFactory`, `Adapters` | None (built into JUnit 
5) |
+
+## Considerations for migration
+
+### saaj-testsuite (small suite)
+
+For the saaj-testsuite specifically, migrating to JUnit 5 `@TestFactory` would:
+
+*   Eliminate the `FactorySelector` adapter interface, 
`SOAPSpecAdapterFactory`, and the
+    `@AutoService` dependency for adapter registration.
+*   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 12 to approximately 3 (`SAAJImplementation`, 
`SAAJTests`,
+    `SAAJRITests`).
+
+The `SAAJImplementation` class (which uses reflection to access protected 
methods on
+`SAAJMetaFactory`) would be retained as-is.
+
+### Large suites (OMTestSuiteBuilder, SOAPTestSuiteBuilder)
+
+The larger suites present additional considerations:
+
+*   The one-class-per-test pattern, while verbose, keeps each test 
independently
+    navigable and organizes tests by the API area they cover.
+*   The exclusion mechanism (LDAP filters on test parameters) is heavily used 
by
+    consumers to skip known-failing tests for specific implementations. A 
JUnit 5
+    equivalent would need to provide comparable functionality.
+*   The sheer number of test case classes (hundreds) means migration would be a
+    substantial effort.
+*   A phased approach is possible: a JUnit 5 adapter that wraps 
`MatrixTestSuiteBuilder`
+    output into `DynamicTest` instances would allow consuming modules to 
migrate to
+    JUnit 5 runners without rewriting test case classes.
+
+### 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.

Reply via email to