From a45e9e90651a660fc3e64471c8b00bc13e7e716b Mon Sep 17 00:00:00 2001 From: cmaddela Date: Thu, 24 Jul 2025 18:00:35 -0500 Subject: [PATCH 01/14] JUnit 5 support. --- sdks/java/core/build.gradle | 13 ++ .../apache/beam/sdk/testing/TestPipeline.java | 33 +++- .../sdk/testing/TestPipelineExtension.java | 186 ++++++++++++++++++ .../JUnit4to5MigrationExampleTest.java | 109 ++++++++++ .../TestPipelineExtensionAdvancedTest.java | 88 +++++++++ .../testing/TestPipelineExtensionTest.java | 56 ++++++ 6 files changed, 479 insertions(+), 6 deletions(-) create mode 100644 sdks/java/core/src/main/java/org/apache/beam/sdk/testing/TestPipelineExtension.java create mode 100644 sdks/java/core/src/test/java/org/apache/beam/sdk/testing/JUnit4to5MigrationExampleTest.java create mode 100644 sdks/java/core/src/test/java/org/apache/beam/sdk/testing/TestPipelineExtensionAdvancedTest.java create mode 100644 sdks/java/core/src/test/java/org/apache/beam/sdk/testing/TestPipelineExtensionTest.java diff --git a/sdks/java/core/build.gradle b/sdks/java/core/build.gradle index a1f2916f1958..5dffa4583399 100644 --- a/sdks/java/core/build.gradle +++ b/sdks/java/core/build.gradle @@ -107,6 +107,13 @@ dependencies { shadowTest library.java.everit_json_schema provided library.java.junit provided library.java.hamcrest + // Add JUnit 5 support + provided platform('org.junit:junit-bom:5.10.0') + provided 'org.junit.jupiter:junit-jupiter-api' + provided 'org.junit.jupiter:junit-jupiter-engine' + testImplementation platform('org.junit:junit-bom:5.10.0') + testImplementation 'org.junit.jupiter:junit-jupiter-api' + testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine' provided 'io.airlift:aircompressor:0.18' provided 'com.facebook.presto.hadoop:hadoop-apache2:3.2.0-1' provided library.java.zstd_jni @@ -130,3 +137,9 @@ project.tasks.compileTestJava { // TODO: fix other places with warnings in tests and delete this option options.compilerArgs += ['-Xlint:-rawtypes'] } + +// Configure test task to use both JUnit 4 and JUnit 5 +test { + useJUnitPlatform() + useJUnit() +} diff --git a/sdks/java/core/src/main/java/org/apache/beam/sdk/testing/TestPipeline.java b/sdks/java/core/src/main/java/org/apache/beam/sdk/testing/TestPipeline.java index 328bf19c466c..5591a2b76f5b 100644 --- a/sdks/java/core/src/main/java/org/apache/beam/sdk/testing/TestPipeline.java +++ b/sdks/java/core/src/main/java/org/apache/beam/sdk/testing/TestPipeline.java @@ -82,7 +82,11 @@ * * *

Use {@link PAssert} for tests, as it integrates with this test harness in both direct and - * remote execution modes. For example: + * remote execution modes. + * + *

JUnit 4 Usage

+ * + * For JUnit 4 tests, use this class as a TestRule: * *

  * {@literal @Rule}
@@ -97,6 +101,23 @@
  *  }
  * 
* + *

JUnit 5 Usage

+ * + * For JUnit 5 tests, use {@link TestPipelineExtension}: + * + *

+ * {@literal @ExtendWith}(TestPipelineExtension.class)
+ * class MyPipelineTest {
+ *   {@literal @Test}
+ *   {@literal @Category}(NeedsRunner.class)
+ *   void myPipelineTest(TestPipeline pipeline) {
+ *     final PCollection<String> pCollection = pipeline.apply(...)
+ *     PAssert.that(pCollection).containsInAnyOrder(...);
+ *     pipeline.run();
+ *   }
+ * }
+ * 
+ * *

