Skip to content

Commit c906b66

Browse files
authored
Add constructor injection support for TempDir (#4060)
This was made possible by lifting the `ParameterResolver` limitation for accessing the test-scoped `ExtensionContext` for test class constructors in #3445.
1 parent aaac2b5 commit c906b66

File tree

5 files changed

+127
-95
lines changed

5 files changed

+127
-95
lines changed

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@ JUnit repository on GitHub.
6262
`ExtensionContext` while instantiating the test instance.
6363
The behavior enabled by the annotation is expected to eventually become the default in
6464
future versions of JUnit Jupiter.
65+
* `@TempDir` is now supported on test class constructors.
6566

6667

6768
[[release-notes-5.12.0-M1-junit-vintage]]

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

Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3003,7 +3003,8 @@ The built-in `{TempDirectory}` extension is used to create and clean up a tempor
30033003
directory for an individual test or all tests in a test class. It is registered by
30043004
default. To use it, annotate a non-final, unassigned field of type `java.nio.file.Path` or
30053005
`java.io.File` with `{TempDir}` or add a parameter of type `java.nio.file.Path` or
3006-
`java.io.File` annotated with `@TempDir` to a lifecycle method or test method.
3006+
`java.io.File` annotated with `@TempDir` to a test class constructor, lifecycle method, or
3007+
test method.
30073008

30083009
For example, the following test declares a parameter annotated with `@TempDir` for a
30093010
single test method, creates and writes to a file in the temporary directory, and checks
@@ -3028,14 +3029,10 @@ entire test class or method (depending on which level the annotation is used), y
30283029
the `junit.jupiter.tempdir.scope` configuration parameter to `per_context`. However,
30293030
please note that this option is deprecated and will be removed in a future release.
30303031

3031-
`@TempDir` is not supported on constructor parameters. If you wish to retain a single
3032-
reference to a temp directory across lifecycle methods and the current test method, please
3033-
use field injection by annotating an instance field with `@TempDir`.
3034-
30353032
The following example stores a _shared_ temporary directory in a `static` field. This
30363033
allows the same `sharedTempDir` to be used in all lifecycle methods and test methods of
3037-
the test class. For better isolation, you should use an instance field so that each test
3038-
method uses a separate directory.
3034+
the test class. For better isolation, you should use an instance field or constructor
3035+
injection so that each test method uses a separate directory.
30393036

30403037
[source,java,indent=0]
30413038
.A test class that shares a temporary directory across test methods

junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/extension/TempDirectory.java

Lines changed: 3 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,6 @@
2222
import java.io.File;
2323
import java.io.IOException;
2424
import java.lang.reflect.AnnotatedElement;
25-
import java.lang.reflect.Constructor;
2625
import java.lang.reflect.Field;
2726
import java.lang.reflect.Parameter;
2827
import java.nio.file.DirectoryNotEmptyException;
@@ -45,12 +44,12 @@
4544
import org.junit.jupiter.api.extension.AnnotatedElementContext;
4645
import org.junit.jupiter.api.extension.BeforeAllCallback;
4746
import org.junit.jupiter.api.extension.BeforeEachCallback;
47+
import org.junit.jupiter.api.extension.EnableTestScopedConstructorContext;
4848
import org.junit.jupiter.api.extension.ExtensionConfigurationException;
4949
import org.junit.jupiter.api.extension.ExtensionContext;
5050
import org.junit.jupiter.api.extension.ExtensionContext.Namespace;
5151
import org.junit.jupiter.api.extension.ExtensionContext.Store.CloseableResource;
5252
import org.junit.jupiter.api.extension.ParameterContext;
53-
import org.junit.jupiter.api.extension.ParameterResolutionException;
5453
import org.junit.jupiter.api.extension.ParameterResolver;
5554
import org.junit.jupiter.api.io.CleanupMode;
5655
import org.junit.jupiter.api.io.TempDir;
@@ -78,6 +77,7 @@
7877
* @see TempDir
7978
* @see Files#createTempDirectory
8079
*/
80+
@EnableTestScopedConstructorContext
8181
class TempDirectory implements BeforeAllCallback, BeforeEachCallback, ParameterResolver {
8282

8383
static final Namespace NAMESPACE = Namespace.create(TempDirectory.class);
@@ -161,12 +161,7 @@ private void injectFields(ExtensionContext context, Object testInstance, Class<?
161161
*/
162162
@Override
163163
public boolean supportsParameter(ParameterContext parameterContext, ExtensionContext extensionContext) {
164-
boolean annotated = parameterContext.isAnnotated(TempDir.class);
165-
if (annotated && parameterContext.getDeclaringExecutable() instanceof Constructor) {
166-
throw new ParameterResolutionException(
167-
"@TempDir is not supported on constructor parameters. Please use field injection instead.");
168-
}
169-
return annotated;
164+
return parameterContext.isAnnotated(TempDir.class);
170165
}
171166

172167
/**

jupiter-tests/src/test/java/org/junit/jupiter/engine/extension/TempDirectoryPerContextTests.java

Lines changed: 80 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,7 @@ void resetStaticVariables() {
9696
BaseSharedTempDirParameterInjectionTestCase.tempDir = null;
9797
BaseSeparateTempDirsFieldInjectionTestCase.tempDirs.clear();
9898
BaseSeparateTempDirsParameterInjectionTestCase.tempDirs.clear();
99+
BaseConstructorInjectionTestCase.tempDirs.clear();
99100
}
100101

101102
@Test
@@ -217,6 +218,17 @@ void resolvesSharedTempDirWhenAnnotationIsUsedOnBeforeAllMethodParameterWithTest
217218
AnnotationOnBeforeAllMethodParameterWithTestInstancePerClassTestCase.class);
218219
}
219220

221+
@Test
222+
@DisplayName("when @TempDir is used on constructor parameter with @TestInstance(PER_CLASS)")
223+
@Order(25)
224+
void resolvesSharedTempDirWhenAnnotationIsUsedOnConstructorWithTestInstancePerClass() {
225+
var results = executeTestsForClass(SharedTempDirsConstructorInjectionPerClassTestCase.class);
226+
227+
results.testEvents().assertStatistics(stats -> stats.started(2).failed(0).succeeded(2));
228+
assertThat(BaseConstructorInjectionTestCase.tempDirs.getFirst()).doesNotExist();
229+
assertThat(BaseConstructorInjectionTestCase.tempDirs.getLast()).doesNotExist();
230+
}
231+
220232
private void assertSharedTempDirForFieldInjection(
221233
Class<? extends BaseSharedTempDirFieldInjectionTestCase> testClass) {
222234

@@ -296,6 +308,17 @@ void resolvesSeparateTempDirWhenAnnotationIsUsedOnAfterAllMethodParameterOnly()
296308
assertThat(AnnotationOnAfterAllMethodParameterTestCase.secondTempDir).isNotNull().doesNotExist();
297309
}
298310

311+
@Test
312+
@DisplayName("when @TempDir is used on constructor parameter")
313+
@Order(32)
314+
void resolvesSeparateTempDirsWhenAnnotationIsUsedOnConstructorWithTestInstancePerMethod() {
315+
var results = executeTestsForClass(SeparateTempDirsConstructorInjectionPerMethodTestCase.class);
316+
317+
results.testEvents().assertStatistics(stats -> stats.started(2).failed(0).succeeded(2));
318+
assertThat(BaseConstructorInjectionTestCase.tempDirs.getFirst()).doesNotExist();
319+
assertThat(BaseConstructorInjectionTestCase.tempDirs.getLast()).doesNotExist();
320+
}
321+
299322
}
300323

301324
@Nested
@@ -370,26 +393,6 @@ void onlySupportsParametersOfTypePathAndFile() {
370393
// @formatter:on
371394
}
372395

373-
@Test
374-
@DisplayName("when @TempDir is used on constructor parameter")
375-
@Order(30)
376-
void doesNotSupportTempDirAnnotationOnConstructorParameter() {
377-
var results = executeTestsForClass(AnnotationOnConstructorParameterTestCase.class);
378-
379-
assertSingleFailedTest(results, ParameterResolutionException.class,
380-
"@TempDir is not supported on constructor parameters. Please use field injection instead.");
381-
}
382-
383-
@Test
384-
@DisplayName("when @TempDir is used on constructor parameter with @TestInstance(PER_CLASS)")
385-
@Order(31)
386-
void doesNotSupportTempDirAnnotationOnConstructorParameterWithTestInstancePerClass() {
387-
var results = executeTestsForClass(AnnotationOnConstructorParameterWithTestInstancePerClassTestCase.class);
388-
389-
assertSingleFailedContainer(results, ParameterResolutionException.class,
390-
"@TempDir is not supported on constructor parameters. Please use field injection instead.");
391-
}
392-
393396
@Test
394397
@DisplayName("when non-default @TempDir factory is set")
395398
@Order(32)
@@ -693,26 +696,75 @@ static void check(Path tempDir) {
693696

694697
}
695698

696-
static class AnnotationOnConstructorParameterTestCase {
699+
static class BaseConstructorInjectionTestCase {
697700

698-
AnnotationOnConstructorParameterTestCase(@SuppressWarnings("unused") @TempDir Path tempDir) {
699-
// never called
701+
static final Deque<Path> tempDirs = new LinkedList<>();
702+
703+
private final Path tempDir;
704+
705+
BaseConstructorInjectionTestCase(Path tempDir) {
706+
this.tempDir = tempDir;
700707
}
701708

702709
@Test
703-
void test() {
704-
// never called
710+
void test1(@TempDir Path tempDir, TestInfo testInfo) throws Exception {
711+
check(tempDir);
712+
writeFile(tempDir, testInfo);
713+
}
714+
715+
@Test
716+
void test2(TestInfo testInfo, @TempDir Path tempDir) throws Exception {
717+
check(tempDir);
718+
writeFile(tempDir, testInfo);
719+
}
720+
721+
@AfterEach
722+
void afterEach(@TempDir Path tempDir) {
723+
check(tempDir);
724+
}
725+
726+
void check(Path tempDir) {
727+
assertThat(tempDirs.getLast())//
728+
.isNotNull()//
729+
.isSameAs(tempDir)//
730+
.isSameAs(this.tempDir);
731+
assertTrue(Files.exists(tempDir));
705732
}
706733

707734
}
708735

736+
static class SeparateTempDirsConstructorInjectionPerMethodTestCase extends BaseConstructorInjectionTestCase {
737+
738+
SeparateTempDirsConstructorInjectionPerMethodTestCase(@TempDir Path tempDir) {
739+
super(tempDir);
740+
}
741+
742+
@BeforeEach
743+
void beforeEach(@TempDir Path tempDir) {
744+
for (Path dir : tempDirs) {
745+
assertThat(dir).doesNotExist();
746+
}
747+
assertThat(tempDirs).doesNotContain(tempDir);
748+
tempDirs.add(tempDir);
749+
check(tempDir);
750+
}
751+
}
752+
709753
@TestInstance(PER_CLASS)
710-
static class AnnotationOnConstructorParameterWithTestInstancePerClassTestCase
711-
extends AnnotationOnConstructorParameterTestCase {
754+
static class SharedTempDirsConstructorInjectionPerClassTestCase extends BaseConstructorInjectionTestCase {
712755

713-
AnnotationOnConstructorParameterWithTestInstancePerClassTestCase(@TempDir Path tempDir) {
756+
SharedTempDirsConstructorInjectionPerClassTestCase(@TempDir Path tempDir) {
714757
super(tempDir);
715758
}
759+
760+
@BeforeEach
761+
void beforeEach(@TempDir Path tempDir) {
762+
for (Path dir : tempDirs) {
763+
assertThat(dir).isSameAs(tempDir).exists();
764+
}
765+
tempDirs.add(tempDir);
766+
check(tempDir);
767+
}
716768
}
717769

718770
static class NonDefaultFactoryTestCase {

0 commit comments

Comments
 (0)