Skip to content

Commit 202d939

Browse files
authored
Introduce extension API for container templates (#4315)
Analogous to `@TestTemplate` on the method level, this commit introduces a class-level `@ContainerTemplate` annotation with an accompanying `ContainerTemplateInvocationContextProvider` extension API. Resolves #871.
1 parent 67071ee commit 202d939

File tree

61 files changed

+4228
-282
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

61 files changed

+4228
-282
lines changed

documentation/src/docs/asciidoc/link-attributes.adoc

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,7 @@ endif::[]
111111
:ClassOrderer_OrderAnnotation: {javadoc-root}/org.junit.jupiter.api/org/junit/jupiter/api/ClassOrderer.OrderAnnotation.html[ClassOrderer.OrderAnnotation]
112112
:ClassOrderer_Random: {javadoc-root}/org.junit.jupiter.api/org/junit/jupiter/api/ClassOrderer.Random.html[ClassOrderer.Random]
113113
:ClassOrderer: {javadoc-root}/org.junit.jupiter.api/org/junit/jupiter/api/ClassOrderer.html[ClassOrderer]
114+
:ContainerTemplate: {javadoc-root}/org.junit.jupiter.api/org/junit/jupiter/api/ContainerTemplate.html[@ContainerTemplate]
114115
:Disabled: {javadoc-root}/org.junit.jupiter.api/org/junit/jupiter/api/Disabled.html[@Disabled]
115116
:MethodOrderer_Alphanumeric: {javadoc-root}/org.junit.jupiter.api/org/junit/jupiter/api/MethodOrderer.Alphanumeric.html[MethodOrderer.Alphanumeric]
116117
:MethodOrderer_DisplayName: {javadoc-root}/org.junit.jupiter.api/org/junit/jupiter/api/MethodOrderer.DisplayName.html[MethodOrderer.DisplayName]
@@ -142,6 +143,8 @@ endif::[]
142143
:BeforeAllCallback: {javadoc-root}/org.junit.jupiter.api/org/junit/jupiter/api/extension/BeforeAllCallback.html[BeforeAllCallback]
143144
:BeforeEachCallback: {javadoc-root}/org.junit.jupiter.api/org/junit/jupiter/api/extension/BeforeEachCallback.html[BeforeEachCallback]
144145
:BeforeTestExecutionCallback: {javadoc-root}/org.junit.jupiter.api/org/junit/jupiter/api/extension/BeforeTestExecutionCallback.html[BeforeTestExecutionCallback]
146+
:ContainerTemplateInvocationContext: {javadoc-root}/org.junit.jupiter.api/org/junit/jupiter/api/extension/ContainerTemplateInvocationContext.html[ContainerTemplateInvocationContext]
147+
:ContainerTemplateInvocationContextProvider: {javadoc-root}/org.junit.jupiter.api/org/junit/jupiter/api/extension/ContainerTemplateInvocationContextProvider.html[ContainerTemplateInvocationContextProvider]
145148
:ExecutableInvoker: {javadoc-root}/org.junit.jupiter.api/org/junit/jupiter/api/extension/ExecutableInvoker.html[ExecutableInvoker]
146149
:ExecutionCondition: {javadoc-root}/org.junit.jupiter.api/org/junit/jupiter/api/extension/ExecutionCondition.html[ExecutionCondition]
147150
:ExtendWith: {javadoc-root}/org.junit.jupiter.api/org/junit/jupiter/api/extension/ExtendWith.html[@ExtendWith]

documentation/src/docs/asciidoc/release-notes/release-notes-5.13.0-M1.adoc

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,11 @@ repository on GitHub.
4545
[[release-notes-5.13.0-M1-junit-jupiter-new-features-and-improvements]]
4646
==== New Features and Improvements
4747

48-
* ❓
48+
* Introduce `@ContainerTemplate` and `ContainerTemplateInvocationContextProvider` that
49+
allow declaring a top-level or `@Nested` test class as a template to be invoked multiple
50+
times. This may be used, for example, to inject different parameters to be used by all
51+
tests in the container template class or to set up each invocation of the container
52+
template differently.
4953

5054

5155
[[release-notes-5.13.0-M1-junit-vintage]]

documentation/src/docs/asciidoc/user-guide/extensions.adoc

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -765,6 +765,46 @@ You may override the `getTestInstantiationExtensionContextScope(...)` method to
765765
on the test method level.
766766
====
767767

768+
[[extensions-container-templates]]
769+
=== Providing Invocation Contexts for Container Templates
770+
771+
A `{ContainerTemplate}` class can only be executed when at least one
772+
`{ContainerTemplateInvocationContextProvider}` is registered. Each such provider is
773+
responsible for providing a `Stream` of `{ContainerTemplateInvocationContext}` instances.
774+
Each context may specify a custom display name and a list of additional extensions that
775+
will only be used for the next invocation of the `{ContainerTemplate}` class.
776+
777+
The following example shows how to write a container template as well as how to register
778+
and implement a `{ContainerTemplateInvocationContextProvider}`.
779+
780+
[source,java,indent=0]
781+
.A container template with accompanying extension
782+
----
783+
include::{testDir}/example/ContainerTemplateDemo.java[tags=user_guide]
784+
----
785+
786+
In this example, the container template will be invoked twice, meaning all test methods in
787+
the container template class will be executed twice. The display names of the container
788+
invocations will be `apple` and `banana` as specified by the invocation context. Each
789+
invocation registers a custom `{TestInstancePostProcessor}` which is used to inject a
790+
value into a field. The output when using the `ConsoleLauncher` is as follows.
791+
792+
....
793+
└─ ContainerTemplateDemo ✔
794+
├─ apple ✔
795+
│ ├─ notNull() ✔
796+
│ └─ wellKnown() ✔
797+
└─ banana ✔
798+
├─ notNull() ✔
799+
└─ wellKnown() ✔
800+
....
801+
802+
The `{ContainerTemplateInvocationContextProvider}` extension API is primarily intended for
803+
implementing different kinds of tests that rely on repetitive invocation of _all_ test
804+
methods in a test class albeit in different contexts — for example, with different
805+
parameters, by preparing the test class instance differently, or multiple times without
806+
modifying the context.
807+
768808
[[extensions-test-templates]]
769809
=== Providing Invocation Contexts for Test Templates
770810

documentation/src/docs/asciidoc/user-guide/writing-tests.adoc

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ in the `junit-jupiter-api` module.
3232
| `@ParameterizedTest` | Denotes that a method is a <<writing-tests-parameterized-tests, parameterized test>>. Such methods are inherited unless they are overridden.
3333
| `@RepeatedTest` | Denotes that a method is a test template for a <<writing-tests-repeated-tests, repeated test>>. Such methods are inherited unless they are overridden.
3434
| `@TestFactory` | Denotes that a method is a test factory for <<writing-tests-dynamic-tests, dynamic tests>>. Such methods are inherited unless they are overridden.
35-
| `@TestTemplate` | Denotes that a method is a <<writing-tests-test-templates, template for test cases>> designed to be invoked multiple times depending on the number of invocation contexts returned by the registered <<extensions-test-templates, providers>>. Such methods are inherited unless they are overridden.
35+
| `@TestTemplate` | Denotes that a method is a <<writing-tests-test-templates, template for a test case>> designed to be invoked multiple times depending on the number of invocation contexts returned by the registered <<extensions-test-templates, providers>>. Such methods are inherited unless they are overridden.
3636
| `@TestClassOrder` | Used to configure the <<writing-tests-test-execution-order-classes, test class execution order>> for `@Nested` test classes in the annotated test class. Such annotations are inherited.
3737
| `@TestMethodOrder` | Used to configure the <<writing-tests-test-execution-order-methods, test method execution order>> for the annotated test class; similar to JUnit 4's `@FixMethodOrder`. Such annotations are inherited.
3838
| `@TestInstance` | Used to configure the <<writing-tests-test-instance-lifecycle, test instance lifecycle>> for the annotated test class. Such annotations are inherited.
@@ -42,6 +42,7 @@ in the `junit-jupiter-api` module.
4242
| `@AfterEach` | Denotes that the annotated method should be executed _after_ *each* `@Test`, `@RepeatedTest`, `@ParameterizedTest`, or `@TestFactory` method in the current class; analogous to JUnit 4's `@After`. Such methods are inherited unless they are overridden.
4343
| `@BeforeAll` | Denotes that the annotated method should be executed _before_ *all* `@Test`, `@RepeatedTest`, `@ParameterizedTest`, and `@TestFactory` methods in the current class; analogous to JUnit 4's `@BeforeClass`. Such methods are inherited unless they are overridden and must be `static` unless the "per-class" <<writing-tests-test-instance-lifecycle, test instance lifecycle>> is used.
4444
| `@AfterAll` | Denotes that the annotated method should be executed _after_ *all* `@Test`, `@RepeatedTest`, `@ParameterizedTest`, and `@TestFactory` methods in the current class; analogous to JUnit 4's `@AfterClass`. Such methods are inherited unless they are overridden and must be `static` unless the "per-class" <<writing-tests-test-instance-lifecycle, test instance lifecycle>> is used.
45+
| `@ContainerTemplate` | Denotes that the annotated class is a <<writing-tests-container-templates, template for a set of test cases>> designed to be executed multiple times depending on the number of invocation contexts returned by the registered <<extensions-container-templates, providers>>.
4546
| `@Nested` | Denotes that the annotated class is a non-static <<writing-tests-nested,nested test class>>. On Java 8 through Java 15, `@BeforeAll` and `@AfterAll` methods cannot be used directly in a `@Nested` test class unless the "per-class" <<writing-tests-test-instance-lifecycle, test instance lifecycle>> is used. Beginning with Java 16, `@BeforeAll` and `@AfterAll` methods can be declared as `static` in a `@Nested` test class with either test instance lifecycle mode. Such annotations are not inherited.
4647
| `@Tag` | Used to declare <<writing-tests-tagging-and-filtering,tags for filtering tests>>, either at the class or method level; analogous to test groups in TestNG or Categories in JUnit 4. Such annotations are inherited at the class level but not at the method level.
4748
| `@Disabled` | Used to <<writing-tests-disabling,disable>> a test class or test method; analogous to JUnit 4's `@Ignore`. Such annotations are not inherited.
@@ -2448,12 +2449,22 @@ lifecycle methods (e.g. `@BeforeEach`) and test class constructors.
24482449
include::{testDir}/example/ParameterizedTestDemo.java[tags=ParameterResolver_example]
24492450
----
24502451

2452+
[[writing-tests-container-templates]]
2453+
=== Container Templates
2454+
2455+
A `{ContainerTemplate}` class is not a regular test class but rather a template for the
2456+
contained test cases. As such, it is designed to be invoked multiple times depending on
2457+
invocation contexts returned by the registered providers. Thus, it must be used in
2458+
conjunction with a registered `{ContainerTemplateInvocationContextProvider}` extension.
2459+
Each invocation of a container template class behaves like the execution of a regular test
2460+
class with full support for the same lifecycle callbacks and extensions. Please refer to
2461+
<<extensions-container-templates>> for usage examples.
24512462

24522463
[[writing-tests-test-templates]]
24532464
=== Test Templates
24542465

2455-
A `{TestTemplate}` method is not a regular test case but rather a template for test
2456-
cases. As such, it is designed to be invoked multiple times depending on the number of
2466+
A `{TestTemplate}` method is not a regular test case but rather a template for a test
2467+
case. As such, it is designed to be invoked multiple times depending on the number of
24572468
invocation contexts returned by the registered providers. Thus, it must be used in
24582469
conjunction with a registered `{TestTemplateInvocationContextProvider}` extension. Each
24592470
invocation of a test template method behaves like the execution of a regular `@Test`
Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
/*
2+
* Copyright 2015-2025 the original author or authors.
3+
*
4+
* All rights reserved. This program and the accompanying materials are
5+
* made available under the terms of the Eclipse Public License v2.0 which
6+
* accompanies this distribution and is available at
7+
*
8+
* https://www.eclipse.org/legal/epl-v20.html
9+
*/
10+
11+
package example;
12+
13+
import static java.util.Collections.singletonList;
14+
import static java.util.Collections.unmodifiableList;
15+
import static org.junit.jupiter.api.Assertions.assertNotNull;
16+
import static org.junit.jupiter.api.Assertions.assertTrue;
17+
18+
import java.util.Arrays;
19+
import java.util.List;
20+
import java.util.stream.Stream;
21+
22+
import example.ContainerTemplateDemo.MyContainerTemplateInvocationContextProvider;
23+
24+
import org.junit.jupiter.api.ContainerTemplate;
25+
import org.junit.jupiter.api.Test;
26+
import org.junit.jupiter.api.extension.ContainerTemplateInvocationContext;
27+
import org.junit.jupiter.api.extension.ContainerTemplateInvocationContextProvider;
28+
import org.junit.jupiter.api.extension.ExtendWith;
29+
import org.junit.jupiter.api.extension.Extension;
30+
import org.junit.jupiter.api.extension.ExtensionContext;
31+
import org.junit.jupiter.api.extension.TestInstancePostProcessor;
32+
33+
// tag::user_guide[]
34+
@ContainerTemplate
35+
@ExtendWith(MyContainerTemplateInvocationContextProvider.class)
36+
class ContainerTemplateDemo {
37+
38+
static final List<String> WELL_KNOWN_FRUITS
39+
// tag::custom_line_break[]
40+
= unmodifiableList(Arrays.asList("apple", "banana", "lemon"));
41+
42+
private String fruit;
43+
44+
@Test
45+
void notNull() {
46+
assertNotNull(fruit);
47+
}
48+
49+
@Test
50+
void wellKnown() {
51+
assertTrue(WELL_KNOWN_FRUITS.contains(fruit));
52+
}
53+
54+
// end::user_guide[]
55+
static
56+
// tag::user_guide[]
57+
public class MyContainerTemplateInvocationContextProvider
58+
// tag::custom_line_break[]
59+
implements ContainerTemplateInvocationContextProvider {
60+
61+
@Override
62+
public boolean supportsContainerTemplate(ExtensionContext context) {
63+
return true;
64+
}
65+
66+
@Override
67+
public Stream<ContainerTemplateInvocationContext>
68+
// tag::custom_line_break[]
69+
provideContainerTemplateInvocationContexts(ExtensionContext context) {
70+
71+
return Stream.of(invocationContext("apple"), invocationContext("banana"));
72+
}
73+
74+
private ContainerTemplateInvocationContext invocationContext(String parameter) {
75+
return new ContainerTemplateInvocationContext() {
76+
@Override
77+
public String getDisplayName(int invocationIndex) {
78+
return parameter;
79+
}
80+
81+
// end::user_guide[]
82+
@SuppressWarnings("Convert2Lambda")
83+
// tag::user_guide[]
84+
@Override
85+
public List<Extension> getAdditionalExtensions() {
86+
return singletonList(new TestInstancePostProcessor() {
87+
@Override
88+
public void postProcessTestInstance(
89+
// tag::custom_line_break[]
90+
Object testInstance, ExtensionContext context) {
91+
((ContainerTemplateDemo) testInstance).fruit = parameter;
92+
}
93+
});
94+
}
95+
};
96+
}
97+
}
98+
}
99+
// end::user_guide[]

junit-jupiter-api/src/main/java/org/junit/jupiter/api/AfterAll.java

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,13 @@
2525
* executed <em>after</em> <strong>all</strong> tests in the current test class.
2626
*
2727
* <p>In contrast to {@link AfterEach @AfterEach} methods, {@code @AfterAll}
28-
* methods are only executed once for a given test class.
28+
* methods are only executed once per execution of a given test class. If the
29+
* test class is annotated with {@link ContainerTemplate @ContainerTemplate},
30+
* the {@code @AfterAll} methods are executed once after the last invocation of
31+
* the container template. If a {@link Nested @Nested} test class is declared in
32+
* a {@link ContainerTemplate @ContainerTemplate} class, its {@code @AfterAll}
33+
* methods are called once per execution of the nested test class, namely, once
34+
* per invocation of the outer container template.
2935
*
3036
* <h2>Method Signatures</h2>
3137
*

junit-jupiter-api/src/main/java/org/junit/jupiter/api/BeforeAll.java

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,13 @@
2525
* executed <em>before</em> <strong>all</strong> tests in the current test class.
2626
*
2727
* <p>In contrast to {@link BeforeEach @BeforeEach} methods, {@code @BeforeAll}
28-
* methods are only executed once for a given test class.
28+
* methods are only executed once per execution of a given test class. If the
29+
* test class is annotated with {@link ContainerTemplate @ContainerTemplate},
30+
* the {@code @BeforeAll} methods are executed once before the first invocation
31+
* of the container template. If a {@link Nested @Nested} test class is declared
32+
* in a {@link ContainerTemplate @ContainerTemplate} class, its
33+
* {@code @BeforeAll} methods are called once per execution of the nested test
34+
* class, namely, once per invocation of the outer container template.
2935
*
3036
* <h2>Method Signatures</h2>
3137
*
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
/*
2+
* Copyright 2015-2025 the original author or authors.
3+
*
4+
* All rights reserved. This program and the accompanying materials are
5+
* made available under the terms of the Eclipse Public License v2.0 which
6+
* accompanies this distribution and is available at
7+
*
8+
* https://www.eclipse.org/legal/epl-v20.html
9+
*/
10+
11+
package org.junit.jupiter.api;
12+
13+
import static org.apiguardian.api.API.Status.EXPERIMENTAL;
14+
15+
import java.lang.annotation.Documented;
16+
import java.lang.annotation.ElementType;
17+
import java.lang.annotation.Retention;
18+
import java.lang.annotation.RetentionPolicy;
19+
import java.lang.annotation.Target;
20+
21+
import org.apiguardian.api.API;
22+
import org.junit.platform.commons.annotation.Testable;
23+
24+
/**
25+
* {@code @ContainerTemplate} is used to signal that the annotated class is a
26+
* <em>container template</em>.
27+
*
28+
* <p>In contrast to regular test classes, a container template is not directly
29+
* a test class but rather a template for a set of test cases. As such, it is
30+
* designed to be invoked multiple times depending on the number of {@linkplain
31+
* org.junit.jupiter.api.extension.ContainerTemplateInvocationContext invocation
32+
* contexts} returned by the registered {@linkplain
33+
* org.junit.jupiter.api.extension.ContainerTemplateInvocationContextProvider
34+
* providers}. Must be used together with at least one provider. Otherwise,
35+
* execution will fail.
36+
*
37+
* <p>Each invocation of a container template method behaves like the execution
38+
* of a regular test class with full support for the same lifecycle callbacks
39+
* and extensions.
40+
*
41+
* <p>{@code @ContainerTemplate} may be combined with {@link Nested @Nested} and
42+
* a container template may contain regular nested test classes or nested
43+
* container templates.
44+
*
45+
* <p>{@code @ContainerTemplate} may also be used as a meta-annotation in order
46+
* to create a custom <em>composed annotation</em> that inherits the semantics
47+
* of {@code @ContainerTemplate}.
48+
*
49+
* <h2>Inheritance</h2>
50+
*
51+
* <p>The {@code @ContainerTemplate} annotation is not inherited to subclasses
52+
* but needs to be declared on each container template class individually.
53+
*
54+
* @since 5.13
55+
* @see TestTemplate
56+
* @see org.junit.jupiter.api.extension.ContainerTemplateInvocationContext
57+
* @see org.junit.jupiter.api.extension.ContainerTemplateInvocationContextProvider
58+
*/
59+
@Target({ ElementType.ANNOTATION_TYPE, ElementType.TYPE })
60+
@Retention(RetentionPolicy.RUNTIME)
61+
@Documented
62+
@API(status = EXPERIMENTAL, since = "5.13")
63+
@Testable
64+
public @interface ContainerTemplate {
65+
}

junit-jupiter-api/src/main/java/org/junit/jupiter/api/Nested.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,9 @@
3131
* <p>{@code @Nested} test classes may be ordered via
3232
* {@link TestClassOrder @TestClassOrder} or a global {@link ClassOrderer}.
3333
*
34+
* <p>{@code @Nested} may be combined with
35+
* {@link ContainerTemplate @ContainerTemplate}.
36+
*
3437
* <h2>Test Instance Lifecycle</h2>
3538
*
3639
* <ul>
@@ -42,6 +45,7 @@
4245
* </ul>
4346
*
4447
* @since 5.0
48+
* @see ContainerTemplate
4549
* @see Test
4650
* @see TestInstance
4751
* @see TestClassOrder

junit-jupiter-api/src/main/java/org/junit/jupiter/api/TestInstance.java

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,12 @@ enum Lifecycle {
8686

8787
/**
8888
* When using this mode, a new test instance will be created once per
89-
* test class.
89+
* test or container template class.
90+
*
91+
* <p>For {@link Nested @Nested}</p> test classes declared inside an
92+
* enclosing {@link ContainerTemplate @ContainerTemplate} test class, an
93+
* instance of the {@code @Nested} class will be created for each
94+
* invocation of the {@code @ContainerTemplate} test class.
9095
*
9196
* @see #PER_METHOD
9297
*/

junit-jupiter-api/src/main/java/org/junit/jupiter/api/TestTemplate.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,7 @@
7979
*
8080
* @since 5.0
8181
* @see Test
82+
* @see ContainerTemplate
8283
* @see org.junit.jupiter.api.extension.TestTemplateInvocationContext
8384
* @see org.junit.jupiter.api.extension.TestTemplateInvocationContextProvider
8485
*/

0 commit comments

Comments
 (0)