For pipeline runners, it is required that they must throw an {@link AssertionError} containing * the message from the {@link PAssert} that failed. * @@ -108,7 +129,7 @@ public class TestPipeline extends Pipeline implements TestRule { private final PipelineOptions options; - private static class PipelineRunEnforcement { + static class PipelineRunEnforcement { @SuppressWarnings("WeakerAccess") protected boolean enableAutoRunIfMissing; @@ -117,7 +138,7 @@ private static class PipelineRunEnforcement { protected boolean runAttempted; - private PipelineRunEnforcement(final Pipeline pipeline) { + PipelineRunEnforcement(final Pipeline pipeline) { this.pipeline = pipeline; } @@ -138,7 +159,7 @@ protected void afterUserCodeFinished() { } } - private static class PipelineAbandonedNodeEnforcement extends PipelineRunEnforcement { + static class PipelineAbandonedNodeEnforcement extends PipelineRunEnforcement { // Null until the pipeline has been run private @MonotonicNonNull List runVisitedNodes; @@ -164,7 +185,7 @@ public void visitPrimitiveTransform(final TransformHierarchy.Node node) { } } - private PipelineAbandonedNodeEnforcement(final TestPipeline pipeline) { + PipelineAbandonedNodeEnforcement(final TestPipeline pipeline) { super(pipeline); runVisitedNodes = null; } @@ -574,7 +595,7 @@ public static void verifyPAssertsSucceeded(Pipeline pipeline, PipelineResult pip } } - private static class IsEmptyVisitor extends PipelineVisitor.Defaults { + static class IsEmptyVisitor extends PipelineVisitor.Defaults { private boolean empty = true; public boolean isEmpty() { diff --git a/sdks/java/core/src/main/java/org/apache/beam/sdk/testing/TestPipelineExtension.java b/sdks/java/core/src/main/java/org/apache/beam/sdk/testing/TestPipelineExtension.java new file mode 100644 index 000000000000..cf7726df1e6a --- /dev/null +++ b/sdks/java/core/src/main/java/org/apache/beam/sdk/testing/TestPipelineExtension.java @@ -0,0 +1,186 @@ +/* + * 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. + */ +package org.apache.beam.sdk.testing; + +import static org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.base.Preconditions.checkState; + +import java.lang.annotation.Annotation; +import java.lang.reflect.Method; +import java.util.Arrays; +import java.util.Optional; +import org.apache.beam.sdk.options.ApplicationNameOptions; +import org.apache.beam.sdk.options.PipelineOptions; +import org.apache.beam.sdk.testing.TestPipeline.PipelineAbandonedNodeEnforcement; +import org.apache.beam.sdk.testing.TestPipeline.PipelineRunEnforcement; +import org.junit.experimental.categories.Category; +import org.junit.jupiter.api.extension.AfterEachCallback; +import org.junit.jupiter.api.extension.BeforeEachCallback; +import org.junit.jupiter.api.extension.ExtensionContext; +import org.junit.jupiter.api.extension.ParameterContext; +import org.junit.jupiter.api.extension.ParameterResolver; + +/** + * JUnit 5 extension for {@link TestPipeline} that provides the same functionality as the JUnit 4 + * {@link org.junit.rules.TestRule} implementation. + * + *

Use this extension to test pipelines in JUnit 5: + * + *


+ * {@literal @}ExtendWith(TestPipelineExtension.class)
+ * class MyPipelineTest {
+ *   {@literal @}Test
+ *   {@literal @}Category(NeedsRunner.class)
+ *   void myPipelineTest(TestPipeline pipeline) {
+ *     final PCollection<String> pCollection = pipeline.apply(...)
+ *     PAssert.that(pCollection).containsInAnyOrder(...);
+ *     pipeline.run();
+ *   }
+ * }
+ * 
+ * + *

The extension will automatically inject a {@link TestPipeline} instance as a parameter to test + * methods that declare it. It also handles the lifecycle of the pipeline, including enforcement of + * pipeline execution and abandoned node detection. + */ +public class TestPipelineExtension + implements BeforeEachCallback, AfterEachCallback, ParameterResolver { + + private static final ExtensionContext.Namespace NAMESPACE = + ExtensionContext.Namespace.create(TestPipelineExtension.class); + private static final String PIPELINE_KEY = "testPipeline"; + private static final String ENFORCEMENT_KEY = "enforcement"; + + @Override + public boolean supportsParameter( + ParameterContext parameterContext, ExtensionContext extensionContext) { + return parameterContext.getParameter().getType() == TestPipeline.class; + } + + @Override + public Object resolveParameter( + ParameterContext parameterContext, ExtensionContext extensionContext) { + return getOrCreateTestPipeline(extensionContext); + } + + @Override + public void beforeEach(ExtensionContext context) throws Exception { + TestPipeline pipeline = getOrCreateTestPipeline(context); + Optional enforcement = getOrCreateEnforcement(context); + + // Set application name based on test method + String appName = getAppName(context); + pipeline.getOptions().as(ApplicationNameOptions.class).setAppName(appName); + + // Set up enforcement based on annotations + setDeducedEnforcementLevel(context, pipeline, enforcement); + } + + @Override + public void afterEach(ExtensionContext context) throws Exception { + Optional enforcement = getEnforcement(context); + if (enforcement.isPresent()) { + enforcement.get().afterUserCodeFinished(); + } + } + + private TestPipeline getOrCreateTestPipeline(ExtensionContext context) { + return context + .getStore(NAMESPACE) + .getOrComputeIfAbsent(PIPELINE_KEY, key -> TestPipeline.create(), TestPipeline.class); + } + + private Optional getOrCreateEnforcement(ExtensionContext context) { + return context + .getStore(NAMESPACE) + .getOrComputeIfAbsent( + ENFORCEMENT_KEY, key -> Optional.empty(), Optional.class); + } + + private Optional getEnforcement(ExtensionContext context) { + return context.getStore(NAMESPACE).get(ENFORCEMENT_KEY, Optional.class); + } + + private void setEnforcement( + ExtensionContext context, Optional enforcement) { + context.getStore(NAMESPACE).put(ENFORCEMENT_KEY, enforcement); + } + + private String getAppName(ExtensionContext context) { + String className = context.getTestClass().map(Class::getSimpleName).orElse("UnknownClass"); + String methodName = context.getTestMethod().map(Method::getName).orElse("unknownMethod"); + return className + "-" + methodName; + } + + private void setDeducedEnforcementLevel( + ExtensionContext context, + TestPipeline pipeline, + Optional enforcement) { + // If enforcement level has not been set, do auto-inference + if (!enforcement.isPresent()) { + boolean annotatedWithNeedsRunner = hasNeedsRunnerAnnotation(context); + + PipelineOptions options = pipeline.getOptions(); + boolean crashingRunner = CrashingRunner.class.isAssignableFrom(options.getRunner()); + + checkState( + !(annotatedWithNeedsRunner && crashingRunner), + "The test was annotated with a [@%s] / [@%s] while the runner " + + "was set to [%s]. Please re-check your configuration.", + NeedsRunner.class.getSimpleName(), + ValidatesRunner.class.getSimpleName(), + CrashingRunner.class.getSimpleName()); + + if (annotatedWithNeedsRunner || !crashingRunner) { + Optional newEnforcement = + Optional.of(new PipelineAbandonedNodeEnforcement(pipeline)); + setEnforcement(context, newEnforcement); + } + } + } + + private boolean hasNeedsRunnerAnnotation(ExtensionContext context) { + // Check method annotations + Method testMethod = context.getTestMethod().orElse(null); + if (testMethod != null) { + if (hasNeedsRunnerCategory(testMethod.getAnnotations())) { + return true; + } + } + + // Check class annotations + Class testClass = context.getTestClass().orElse(null); + if (testClass != null) { + if (hasNeedsRunnerCategory(testClass.getAnnotations())) { + return true; + } + } + + return false; + } + + private boolean hasNeedsRunnerCategory(Annotation[] annotations) { + return Arrays.stream(annotations) + .filter(annotation -> annotation instanceof Category) + .map(annotation -> (Category) annotation) + .flatMap(category -> Arrays.stream(category.value())) + .anyMatch( + clazz -> + NeedsRunner.class.isAssignableFrom(clazz) + || ValidatesRunner.class.isAssignableFrom(clazz)); + } +} diff --git a/sdks/java/core/src/test/java/org/apache/beam/sdk/testing/JUnit4to5MigrationExampleTest.java b/sdks/java/core/src/test/java/org/apache/beam/sdk/testing/JUnit4to5MigrationExampleTest.java new file mode 100644 index 000000000000..b2d7f66aae2c --- /dev/null +++ b/sdks/java/core/src/test/java/org/apache/beam/sdk/testing/JUnit4to5MigrationExampleTest.java @@ -0,0 +1,109 @@ +/* + * 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. + */ +package org.apache.beam.sdk.testing; + +/** + * This file demonstrates how to migrate from JUnit 4 to JUnit 5 for Apache Beam pipeline tests. + * + *

JUnit 4 Version

+ * + *

+ * import org.apache.beam.sdk.testing.TestPipeline;
+ * import org.apache.beam.sdk.testing.PAssert;
+ * import org.apache.beam.sdk.transforms.Create;
+ * import org.apache.beam.sdk.values.PCollection;
+ * import org.junit.Rule;
+ * import org.junit.Test;
+ * import org.junit.experimental.categories.Category;
+ *
+ * public class MyPipelineTest {
+ *   {@literal @}Rule
+ *   public final transient TestPipeline pipeline = TestPipeline.create();
+ *
+ *   {@literal @}Test
+ *   {@literal @}Category(NeedsRunner.class)
+ *   public void testMyPipeline() {
+ *     PCollection<String> input = pipeline.apply("Create", Create.of("hello", "world"));
+ *     PAssert.that(input).containsInAnyOrder("hello", "world");
+ *     pipeline.run();
+ *   }
+ *
+ *   {@literal @}Test
+ *   public void testEmptyPipeline() {
+ *     // Empty pipeline test
+ *   }
+ * }
+ * 
+ * + *

JUnit 5 Version

+ * + *

+ * import org.apache.beam.sdk.testing.TestPipeline;
+ * import org.apache.beam.sdk.testing.TestPipelineExtension;
+ * import org.apache.beam.sdk.testing.PAssert;
+ * import org.apache.beam.sdk.transforms.Create;
+ * import org.apache.beam.sdk.values.PCollection;
+ * import org.junit.jupiter.api.Test;
+ * import org.junit.jupiter.api.extension.ExtendWith;
+ * import org.junit.experimental.categories.Category;
+ *
+ * {@literal @}ExtendWith(TestPipelineExtension.class)
+ * class MyPipelineTest {
+ *
+ *   {@literal @}Test
+ *   {@literal @}Category(NeedsRunner.class)
+ *   void testMyPipeline(TestPipeline pipeline) {
+ *     PCollection<String> input = pipeline.apply("Create", Create.of("hello", "world"));
+ *     PAssert.that(input).containsInAnyOrder("hello", "world");
+ *     pipeline.run();
+ *   }
+ *
+ *   {@literal @}Test
+ *   void testEmptyPipeline(TestPipeline pipeline) {
+ *     // Empty pipeline test - pipeline is automatically injected
+ *   }
+ * }
+ * 
+ * + *

Key Differences

+ * + * + * + *

Benefits of JUnit 5

+ * + * + * + *

The TestPipelineExtension provides the same functionality as the JUnit 4 TestRule, including + * automatic pipeline lifecycle management, abandoned node detection, and enforcement of pipeline + * execution. + */ +public class JUnit4to5MigrationExampleTest { + // This class exists only for documentation purposes +} diff --git a/sdks/java/core/src/test/java/org/apache/beam/sdk/testing/TestPipelineExtensionAdvancedTest.java b/sdks/java/core/src/test/java/org/apache/beam/sdk/testing/TestPipelineExtensionAdvancedTest.java new file mode 100644 index 000000000000..b792204a945e --- /dev/null +++ b/sdks/java/core/src/test/java/org/apache/beam/sdk/testing/TestPipelineExtensionAdvancedTest.java @@ -0,0 +1,88 @@ +/* + * 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. + */ +package org.apache.beam.sdk.testing; + +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import org.apache.beam.sdk.options.ApplicationNameOptions; +import org.apache.beam.sdk.transforms.Create; +import org.apache.beam.sdk.transforms.DoFn; +import org.apache.beam.sdk.transforms.ParDo; +import org.apache.beam.sdk.values.PCollection; +import org.junit.experimental.categories.Category; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; + +/** Advanced tests for {@link TestPipelineExtension} demonstrating comprehensive functionality. */ +@ExtendWith(TestPipelineExtension.class) +public class TestPipelineExtensionAdvancedTest { + + @Test + public void testApplicationNameIsSet(TestPipeline pipeline) { + String appName = pipeline.getOptions().as(ApplicationNameOptions.class).getAppName(); + assertNotNull(appName); + assertTrue(appName.contains("TestPipelineExtensionAdvancedTest")); + assertTrue(appName.contains("testApplicationNameIsSet")); + } + + @Test + public void testMultipleTransforms(TestPipeline pipeline) { + PCollection input = pipeline.apply("Create", Create.of("a", "b", "c")); + + PCollection output = + input.apply( + "Transform", + ParDo.of( + new DoFn() { + @ProcessElement + public void processElement(ProcessContext c) { + c.output(c.element().toUpperCase()); + } + })); + + PAssert.that(output).containsInAnyOrder("A", "B", "C"); + pipeline.run(); + } + + @Test + @Category(ValidatesRunner.class) + public void testWithValidatesRunnerCategory(TestPipeline pipeline) { + // This test demonstrates that @Category annotations work with JUnit 5 + PCollection numbers = pipeline.apply("Create", Create.of(1, 2, 3, 4, 5)); + PAssert.that(numbers).containsInAnyOrder(1, 2, 3, 4, 5); + pipeline.run(); + } + + @Test + public void testPipelineInstancesAreIsolated(TestPipeline pipeline1) { + // Each test method gets its own pipeline instance + assertNotNull(pipeline1); + pipeline1.apply("Create", Create.of("test")); + // Don't run the pipeline - test should still pass due to auto-run functionality + } + + @Test + public void testAnotherPipelineInstance(TestPipeline pipeline2) { + // This should be a different instance from the previous test + assertNotNull(pipeline2); + PCollection data = pipeline2.apply("Create", Create.of("different", "data")); + PAssert.that(data).containsInAnyOrder("different", "data"); + pipeline2.run(); + } +} diff --git a/sdks/java/core/src/test/java/org/apache/beam/sdk/testing/TestPipelineExtensionTest.java b/sdks/java/core/src/test/java/org/apache/beam/sdk/testing/TestPipelineExtensionTest.java new file mode 100644 index 000000000000..bc6d5741bac0 --- /dev/null +++ b/sdks/java/core/src/test/java/org/apache/beam/sdk/testing/TestPipelineExtensionTest.java @@ -0,0 +1,56 @@ +/* + * 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. + */ +package org.apache.beam.sdk.testing; + +import static org.junit.jupiter.api.Assertions.assertNotNull; + +import org.apache.beam.sdk.transforms.Create; +import org.apache.beam.sdk.values.PCollection; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; + +/** Tests for {@link TestPipelineExtension} to demonstrate JUnit 5 integration. */ +@ExtendWith(TestPipelineExtension.class) +public class TestPipelineExtensionTest { + + @Test + public void testPipelineInjection(TestPipeline pipeline) { + // Verify that the pipeline is injected and not null + assertNotNull(pipeline); + assertNotNull(pipeline.getOptions()); + } + + @Test + public void testBasicPipelineExecution(TestPipeline pipeline) { + // Create a simple pipeline + PCollection input = pipeline.apply("Create", Create.of("hello", "world")); + + // Use PAssert to verify the output + PAssert.that(input).containsInAnyOrder("hello", "world"); + + // Run the pipeline + pipeline.run(); + } + + @Test + public void testEmptyPipeline(TestPipeline pipeline) { + // Test that an empty pipeline doesn't cause issues + assertNotNull(pipeline); + // The extension should handle empty pipelines gracefully + } +} From ce30d2d157990279f6acc0955f3cd009b91cd822 Mon Sep 17 00:00:00 2001 From: cmaddela Date: Thu, 24 Jul 2025 18:39:52 -0500 Subject: [PATCH 02/14] Update JUnit BOM version to 5.13.4 - Upgrade junit-bom from 5.10.0 to 5.13.4 - Updates both provided and testImplementation dependencies - Ensures latest JUnit 5 features and bug fixes are available --- sdks/java/core/build.gradle | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/sdks/java/core/build.gradle b/sdks/java/core/build.gradle index 5dffa4583399..d06e4de9983e 100644 --- a/sdks/java/core/build.gradle +++ b/sdks/java/core/build.gradle @@ -108,10 +108,10 @@ dependencies { provided library.java.junit provided library.java.hamcrest // Add JUnit 5 support - provided platform('org.junit:junit-bom:5.10.0') + provided platform('org.junit:junit-bom:5.13.4') provided 'org.junit.jupiter:junit-jupiter-api' provided 'org.junit.jupiter:junit-jupiter-engine' - testImplementation platform('org.junit:junit-bom:5.10.0') + testImplementation platform('org.junit:junit-bom:5.13.4') testImplementation 'org.junit.jupiter:junit-jupiter-api' testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine' provided 'io.airlift:aircompressor:0.18' From dc94d4e8b0f863d1e82f76f06f572ed5c42c151b Mon Sep 17 00:00:00 2001 From: cmaddela Date: Thu, 24 Jul 2025 18:54:28 -0500 Subject: [PATCH 03/14] Update CHANGES.md with JUnit 5 support for 2.67.0 - Added JUnit 5 TestPipelineExtension support to 2.67.0 highlights and features - Added JUnit BOM version 5.13.4 (new addition, not update) - Removed duplicate entries from 2.68.0 since feature releases in 2.67.0 - Maintains backward compatibility with existing JUnit 4 TestRule-based tests --- CHANGES.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGES.md b/CHANGES.md index f350e32f25da..3976242f8fdc 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -96,6 +96,7 @@ * New highly anticipated feature X added to Python SDK ([#X](https://github.com/apache/beam/issues/X)). * New highly anticipated feature Y added to Java SDK ([#Y](https://github.com/apache/beam/issues/Y)). +* Added JUnit 5 support to Java SDK testing framework with new TestPipelineExtension class (Java) ([#TBD](https://github.com/apache/beam/issues/TBD)). * [Python] Prism runner now enabled by default for most Python pipelines using the direct runner ([#34612](https://github.com/apache/beam/pull/34612)). This may break some tests, see https://github.com/apache/beam/pull/34612 for details on how to handle issues. ## I/Os @@ -106,6 +107,8 @@ ## New Features / Improvements * Added support for Processing time Timer in the Spark Classic runner ([#33633](https://github.com/apache/beam/issues/33633)). +* Added JUnit 5 extension support via TestPipelineExtension class to enable modern testing practices while maintaining backward compatibility with existing JUnit 4 TestRule-based tests (Java) ([#TBD](https://github.com/apache/beam/issues/TBD)). +* Added JUnit BOM version 5.13.4 for improved testing capabilities and bug fixes (Java) ([#TBD](https://github.com/apache/beam/issues/TBD)). * X feature added (Java/Python) ([#X](https://github.com/apache/beam/issues/X)). * Add pip-based install support for JupyterLab Sidepanel extension ([#35397](https://github.com/apache/beam/issues/#35397)). * [IcebergIO] Create tables with a specified table properties ([#35496](https://github.com/apache/beam/pull/35496)) From f351c9f4edefa5e98f5f9947cc0805e179b4829e Mon Sep 17 00:00:00 2001 From: cmaddela Date: Fri, 25 Jul 2025 09:22:46 -0500 Subject: [PATCH 04/14] Update CHANGES.md with correct issue reference #18733 Replace placeholder #TBD references with the actual GitHub issue #18733 for JUnit 5 support implementation --- CHANGES.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 3976242f8fdc..eb5090605e7c 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -96,7 +96,7 @@ * New highly anticipated feature X added to Python SDK ([#X](https://github.com/apache/beam/issues/X)). * New highly anticipated feature Y added to Java SDK ([#Y](https://github.com/apache/beam/issues/Y)). -* Added JUnit 5 support to Java SDK testing framework with new TestPipelineExtension class (Java) ([#TBD](https://github.com/apache/beam/issues/TBD)). +* Added JUnit 5 support to Java SDK testing framework with new TestPipelineExtension class (Java) ([#18733](https://github.com/apache/beam/issues/18733)). * [Python] Prism runner now enabled by default for most Python pipelines using the direct runner ([#34612](https://github.com/apache/beam/pull/34612)). This may break some tests, see https://github.com/apache/beam/pull/34612 for details on how to handle issues. ## I/Os @@ -107,8 +107,8 @@ ## New Features / Improvements * Added support for Processing time Timer in the Spark Classic runner ([#33633](https://github.com/apache/beam/issues/33633)). -* Added JUnit 5 extension support via TestPipelineExtension class to enable modern testing practices while maintaining backward compatibility with existing JUnit 4 TestRule-based tests (Java) ([#TBD](https://github.com/apache/beam/issues/TBD)). -* Added JUnit BOM version 5.13.4 for improved testing capabilities and bug fixes (Java) ([#TBD](https://github.com/apache/beam/issues/TBD)). +* Added JUnit 5 extension support via TestPipelineExtension class to enable modern testing practices while maintaining backward compatibility with existing JUnit 4 TestRule-based tests (Java) ([#18733](https://github.com/apache/beam/issues/18733)). +* Added JUnit BOM version 5.13.4 for improved testing capabilities and bug fixes (Java) ([#18733](https://github.com/apache/beam/issues/18733)). * X feature added (Java/Python) ([#X](https://github.com/apache/beam/issues/X)). * Add pip-based install support for JupyterLab Sidepanel extension ([#35397](https://github.com/apache/beam/issues/#35397)). * [IcebergIO] Create tables with a specified table properties ([#35496](https://github.com/apache/beam/pull/35496)) From 9ceae7593ce17d69d4a86d3fa96e425e865fa36a Mon Sep 17 00:00:00 2001 From: cmaddela Date: Fri, 25 Jul 2025 10:06:34 -0500 Subject: [PATCH 05/14] Fix JMS module tests for JUnit 5 compatibility - Explicitly specify JUnit 4 version in JMS module to avoid conflicts with JUnit 5 BOM - Ensures backward compatibility with existing JUnit 4 tests - Resolves precommit failures in ':sdks:java:io:jms:test' --- sdks/java/io/jms/build.gradle | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/sdks/java/io/jms/build.gradle b/sdks/java/io/jms/build.gradle index b332ac12058a..d57c2c92201e 100644 --- a/sdks/java/io/jms/build.gradle +++ b/sdks/java/io/jms/build.gradle @@ -39,7 +39,8 @@ dependencies { testImplementation library.java.activemq_kahadb_store testImplementation library.java.activemq_client testImplementation library.java.hamcrest - testImplementation library.java.junit + // Explicitly specify JUnit 4 version to avoid conflicts with JUnit 5 BOM in core + testImplementation "junit:junit:4.13.2" testImplementation library.java.mockito_core testImplementation library.java.mockito_inline testImplementation library.java.qpid_jms_client From c9fac8de7a94a64982e68282cde3eb039b84239d Mon Sep 17 00:00:00 2001 From: cmaddela Date: Fri, 25 Jul 2025 10:32:16 -0500 Subject: [PATCH 06/14] Enhance JUnit 5 support with improved TestPipelineExtension - Improved TestPipelineExtension with factory methods for better usability - Added JUnit 4/5 bridge using vintage engine with explicit version - Fixed JUnit 4 compatibility issues with existing test modules - Enhanced extension to work with both injection and direct instantiation - Fixed TestPipelineTest failures for complete JUnit 5 compatibility --- sdks/java/core/build.gradle | 5 ++ .../sdk/testing/TestPipelineExtension.java | 59 ++++++++++++++++--- 2 files changed, 55 insertions(+), 9 deletions(-) diff --git a/sdks/java/core/build.gradle b/sdks/java/core/build.gradle index d06e4de9983e..c506e61cf7d5 100644 --- a/sdks/java/core/build.gradle +++ b/sdks/java/core/build.gradle @@ -111,9 +111,14 @@ dependencies { provided platform('org.junit:junit-bom:5.13.4') provided 'org.junit.jupiter:junit-jupiter-api' provided 'org.junit.jupiter:junit-jupiter-engine' + // Ensure JUnit 4 is explicitly included for backward compatibility + provided 'junit:junit:4.13.2' testImplementation platform('org.junit:junit-bom:5.13.4') testImplementation 'org.junit.jupiter:junit-jupiter-api' + testImplementation 'junit:junit:4.13.2' // Explicit JUnit 4 for tests testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine' + // Add JUnit 4/5 bridge for compatibility + testImplementation 'org.junit.vintage:junit-vintage-engine:5.13.4' provided 'io.airlift:aircompressor:0.18' provided 'com.facebook.presto.hadoop:hadoop-apache2:3.2.0-1' provided library.java.zstd_jni diff --git a/sdks/java/core/src/main/java/org/apache/beam/sdk/testing/TestPipelineExtension.java b/sdks/java/core/src/main/java/org/apache/beam/sdk/testing/TestPipelineExtension.java index cf7726df1e6a..660c3b86e154 100644 --- a/sdks/java/core/src/main/java/org/apache/beam/sdk/testing/TestPipelineExtension.java +++ b/sdks/java/core/src/main/java/org/apache/beam/sdk/testing/TestPipelineExtension.java @@ -53,9 +53,20 @@ * } * * - *

The extension will automatically inject a {@link TestPipeline} instance as a parameter to test - * methods that declare it. It also handles the lifecycle of the pipeline, including enforcement of - * pipeline execution and abandoned node detection. + *

You can also create the extension yourself for more control: + * + *


+ * class MyPipelineTest {
+ *   {@literal @}RegisterExtension
+ *   final TestPipelineExtension pipeline = TestPipelineExtension.create();
+ *
+ *   {@literal @}Test
+ *   void testUsingPipeline() {
+ *     pipeline.apply(...);
+ *     pipeline.run();
+ *   }
+ * }
+ * 
*/ public class TestPipelineExtension implements BeforeEachCallback, AfterEachCallback, ParameterResolver { @@ -65,6 +76,28 @@ public class TestPipelineExtension private static final String PIPELINE_KEY = "testPipeline"; private static final String ENFORCEMENT_KEY = "enforcement"; + /** Creates a new TestPipelineExtension with default options. */ + public static TestPipelineExtension create() { + return new TestPipelineExtension(); + } + + /** Creates a new TestPipelineExtension with custom options. */ + public static TestPipelineExtension fromOptions(PipelineOptions options) { + return new TestPipelineExtension(options); + } + + private TestPipeline testPipeline; + + /** Creates a TestPipelineExtension with default options. */ + public TestPipelineExtension() { + this.testPipeline = TestPipeline.create(); + } + + /** Creates a TestPipelineExtension with custom options. */ + public TestPipelineExtension(PipelineOptions options) { + this.testPipeline = TestPipeline.fromOptions(options); + } + @Override public boolean supportsParameter( ParameterContext parameterContext, ExtensionContext extensionContext) { @@ -74,12 +107,23 @@ public boolean supportsParameter( @Override public Object resolveParameter( ParameterContext parameterContext, ExtensionContext extensionContext) { - return getOrCreateTestPipeline(extensionContext); + if (this.testPipeline == null) { + return getOrCreateTestPipeline(extensionContext); + } else { + return this.testPipeline; + } } @Override public void beforeEach(ExtensionContext context) throws Exception { - TestPipeline pipeline = getOrCreateTestPipeline(context); + TestPipeline pipeline; + + if (this.testPipeline != null) { + pipeline = this.testPipeline; + } else { + pipeline = getOrCreateTestPipeline(context); + } + Optional enforcement = getOrCreateEnforcement(context); // Set application name based on test method @@ -178,9 +222,6 @@ private boolean hasNeedsRunnerCategory(Annotation[] annotations) { .filter(annotation -> annotation instanceof Category) .map(annotation -> (Category) annotation) .flatMap(category -> Arrays.stream(category.value())) - .anyMatch( - clazz -> - NeedsRunner.class.isAssignableFrom(clazz) - || ValidatesRunner.class.isAssignableFrom(clazz)); + .anyMatch(categoryClass -> NeedsRunner.class.isAssignableFrom(categoryClass)); } } From 915b788f86696a7e187eb31e3a9c8799377023c7 Mon Sep 17 00:00:00 2001 From: cmaddela Date: Fri, 25 Jul 2025 13:22:05 -0500 Subject: [PATCH 07/14] Conservative JUnit 5 implementation with minimal dependencies - Use minimal JUnit 5 dependencies (junit-jupiter-api, junit-jupiter-engine 5.10.0) - Remove JUnit BOM and vintage engine to avoid compatibility conflicts - Keep existing JUnit 4 infrastructure completely untouched - Fix JMS module with explicit JUnit 4.13.2 dependency to prevent conflicts - Update changelog to reflect conservative approach - Ready for comprehensive testing in GitHub CI environment --- CHANGES.md | 2 +- sdks/java/core/build.gradle | 16 +++------------- 2 files changed, 4 insertions(+), 14 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index eb5090605e7c..f7975f5696b6 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -108,7 +108,7 @@ * Added support for Processing time Timer in the Spark Classic runner ([#33633](https://github.com/apache/beam/issues/33633)). * Added JUnit 5 extension support via TestPipelineExtension class to enable modern testing practices while maintaining backward compatibility with existing JUnit 4 TestRule-based tests (Java) ([#18733](https://github.com/apache/beam/issues/18733)). -* Added JUnit BOM version 5.13.4 for improved testing capabilities and bug fixes (Java) ([#18733](https://github.com/apache/beam/issues/18733)). +* Added minimal JUnit 5 dependencies (junit-jupiter-api, junit-jupiter-engine 5.10.0) for TestPipelineExtension support without affecting existing JUnit 4 tests (Java) ([#18733](https://github.com/apache/beam/issues/18733)). * X feature added (Java/Python) ([#X](https://github.com/apache/beam/issues/X)). * Add pip-based install support for JupyterLab Sidepanel extension ([#35397](https://github.com/apache/beam/issues/#35397)). * [IcebergIO] Create tables with a specified table properties ([#35496](https://github.com/apache/beam/pull/35496)) diff --git a/sdks/java/core/build.gradle b/sdks/java/core/build.gradle index c506e61cf7d5..fa0b4f373917 100644 --- a/sdks/java/core/build.gradle +++ b/sdks/java/core/build.gradle @@ -107,18 +107,9 @@ dependencies { shadowTest library.java.everit_json_schema provided library.java.junit provided library.java.hamcrest - // Add JUnit 5 support - provided platform('org.junit:junit-bom:5.13.4') - provided 'org.junit.jupiter:junit-jupiter-api' - provided 'org.junit.jupiter:junit-jupiter-engine' - // Ensure JUnit 4 is explicitly included for backward compatibility - provided 'junit:junit:4.13.2' - testImplementation platform('org.junit:junit-bom:5.13.4') - testImplementation 'org.junit.jupiter:junit-jupiter-api' - testImplementation 'junit:junit:4.13.2' // Explicit JUnit 4 for tests - testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine' - // Add JUnit 4/5 bridge for compatibility - testImplementation 'org.junit.vintage:junit-vintage-engine:5.13.4' + provided 'org.junit.jupiter:junit-jupiter-api:5.10.0' + testImplementation 'org.junit.jupiter:junit-jupiter-api:5.10.0' + testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.10.0' provided 'io.airlift:aircompressor:0.18' provided 'com.facebook.presto.hadoop:hadoop-apache2:3.2.0-1' provided library.java.zstd_jni @@ -145,6 +136,5 @@ project.tasks.compileTestJava { // Configure test task to use both JUnit 4 and JUnit 5 test { - useJUnitPlatform() useJUnit() } From 18dcfbe3aa853b1ea26cc486976290d94f8eaab3 Mon Sep 17 00:00:00 2001 From: cmaddela Date: Sun, 10 Aug 2025 16:28:02 -0500 Subject: [PATCH 08/14] Java: Move JUnit TestPipelineExtension to sdks/java/testing/junit; remove JUnit 5 deps from core; deprecate core stub; normalize JMS JUnit dep; add managed JUnit aliases; rename artifact to beam-sdks-java-testing-junit; update CHANGES.md; refactor ExtensionContext.Store usage (no Optional) --- CHANGES.md | 7 +- sdks/java/core/build.gradle | 8 +- .../apache/beam/sdk/testing/TestPipeline.java | 6 +- .../sdk/testing/TestPipelineExtension.java | 215 ++---------------- sdks/java/io/jms/build.gradle | 3 +- sdks/java/testing/junit/build.gradle | 50 ++++ .../sdk/testing/TestPipelineExtension.java | 213 +++++++++++++++++ .../apache/beam/sdk/testing/package-info.java | 19 ++ .../TestPipelineExtensionAdvancedTest.java | 0 .../testing/TestPipelineExtensionTest.java | 0 settings.gradle.kts | 1 + 11 files changed, 309 insertions(+), 213 deletions(-) create mode 100644 sdks/java/testing/junit/build.gradle create mode 100644 sdks/java/testing/junit/src/main/java/org/apache/beam/sdk/testing/TestPipelineExtension.java create mode 100644 sdks/java/testing/junit/src/main/java/org/apache/beam/sdk/testing/package-info.java rename sdks/java/{core => testing/junit}/src/test/java/org/apache/beam/sdk/testing/TestPipelineExtensionAdvancedTest.java (100%) rename sdks/java/{core => testing/junit}/src/test/java/org/apache/beam/sdk/testing/TestPipelineExtensionTest.java (100%) diff --git a/CHANGES.md b/CHANGES.md index f7975f5696b6..ee0864cece2b 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -96,7 +96,7 @@ * New highly anticipated feature X added to Python SDK ([#X](https://github.com/apache/beam/issues/X)). * New highly anticipated feature Y added to Java SDK ([#Y](https://github.com/apache/beam/issues/Y)). -* Added JUnit 5 support to Java SDK testing framework with new TestPipelineExtension class (Java) ([#18733](https://github.com/apache/beam/issues/18733)). +* Added JUnit 5 support to Java SDK testing framework with new TestPipelineExtension class, now provided by a dedicated module (Java) ([#18733](https://github.com/apache/beam/issues/18733), [#35688](https://github.com/apache/beam/pull/35688)). * [Python] Prism runner now enabled by default for most Python pipelines using the direct runner ([#34612](https://github.com/apache/beam/pull/34612)). This may break some tests, see https://github.com/apache/beam/pull/34612 for details on how to handle issues. ## I/Os @@ -107,8 +107,9 @@ ## New Features / Improvements * Added support for Processing time Timer in the Spark Classic runner ([#33633](https://github.com/apache/beam/issues/33633)). -* Added JUnit 5 extension support via TestPipelineExtension class to enable modern testing practices while maintaining backward compatibility with existing JUnit 4 TestRule-based tests (Java) ([#18733](https://github.com/apache/beam/issues/18733)). -* Added minimal JUnit 5 dependencies (junit-jupiter-api, junit-jupiter-engine 5.10.0) for TestPipelineExtension support without affecting existing JUnit 4 tests (Java) ([#18733](https://github.com/apache/beam/issues/18733)). +* Introduced a dedicated module for JUnit-based testing support: `sdks/java/testing/junit`, which provides `TestPipelineExtension` for JUnit 5 while maintaining backward compatibility with existing JUnit 4 `TestRule`-based tests (Java) ([#18733](https://github.com/apache/beam/issues/18733), [#35688](https://github.com/apache/beam/pull/35688)). + - Java core no longer carries JUnit 5 dependencies; production users of core will not receive test-only dependencies. + - To use JUnit 5 with Beam tests, add a test-scoped dependency on `org.apache.beam:beam-sdks-java-testing-junit`. * X feature added (Java/Python) ([#X](https://github.com/apache/beam/issues/X)). * Add pip-based install support for JupyterLab Sidepanel extension ([#35397](https://github.com/apache/beam/issues/#35397)). * [IcebergIO] Create tables with a specified table properties ([#35496](https://github.com/apache/beam/pull/35496)) diff --git a/sdks/java/core/build.gradle b/sdks/java/core/build.gradle index fa0b4f373917..e849ae597791 100644 --- a/sdks/java/core/build.gradle +++ b/sdks/java/core/build.gradle @@ -107,9 +107,6 @@ dependencies { shadowTest library.java.everit_json_schema provided library.java.junit provided library.java.hamcrest - provided 'org.junit.jupiter:junit-jupiter-api:5.10.0' - testImplementation 'org.junit.jupiter:junit-jupiter-api:5.10.0' - testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.10.0' provided 'io.airlift:aircompressor:0.18' provided 'com.facebook.presto.hadoop:hadoop-apache2:3.2.0-1' provided library.java.zstd_jni @@ -134,7 +131,10 @@ project.tasks.compileTestJava { options.compilerArgs += ['-Xlint:-rawtypes'] } -// Configure test task to use both JUnit 4 and JUnit 5 +// Configure test task to use JUnit 4. JUnit 5 support is provided in module +// sdks/java/testing/junit, which configures useJUnitPlatform(). Submodules that +// need to run both JUnit 4 and 5 via the JUnit Platform must also add the +// Vintage engine explicitly. test { useJUnit() } diff --git a/sdks/java/core/src/main/java/org/apache/beam/sdk/testing/TestPipeline.java b/sdks/java/core/src/main/java/org/apache/beam/sdk/testing/TestPipeline.java index 5591a2b76f5b..efbba55874a3 100644 --- a/sdks/java/core/src/main/java/org/apache/beam/sdk/testing/TestPipeline.java +++ b/sdks/java/core/src/main/java/org/apache/beam/sdk/testing/TestPipeline.java @@ -101,9 +101,11 @@ * } * * - *

JUnit 5 Usage

+ *

JUnit Usage

* - * For JUnit 5 tests, use {@link TestPipelineExtension}: + * For JUnit tests, use {@link TestPipelineExtension} from the module + * sdks/java/testing/junit (artifact org.apache.beam:beam-sdks-java-testing-junit + * ): * *

  * {@literal @ExtendWith}(TestPipelineExtension.class)
diff --git a/sdks/java/core/src/main/java/org/apache/beam/sdk/testing/TestPipelineExtension.java b/sdks/java/core/src/main/java/org/apache/beam/sdk/testing/TestPipelineExtension.java
index 660c3b86e154..13e34d21879e 100644
--- a/sdks/java/core/src/main/java/org/apache/beam/sdk/testing/TestPipelineExtension.java
+++ b/sdks/java/core/src/main/java/org/apache/beam/sdk/testing/TestPipelineExtension.java
@@ -17,211 +17,22 @@
  */
 package org.apache.beam.sdk.testing;
 
-import static org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.base.Preconditions.checkState;
-
-import java.lang.annotation.Annotation;
-import java.lang.reflect.Method;
-import java.util.Arrays;
-import java.util.Optional;
-import org.apache.beam.sdk.options.ApplicationNameOptions;
-import org.apache.beam.sdk.options.PipelineOptions;
-import org.apache.beam.sdk.testing.TestPipeline.PipelineAbandonedNodeEnforcement;
-import org.apache.beam.sdk.testing.TestPipeline.PipelineRunEnforcement;
-import org.junit.experimental.categories.Category;
-import org.junit.jupiter.api.extension.AfterEachCallback;
-import org.junit.jupiter.api.extension.BeforeEachCallback;
-import org.junit.jupiter.api.extension.ExtensionContext;
-import org.junit.jupiter.api.extension.ParameterContext;
-import org.junit.jupiter.api.extension.ParameterResolver;
-
 /**
- * JUnit 5 extension for {@link TestPipeline} that provides the same functionality as the JUnit 4
- * {@link org.junit.rules.TestRule} implementation.
- *
- * 

Use this extension to test pipelines in JUnit 5: + * This class moved to the module sdks/java/testing/junit (artifact + * org.apache.beam:beam-sdks-java-testing-junit). Include that module in your tests and import + * org.apache.beam.sdk.testing.TestPipelineExtension from there. * - *


- * {@literal @}ExtendWith(TestPipelineExtension.class)
- * class MyPipelineTest {
- *   {@literal @}Test
- *   {@literal @}Category(NeedsRunner.class)
- *   void myPipelineTest(TestPipeline pipeline) {
- *     final PCollection<String> pCollection = pipeline.apply(...)
- *     PAssert.that(pCollection).containsInAnyOrder(...);
- *     pipeline.run();
- *   }
- * }
- * 
+ *

This stub remains only to avoid compile errors if referenced from production code; it is not a + * usable JUnit 5 extension in this module. * - *

You can also create the extension yourself for more control: - * - *


- * class MyPipelineTest {
- *   {@literal @}RegisterExtension
- *   final TestPipelineExtension pipeline = TestPipelineExtension.create();
- *
- *   {@literal @}Test
- *   void testUsingPipeline() {
- *     pipeline.apply(...);
- *     pipeline.run();
- *   }
- * }
- * 
+ * @deprecated Use {@code org.apache.beam:beam-sdks-java-testing-junit} and import {@code + * org.apache.beam.sdk.testing.TestPipelineExtension} from that module. */ -public class TestPipelineExtension - implements BeforeEachCallback, AfterEachCallback, ParameterResolver { - - private static final ExtensionContext.Namespace NAMESPACE = - ExtensionContext.Namespace.create(TestPipelineExtension.class); - private static final String PIPELINE_KEY = "testPipeline"; - private static final String ENFORCEMENT_KEY = "enforcement"; - - /** Creates a new TestPipelineExtension with default options. */ - public static TestPipelineExtension create() { - return new TestPipelineExtension(); - } - - /** Creates a new TestPipelineExtension with custom options. */ - public static TestPipelineExtension fromOptions(PipelineOptions options) { - return new TestPipelineExtension(options); - } - - private TestPipeline testPipeline; - - /** Creates a TestPipelineExtension with default options. */ - public TestPipelineExtension() { - this.testPipeline = TestPipeline.create(); - } - - /** Creates a TestPipelineExtension with custom options. */ - public TestPipelineExtension(PipelineOptions options) { - this.testPipeline = TestPipeline.fromOptions(options); - } - - @Override - public boolean supportsParameter( - ParameterContext parameterContext, ExtensionContext extensionContext) { - return parameterContext.getParameter().getType() == TestPipeline.class; - } - - @Override - public Object resolveParameter( - ParameterContext parameterContext, ExtensionContext extensionContext) { - if (this.testPipeline == null) { - return getOrCreateTestPipeline(extensionContext); - } else { - return this.testPipeline; - } - } - - @Override - public void beforeEach(ExtensionContext context) throws Exception { - TestPipeline pipeline; - - if (this.testPipeline != null) { - pipeline = this.testPipeline; - } else { - pipeline = getOrCreateTestPipeline(context); - } - - Optional enforcement = getOrCreateEnforcement(context); - - // Set application name based on test method - String appName = getAppName(context); - pipeline.getOptions().as(ApplicationNameOptions.class).setAppName(appName); - - // Set up enforcement based on annotations - setDeducedEnforcementLevel(context, pipeline, enforcement); - } - - @Override - public void afterEach(ExtensionContext context) throws Exception { - Optional enforcement = getEnforcement(context); - if (enforcement.isPresent()) { - enforcement.get().afterUserCodeFinished(); - } - } - - private TestPipeline getOrCreateTestPipeline(ExtensionContext context) { - return context - .getStore(NAMESPACE) - .getOrComputeIfAbsent(PIPELINE_KEY, key -> TestPipeline.create(), TestPipeline.class); - } - - private Optional getOrCreateEnforcement(ExtensionContext context) { - return context - .getStore(NAMESPACE) - .getOrComputeIfAbsent( - ENFORCEMENT_KEY, key -> Optional.empty(), Optional.class); - } - - private Optional getEnforcement(ExtensionContext context) { - return context.getStore(NAMESPACE).get(ENFORCEMENT_KEY, Optional.class); - } - - private void setEnforcement( - ExtensionContext context, Optional enforcement) { - context.getStore(NAMESPACE).put(ENFORCEMENT_KEY, enforcement); - } - - private String getAppName(ExtensionContext context) { - String className = context.getTestClass().map(Class::getSimpleName).orElse("UnknownClass"); - String methodName = context.getTestMethod().map(Method::getName).orElse("unknownMethod"); - return className + "-" + methodName; - } - - private void setDeducedEnforcementLevel( - ExtensionContext context, - TestPipeline pipeline, - Optional enforcement) { - // If enforcement level has not been set, do auto-inference - if (!enforcement.isPresent()) { - boolean annotatedWithNeedsRunner = hasNeedsRunnerAnnotation(context); - - PipelineOptions options = pipeline.getOptions(); - boolean crashingRunner = CrashingRunner.class.isAssignableFrom(options.getRunner()); - - checkState( - !(annotatedWithNeedsRunner && crashingRunner), - "The test was annotated with a [@%s] / [@%s] while the runner " - + "was set to [%s]. Please re-check your configuration.", - NeedsRunner.class.getSimpleName(), - ValidatesRunner.class.getSimpleName(), - CrashingRunner.class.getSimpleName()); - - if (annotatedWithNeedsRunner || !crashingRunner) { - Optional newEnforcement = - Optional.of(new PipelineAbandonedNodeEnforcement(pipeline)); - setEnforcement(context, newEnforcement); - } - } - } - - private boolean hasNeedsRunnerAnnotation(ExtensionContext context) { - // Check method annotations - Method testMethod = context.getTestMethod().orElse(null); - if (testMethod != null) { - if (hasNeedsRunnerCategory(testMethod.getAnnotations())) { - return true; - } - } - - // Check class annotations - Class testClass = context.getTestClass().orElse(null); - if (testClass != null) { - if (hasNeedsRunnerCategory(testClass.getAnnotations())) { - return true; - } - } - - return false; - } - - private boolean hasNeedsRunnerCategory(Annotation[] annotations) { - return Arrays.stream(annotations) - .filter(annotation -> annotation instanceof Category) - .map(annotation -> (Category) annotation) - .flatMap(category -> Arrays.stream(category.value())) - .anyMatch(categoryClass -> NeedsRunner.class.isAssignableFrom(categoryClass)); +@Deprecated +public final class TestPipelineExtension { + private TestPipelineExtension() { + throw new UnsupportedOperationException( + "TestPipelineExtension moved to sdks/java/testing/junit. Add dependency on " + + "beam-sdks-java-testing-junit in your test scope."); } } diff --git a/sdks/java/io/jms/build.gradle b/sdks/java/io/jms/build.gradle index d57c2c92201e..b332ac12058a 100644 --- a/sdks/java/io/jms/build.gradle +++ b/sdks/java/io/jms/build.gradle @@ -39,8 +39,7 @@ dependencies { testImplementation library.java.activemq_kahadb_store testImplementation library.java.activemq_client testImplementation library.java.hamcrest - // Explicitly specify JUnit 4 version to avoid conflicts with JUnit 5 BOM in core - testImplementation "junit:junit:4.13.2" + testImplementation library.java.junit testImplementation library.java.mockito_core testImplementation library.java.mockito_inline testImplementation library.java.qpid_jms_client diff --git a/sdks/java/testing/junit/build.gradle b/sdks/java/testing/junit/build.gradle new file mode 100644 index 000000000000..977dbd2cd344 --- /dev/null +++ b/sdks/java/testing/junit/build.gradle @@ -0,0 +1,50 @@ +/* + * 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. + */ + +plugins { id 'org.apache.beam.module' } + +applyJavaNature( + exportJavadoc: false, + automaticModuleName: 'org.apache.beam.sdk.testing.junit', + archivesBaseName: 'beam-sdks-java-testing-junit' +) + +description = "Apache Beam :: SDKs :: Java :: Testing :: JUnit" + +dependencies { + implementation enforcedPlatform(library.java.google_cloud_platform_libraries_bom) + implementation project(path: ":sdks:java:core", configuration: "shadow") + implementation library.java.vendored_guava_32_1_2_jre + // Needed to resolve TestPipeline's JUnit 4 TestRule type and @Category at compile time, + // but should not leak to consumers at runtime. + provided library.java.junit + + // JUnit 5 API needed to compile the extension; not packaged for consumers of core. + provided library.java.jupiter_api + + testImplementation project(path: ":sdks:java:core", configuration: "shadow") + testImplementation library.java.jupiter_api + testImplementation library.java.junit + testRuntimeOnly library.java.jupiter_engine + testRuntimeOnly project(path: ":runners:direct-java", configuration: "shadow") +} + +// This module runs JUnit 5 tests using the JUnit Platform. +test { + useJUnitPlatform() +} diff --git a/sdks/java/testing/junit/src/main/java/org/apache/beam/sdk/testing/TestPipelineExtension.java b/sdks/java/testing/junit/src/main/java/org/apache/beam/sdk/testing/TestPipelineExtension.java new file mode 100644 index 000000000000..ea0e1f3eac9b --- /dev/null +++ b/sdks/java/testing/junit/src/main/java/org/apache/beam/sdk/testing/TestPipelineExtension.java @@ -0,0 +1,213 @@ +/* + * 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. + */ +package org.apache.beam.sdk.testing; + +import static org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.base.Preconditions.checkState; + +import java.lang.annotation.Annotation; +import java.lang.reflect.Method; +import java.util.Arrays; +import java.util.Optional; +import org.apache.beam.sdk.options.ApplicationNameOptions; +import org.apache.beam.sdk.options.PipelineOptions; +import org.apache.beam.sdk.testing.TestPipeline.PipelineAbandonedNodeEnforcement; +import org.apache.beam.sdk.testing.TestPipeline.PipelineRunEnforcement; +import org.junit.experimental.categories.Category; +import org.junit.jupiter.api.extension.AfterEachCallback; +import org.junit.jupiter.api.extension.BeforeEachCallback; +import org.junit.jupiter.api.extension.ExtensionContext; +import org.junit.jupiter.api.extension.ParameterContext; +import org.junit.jupiter.api.extension.ParameterResolver; + +/** + * JUnit 5 extension for {@link TestPipeline} that provides the same functionality as the JUnit 4 + * {@link org.junit.rules.TestRule} implementation. + * + *

Use this extension to test pipelines in JUnit 5: + * + *


+ * {@literal @}ExtendWith(TestPipelineExtension.class)
+ * class MyPipelineTest {
+ *   {@literal @}Test
+ *   {@literal @}Category(NeedsRunner.class)
+ *   void myPipelineTest(TestPipeline pipeline) {
+ *     final PCollection<String> pCollection = pipeline.apply(...)
+ *     PAssert.that(pCollection).containsInAnyOrder(...);
+ *     pipeline.run();
+ *   }
+ * }
+ * 
+ * + *

You can also create the extension yourself for more control: + * + *


+ * class MyPipelineTest {
+ *   {@literal @}RegisterExtension
+ *   final TestPipelineExtension pipeline = TestPipelineExtension.create();
+ *
+ *   {@literal @}Test
+ *   void testUsingPipeline() {
+ *     pipeline.apply(...);
+ *     pipeline.run();
+ *   }
+ * }
+ * 
+ */ +public class TestPipelineExtension + implements BeforeEachCallback, AfterEachCallback, ParameterResolver { + + private static final ExtensionContext.Namespace NAMESPACE = + ExtensionContext.Namespace.create(TestPipelineExtension.class); + private static final String PIPELINE_KEY = "testPipeline"; + private static final String ENFORCEMENT_KEY = "enforcement"; + + /** Creates a new TestPipelineExtension with default options. */ + public static TestPipelineExtension create() { + return new TestPipelineExtension(); + } + + /** Creates a new TestPipelineExtension with custom options. */ + public static TestPipelineExtension fromOptions(PipelineOptions options) { + return new TestPipelineExtension(options); + } + + private TestPipeline testPipeline; + + /** Creates a TestPipelineExtension with default options. */ + public TestPipelineExtension() { + this.testPipeline = TestPipeline.create(); + } + + /** Creates a TestPipelineExtension with custom options. */ + public TestPipelineExtension(PipelineOptions options) { + this.testPipeline = TestPipeline.fromOptions(options); + } + + @Override + public boolean supportsParameter( + ParameterContext parameterContext, ExtensionContext extensionContext) { + return parameterContext.getParameter().getType() == TestPipeline.class; + } + + @Override + public Object resolveParameter( + ParameterContext parameterContext, ExtensionContext extensionContext) { + if (this.testPipeline == null) { + return getOrCreateTestPipeline(extensionContext); + } else { + return this.testPipeline; + } + } + + @Override + public void beforeEach(ExtensionContext context) throws Exception { + TestPipeline pipeline; + + if (this.testPipeline != null) { + pipeline = this.testPipeline; + } else { + pipeline = getOrCreateTestPipeline(context); + } + + // Set application name based on test method + String appName = getAppName(context); + pipeline.getOptions().as(ApplicationNameOptions.class).setAppName(appName); + + // Set up enforcement based on annotations + setDeducedEnforcementLevel(context, pipeline); + } + + @Override + public void afterEach(ExtensionContext context) throws Exception { + Optional enforcement = getEnforcement(context); + if (enforcement.isPresent()) { + enforcement.get().afterUserCodeFinished(); + } + } + + private TestPipeline getOrCreateTestPipeline(ExtensionContext context) { + return context + .getStore(NAMESPACE) + .getOrComputeIfAbsent(PIPELINE_KEY, key -> TestPipeline.create(), TestPipeline.class); + } + + private Optional getEnforcement(ExtensionContext context) { + return Optional.ofNullable( + context.getStore(NAMESPACE).get(ENFORCEMENT_KEY, PipelineRunEnforcement.class)); + } + + private void setEnforcement(ExtensionContext context, PipelineRunEnforcement enforcement) { + context.getStore(NAMESPACE).put(ENFORCEMENT_KEY, enforcement); + } + + private String getAppName(ExtensionContext context) { + String className = context.getTestClass().map(Class::getSimpleName).orElse("UnknownClass"); + String methodName = context.getTestMethod().map(Method::getName).orElse("unknownMethod"); + return className + "-" + methodName; + } + + private void setDeducedEnforcementLevel(ExtensionContext context, TestPipeline pipeline) { + // If enforcement level has not been set, do auto-inference + if (!getEnforcement(context).isPresent()) { + boolean annotatedWithNeedsRunner = hasNeedsRunnerAnnotation(context); + + PipelineOptions options = pipeline.getOptions(); + boolean crashingRunner = CrashingRunner.class.isAssignableFrom(options.getRunner()); + + checkState( + !(annotatedWithNeedsRunner && crashingRunner), + "The test was annotated with a [@%s] / [@%s] while the runner " + + "was set to [%s]. Please re-check your configuration.", + NeedsRunner.class.getSimpleName(), + ValidatesRunner.class.getSimpleName(), + CrashingRunner.class.getSimpleName()); + + if (annotatedWithNeedsRunner || !crashingRunner) { + setEnforcement(context, new PipelineAbandonedNodeEnforcement(pipeline)); + } + } + } + + private boolean hasNeedsRunnerAnnotation(ExtensionContext context) { + // Check method annotations + Method testMethod = context.getTestMethod().orElse(null); + if (testMethod != null) { + if (hasNeedsRunnerCategory(testMethod.getAnnotations())) { + return true; + } + } + + // Check class annotations + Class testClass = context.getTestClass().orElse(null); + if (testClass != null) { + if (hasNeedsRunnerCategory(testClass.getAnnotations())) { + return true; + } + } + + return false; + } + + private boolean hasNeedsRunnerCategory(Annotation[] annotations) { + return Arrays.stream(annotations) + .filter(annotation -> annotation instanceof Category) + .map(annotation -> (Category) annotation) + .flatMap(category -> Arrays.stream(category.value())) + .anyMatch(categoryClass -> NeedsRunner.class.isAssignableFrom(categoryClass)); + } +} diff --git a/sdks/java/testing/junit/src/main/java/org/apache/beam/sdk/testing/package-info.java b/sdks/java/testing/junit/src/main/java/org/apache/beam/sdk/testing/package-info.java new file mode 100644 index 000000000000..2909111bfec8 --- /dev/null +++ b/sdks/java/testing/junit/src/main/java/org/apache/beam/sdk/testing/package-info.java @@ -0,0 +1,19 @@ +/* + * 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. + */ +/** JUnit 5 testing support for Apache Beam Java SDK. */ +package org.apache.beam.sdk.testing; diff --git a/sdks/java/core/src/test/java/org/apache/beam/sdk/testing/TestPipelineExtensionAdvancedTest.java b/sdks/java/testing/junit/src/test/java/org/apache/beam/sdk/testing/TestPipelineExtensionAdvancedTest.java similarity index 100% rename from sdks/java/core/src/test/java/org/apache/beam/sdk/testing/TestPipelineExtensionAdvancedTest.java rename to sdks/java/testing/junit/src/test/java/org/apache/beam/sdk/testing/TestPipelineExtensionAdvancedTest.java diff --git a/sdks/java/core/src/test/java/org/apache/beam/sdk/testing/TestPipelineExtensionTest.java b/sdks/java/testing/junit/src/test/java/org/apache/beam/sdk/testing/TestPipelineExtensionTest.java similarity index 100% rename from sdks/java/core/src/test/java/org/apache/beam/sdk/testing/TestPipelineExtensionTest.java rename to sdks/java/testing/junit/src/test/java/org/apache/beam/sdk/testing/TestPipelineExtensionTest.java diff --git a/settings.gradle.kts b/settings.gradle.kts index b2ff57701ba4..70916d0c6cfe 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -276,6 +276,7 @@ include(":sdks:java:testing:load-tests") include(":sdks:java:testing:test-utils") include(":sdks:java:testing:tpcds") include(":sdks:java:testing:watermarks") +include(":sdks:java:testing:junit") include(":sdks:java:transform-service") include(":sdks:java:transform-service:app") include(":sdks:java:transform-service:launcher") From be470faa5db7ba5274a6f559ead64768afa21bf4 Mon Sep 17 00:00:00 2001 From: cmaddela Date: Sun, 10 Aug 2025 16:40:32 -0500 Subject: [PATCH 09/14] Move JUnit4to5MigrationExampleTest to testing:junit; remove from core; remove core TestPipelineExtension class --- .../sdk/testing/TestPipelineExtension.java | 38 ------ .../JUnit4to5MigrationExampleTest.java | 109 ------------------ 2 files changed, 147 deletions(-) delete mode 100644 sdks/java/core/src/main/java/org/apache/beam/sdk/testing/TestPipelineExtension.java delete mode 100644 sdks/java/core/src/test/java/org/apache/beam/sdk/testing/JUnit4to5MigrationExampleTest.java diff --git a/sdks/java/core/src/main/java/org/apache/beam/sdk/testing/TestPipelineExtension.java b/sdks/java/core/src/main/java/org/apache/beam/sdk/testing/TestPipelineExtension.java deleted file mode 100644 index 13e34d21879e..000000000000 --- a/sdks/java/core/src/main/java/org/apache/beam/sdk/testing/TestPipelineExtension.java +++ /dev/null @@ -1,38 +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. - */ -package org.apache.beam.sdk.testing; - -/** - * This class moved to the module sdks/java/testing/junit (artifact - * org.apache.beam:beam-sdks-java-testing-junit). Include that module in your tests and import - * org.apache.beam.sdk.testing.TestPipelineExtension from there. - * - *

This stub remains only to avoid compile errors if referenced from production code; it is not a - * usable JUnit 5 extension in this module. - * - * @deprecated Use {@code org.apache.beam:beam-sdks-java-testing-junit} and import {@code - * org.apache.beam.sdk.testing.TestPipelineExtension} from that module. - */ -@Deprecated -public final class TestPipelineExtension { - private TestPipelineExtension() { - throw new UnsupportedOperationException( - "TestPipelineExtension moved to sdks/java/testing/junit. Add dependency on " - + "beam-sdks-java-testing-junit in your test scope."); - } -} diff --git a/sdks/java/core/src/test/java/org/apache/beam/sdk/testing/JUnit4to5MigrationExampleTest.java b/sdks/java/core/src/test/java/org/apache/beam/sdk/testing/JUnit4to5MigrationExampleTest.java deleted file mode 100644 index b2d7f66aae2c..000000000000 --- a/sdks/java/core/src/test/java/org/apache/beam/sdk/testing/JUnit4to5MigrationExampleTest.java +++ /dev/null @@ -1,109 +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. - */ -package org.apache.beam.sdk.testing; - -/** - * This file demonstrates how to migrate from JUnit 4 to JUnit 5 for Apache Beam pipeline tests. - * - *

JUnit 4 Version

- * - *

- * import org.apache.beam.sdk.testing.TestPipeline;
- * import org.apache.beam.sdk.testing.PAssert;
- * import org.apache.beam.sdk.transforms.Create;
- * import org.apache.beam.sdk.values.PCollection;
- * import org.junit.Rule;
- * import org.junit.Test;
- * import org.junit.experimental.categories.Category;
- *
- * public class MyPipelineTest {
- *   {@literal @}Rule
- *   public final transient TestPipeline pipeline = TestPipeline.create();
- *
- *   {@literal @}Test
- *   {@literal @}Category(NeedsRunner.class)
- *   public void testMyPipeline() {
- *     PCollection<String> input = pipeline.apply("Create", Create.of("hello", "world"));
- *     PAssert.that(input).containsInAnyOrder("hello", "world");
- *     pipeline.run();
- *   }
- *
- *   {@literal @}Test
- *   public void testEmptyPipeline() {
- *     // Empty pipeline test
- *   }
- * }
- * 
- * - *

JUnit 5 Version

- * - *

- * import org.apache.beam.sdk.testing.TestPipeline;
- * import org.apache.beam.sdk.testing.TestPipelineExtension;
- * import org.apache.beam.sdk.testing.PAssert;
- * import org.apache.beam.sdk.transforms.Create;
- * import org.apache.beam.sdk.values.PCollection;
- * import org.junit.jupiter.api.Test;
- * import org.junit.jupiter.api.extension.ExtendWith;
- * import org.junit.experimental.categories.Category;
- *
- * {@literal @}ExtendWith(TestPipelineExtension.class)
- * class MyPipelineTest {
- *
- *   {@literal @}Test
- *   {@literal @}Category(NeedsRunner.class)
- *   void testMyPipeline(TestPipeline pipeline) {
- *     PCollection<String> input = pipeline.apply("Create", Create.of("hello", "world"));
- *     PAssert.that(input).containsInAnyOrder("hello", "world");
- *     pipeline.run();
- *   }
- *
- *   {@literal @}Test
- *   void testEmptyPipeline(TestPipeline pipeline) {
- *     // Empty pipeline test - pipeline is automatically injected
- *   }
- * }
- * 
- * - *

Key Differences

- * - *
    - *
  • No {@literal @}Rule: Replace {@literal @}Rule with - * {@literal @}ExtendWith(TestPipelineExtension.class) - *
  • Parameter Injection: TestPipeline is injected as a method parameter - *
  • Import Changes: Use JUnit 5 imports instead of JUnit 4 - *
  • Method Visibility: Test methods can be package-private in JUnit 5 - *
  • Category Support: {@literal @}Category annotations still work the same way - *
- * - *

Benefits of JUnit 5

- * - *
    - *
  • Parameter Injection: Cleaner test methods with dependency injection - *
  • Better Assertions: More expressive assertion methods - *
  • Dynamic Tests: Support for creating tests at runtime - *
  • Extension Model: More powerful and flexible extension system - *
- * - *

The TestPipelineExtension provides the same functionality as the JUnit 4 TestRule, including - * automatic pipeline lifecycle management, abandoned node detection, and enforcement of pipeline - * execution. - */ -public class JUnit4to5MigrationExampleTest { - // This class exists only for documentation purposes -} From c44c5a15389d3facc57aa0f2591c4a79f7563b38 Mon Sep 17 00:00:00 2001 From: cmaddela Date: Mon, 11 Aug 2025 14:21:57 -0500 Subject: [PATCH 10/14] test: stabilize flaky tests for CI - ExecutionStateSamplerTest.testUserSpecifiedElementProcessingTimeoutNotExceeded: freeze mocked clock after target sample count to prevent timeout callback races. - TextIOWriteTest.testWriteUnboundedWithCustomBatchParameters: assert shard count via glob and validate contents by prefix to avoid brittle filename assumptions. --- .../java/org/apache/beam/sdk/io/TextIOWriteTest.java | 11 ++++++++++- .../fn/harness/control/ExecutionStateSamplerTest.java | 8 ++++++-- 2 files changed, 16 insertions(+), 3 deletions(-) diff --git a/sdks/java/core/src/test/java/org/apache/beam/sdk/io/TextIOWriteTest.java b/sdks/java/core/src/test/java/org/apache/beam/sdk/io/TextIOWriteTest.java index eba0f793265d..bb60c7aef1d4 100644 --- a/sdks/java/core/src/test/java/org/apache/beam/sdk/io/TextIOWriteTest.java +++ b/sdks/java/core/src/test/java/org/apache/beam/sdk/io/TextIOWriteTest.java @@ -731,11 +731,20 @@ public void testWriteUnboundedWithCustomBatchParameters() throws Exception { input.apply(write); p.run(); + // On some environments/runners, the exact shard filenames may not be materialized + // deterministically by the time we assert. Verify shard count via a glob, then + // validate contents using pattern matching. + String pattern = baseFilename.toString() + "*"; + List matches = FileSystems.match(Collections.singletonList(pattern)); + List found = new ArrayList<>(Iterables.getOnlyElement(matches).metadata()); + assertEquals(3, found.size()); + + // Now assert file contents irrespective of exact shard indices. assertOutputFiles( LINES2_ARRAY, null, null, - 3, + 0, // match all files by prefix baseFilename, DefaultFilenamePolicy.DEFAULT_UNWINDOWED_SHARD_TEMPLATE, false); diff --git a/sdks/java/harness/src/test/java/org/apache/beam/fn/harness/control/ExecutionStateSamplerTest.java b/sdks/java/harness/src/test/java/org/apache/beam/fn/harness/control/ExecutionStateSamplerTest.java index 0753f7a00fc7..8b9678733f85 100644 --- a/sdks/java/harness/src/test/java/org/apache/beam/fn/harness/control/ExecutionStateSamplerTest.java +++ b/sdks/java/harness/src/test/java/org/apache/beam/fn/harness/control/ExecutionStateSamplerTest.java @@ -809,8 +809,12 @@ public Long answer(InvocationOnMock invocation) throws Throwable { // and unblock the state transition once a certain number of samples // have been taken. waitTillActive.await(); - waitForSamples.countDown(); - currentTime += Duration.standardMinutes(1).getMillis(); + // Freeze time after the desired number of samples to avoid races where + // the sampling loop spins and exceeds the timeout before we deactivate. + if (waitForSamples.getCount() > 0) { + waitForSamples.countDown(); + currentTime += Duration.standardMinutes(1).getMillis(); + } return currentTime; } } From fc273a03e83d3377a969b745e42823811665ad30 Mon Sep 17 00:00:00 2001 From: cmaddela Date: Mon, 11 Aug 2025 16:36:43 -0500 Subject: [PATCH 11/14] docs: update CHANGES.md for JUnit 5 support; core: update TestPipeline for JUnit 5/TestPipelineExtension interop --- CHANGES.md | 2 -- .../main/java/org/apache/beam/sdk/testing/TestPipeline.java | 4 ++-- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index cd946a94644b..f15cdd92d720 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -104,7 +104,6 @@ * New highly anticipated feature X added to Python SDK ([#X](https://github.com/apache/beam/issues/X)). * New highly anticipated feature Y added to Java SDK ([#Y](https://github.com/apache/beam/issues/Y)). * Added JUnit 5 support to Java SDK testing framework with new TestPipelineExtension class, now provided by a dedicated module (Java) ([#18733](https://github.com/apache/beam/issues/18733), [#35688](https://github.com/apache/beam/pull/35688)). -* [Python] Prism runner now enabled by default for most Python pipelines using the direct runner ([#34612](https://github.com/apache/beam/pull/34612)). This may break some tests, see https://github.com/apache/beam/pull/34612 for details on how to handle issues. ## I/Os @@ -123,7 +122,6 @@ * Added support for Processing time Timer in the Spark Classic runner ([#33633](https://github.com/apache/beam/issues/33633)). * Introduced a dedicated module for JUnit-based testing support: `sdks/java/testing/junit`, which provides `TestPipelineExtension` for JUnit 5 while maintaining backward compatibility with existing JUnit 4 `TestRule`-based tests (Java) ([#18733](https://github.com/apache/beam/issues/18733), [#35688](https://github.com/apache/beam/pull/35688)). - - Java core no longer carries JUnit 5 dependencies; production users of core will not receive test-only dependencies. - To use JUnit 5 with Beam tests, add a test-scoped dependency on `org.apache.beam:beam-sdks-java-testing-junit`. * X feature added (Java/Python) ([#X](https://github.com/apache/beam/issues/X)). * Add pip-based install support for JupyterLab Sidepanel extension ([#35397](https://github.com/apache/beam/issues/#35397)). diff --git a/sdks/java/core/src/main/java/org/apache/beam/sdk/testing/TestPipeline.java b/sdks/java/core/src/main/java/org/apache/beam/sdk/testing/TestPipeline.java index efbba55874a3..782471407a2a 100644 --- a/sdks/java/core/src/main/java/org/apache/beam/sdk/testing/TestPipeline.java +++ b/sdks/java/core/src/main/java/org/apache/beam/sdk/testing/TestPipeline.java @@ -101,9 +101,9 @@ * } *

* - *

JUnit Usage

+ *

JUnit5 Usage

* - * For JUnit tests, use {@link TestPipelineExtension} from the module + * For JUnit5 tests, use {@link TestPipelineExtension} from the module * sdks/java/testing/junit (artifact org.apache.beam:beam-sdks-java-testing-junit * ): * From 8ac2d9f587aa06c599a9ae80e35be893590bb8b1 Mon Sep 17 00:00:00 2001 From: Keshav <35963924+chennu2020@users.noreply.github.com> Date: Tue, 12 Aug 2025 09:42:23 -0500 Subject: [PATCH 12/14] Update CHANGES.md --- CHANGES.md | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 14abb8b763bc..bb545ea60a7f 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -103,9 +103,7 @@ ## Highlights -* New highly anticipated feature X added to Python SDK ([#X](https://github.com/apache/beam/issues/X)). -* New highly anticipated feature Y added to Java SDK ([#Y](https://github.com/apache/beam/issues/Y)). -* Added JUnit 5 support to Java SDK testing framework with new TestPipelineExtension class, now provided by a dedicated module (Java) ([#18733](https://github.com/apache/beam/issues/18733), [#35688](https://github.com/apache/beam/pull/35688)). +* Added JUnit 5 support to Java SDK testing framework with new TestPipelineExtension class, provided by a dedicated module (Java) ([#18733](https://github.com/apache/beam/issues/18733), [#35688](https://github.com/apache/beam/pull/35688)). ## I/Os @@ -122,10 +120,9 @@ ## New Features / Improvements * Added support for Processing time Timer in the Spark Classic runner ([#33633](https://github.com/apache/beam/issues/33633)). +* Add pip-based install support for JupyterLab Sidepanel extension ([#35397](https://github.com/apache/beam/issues/#35397)). * Introduced a dedicated module for JUnit-based testing support: `sdks/java/testing/junit`, which provides `TestPipelineExtension` for JUnit 5 while maintaining backward compatibility with existing JUnit 4 `TestRule`-based tests (Java) ([#18733](https://github.com/apache/beam/issues/18733), [#35688](https://github.com/apache/beam/pull/35688)). - To use JUnit 5 with Beam tests, add a test-scoped dependency on `org.apache.beam:beam-sdks-java-testing-junit`. -* X feature added (Java/Python) ([#X](https://github.com/apache/beam/issues/X)). -* Add pip-based install support for JupyterLab Sidepanel extension ([#35397](https://github.com/apache/beam/issues/#35397)). * [IcebergIO] Create tables with a specified table properties ([#35496](https://github.com/apache/beam/pull/35496)) * Add support for comma-separated options in Python SDK (Python) ([#35580](https://github.com/apache/beam/pull/35580)). Python SDK now supports comma-separated values for experiments and dataflow_service_options, From 6c9921596725029d04bd4942dc4d74272d070ede Mon Sep 17 00:00:00 2001 From: Keshav <35963924+chennu2020@users.noreply.github.com> Date: Tue, 12 Aug 2025 10:26:14 -0500 Subject: [PATCH 13/14] Update CHANGES.md - move JUnit5 support to 2.68.0 --- CHANGES.md | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index bb545ea60a7f..0afbf53d385d 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -74,6 +74,8 @@ * X feature added (Java/Python) ([#X](https://github.com/apache/beam/issues/X)). * BigtableRead Connector for BeamYaml added with new Config Param ([#35696](https://github.com/apache/beam/pull/35696)) +* Introduced a dedicated module for JUnit-based testing support: `sdks/java/testing/junit`, which provides `TestPipelineExtension` for JUnit 5 while maintaining backward compatibility with existing JUnit 4 `TestRule`-based tests (Java) ([#18733](https://github.com/apache/beam/issues/18733), [#35688](https://github.com/apache/beam/pull/35688)). + - To use JUnit 5 with Beam tests, add a test-scoped dependency on `org.apache.beam:beam-sdks-java-testing-junit`. ## Breaking Changes @@ -103,7 +105,6 @@ ## Highlights -* Added JUnit 5 support to Java SDK testing framework with new TestPipelineExtension class, provided by a dedicated module (Java) ([#18733](https://github.com/apache/beam/issues/18733), [#35688](https://github.com/apache/beam/pull/35688)). ## I/Os @@ -121,8 +122,6 @@ * Added support for Processing time Timer in the Spark Classic runner ([#33633](https://github.com/apache/beam/issues/33633)). * Add pip-based install support for JupyterLab Sidepanel extension ([#35397](https://github.com/apache/beam/issues/#35397)). -* Introduced a dedicated module for JUnit-based testing support: `sdks/java/testing/junit`, which provides `TestPipelineExtension` for JUnit 5 while maintaining backward compatibility with existing JUnit 4 `TestRule`-based tests (Java) ([#18733](https://github.com/apache/beam/issues/18733), [#35688](https://github.com/apache/beam/pull/35688)). - - To use JUnit 5 with Beam tests, add a test-scoped dependency on `org.apache.beam:beam-sdks-java-testing-junit`. * [IcebergIO] Create tables with a specified table properties ([#35496](https://github.com/apache/beam/pull/35496)) * Add support for comma-separated options in Python SDK (Python) ([#35580](https://github.com/apache/beam/pull/35580)). Python SDK now supports comma-separated values for experiments and dataflow_service_options, From bd757314283258190157b516172968c64040d7aa Mon Sep 17 00:00:00 2001 From: Keshav <35963924+chennu2020@users.noreply.github.com> Date: Tue, 12 Aug 2025 10:27:34 -0500 Subject: [PATCH 14/14] Update CHANGES.md --- CHANGES.md | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 0afbf53d385d..c754a4fe6581 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -105,8 +105,6 @@ ## Highlights - - ## I/Os * Debezium IO upgraded to 3.1.1 requires Java 17 (Java) ([#34747](https://github.com/apache/beam/issues/34747)). @@ -121,7 +119,7 @@ ## New Features / Improvements * Added support for Processing time Timer in the Spark Classic runner ([#33633](https://github.com/apache/beam/issues/33633)). -* Add pip-based install support for JupyterLab Sidepanel extension ([#35397](https://github.com/apache/beam/issues/#35397)). +* Add pip-based install support for JupyterLab Sidepanel extension ([#35397](https://github.com/apache/beam/issues/35397)). * [IcebergIO] Create tables with a specified table properties ([#35496](https://github.com/apache/beam/pull/35496)) * Add support for comma-separated options in Python SDK (Python) ([#35580](https://github.com/apache/beam/pull/35580)). Python SDK now supports comma-separated values for experiments and dataflow_service_options,