Built-in Extensions

This version is still in development and not considered stable yet.
For the latest stable release, please see JUnit 6.0.2!

While the JUnit team encourages reusable extensions to be packaged and maintained in separate libraries, JUnit Jupiter includes a few user-facing extension implementations that are considered so generally useful that users shouldn’t have to add another dependency.

The @TempDir Extension

The TempDirectory extension is used to create and clean up a temporary directory for an individual test or all tests in a test class. It is registered by default. To use it, annotate a non-final, unassigned field of type java.nio.file.Path or java.io.File with @TempDir or add a parameter of type java.nio.file.Path or java.io.File annotated with @TempDir to a test class constructor, lifecycle method, or test method.

For example, the following test declares a parameter annotated with @TempDir for a single test method, creates and writes to a file in the temporary directory, and checks its content.

A test method that requires a temporary directory
@Test
void writeItemsToFile(@TempDir Path tempDir) throws IOException {
	Path file = tempDir.resolve("test.txt");

	new ListWriter(file).write("a", "b", "c");

	assertEquals(List.of("a,b,c"), Files.readAllLines(file));
}

You can inject multiple temporary directories by specifying multiple annotated parameters.

A test method that requires multiple temporary directories
@Test
void copyFileFromSourceToTarget(@TempDir Path source, @TempDir Path target) throws IOException {
	Path sourceFile = source.resolve("test.txt");
	new ListWriter(sourceFile).write("a", "b", "c");

	Path targetFile = Files.copy(sourceFile, target.resolve("test.txt"));

	assertNotEquals(sourceFile, targetFile);
	assertEquals(List.of("a,b,c"), Files.readAllLines(targetFile));
}

The following example stores a shared temporary directory in a static field. This allows the same sharedTempDir to be used in all lifecycle methods and test methods of the test class. For better isolation, you should use an instance field or constructor injection so that each test method uses a separate directory.

A test class that shares a temporary directory across test methods
class SharedTempDirectoryDemo {

	@TempDir
	static Path sharedTempDir;

	@Test
	void writeItemsToFile() throws IOException {
		Path file = sharedTempDir.resolve("test.txt");

		new ListWriter(file).write("a", "b", "c");

		assertEquals(List.of("a,b,c"), Files.readAllLines(file));
	}

	@Test
	void anotherTestThatUsesTheSameTempDir() {
		// use sharedTempDir
	}

}

The @TempDir annotation has an optional cleanup attribute that can be set to either NEVER, ON_SUCCESS, or ALWAYS. If the cleanup mode is set to NEVER, the temporary directory will not be deleted after the test completes. If it is set to ON_SUCCESS, the temporary directory will only be deleted after the test if the test completed successfully.

The default cleanup mode is ALWAYS. You can use the junit.jupiter.tempdir.cleanup.mode.default configuration parameter to override this default.

A test class with a temporary directory that doesn’t get cleaned up
class CleanupModeDemo {

	@Test
	void fileTest(@TempDir(cleanup = ON_SUCCESS) Path tempDir) {
		// perform test
	}

}

@TempDir supports the programmatic creation of temporary directories via the optional factory attribute. This is typically used to gain control over the temporary directory creation, like defining the parent directory or the file system that should be used.

Factories can be created by implementing TempDirFactory. Implementations must provide a no-args constructor and should not make any assumptions regarding when and how many times they are instantiated, but they can assume that their createTempDirectory(…​) and close() methods will both be called once per instance, in this order, and from the same thread.

The default implementation available in Jupiter delegates directory creation to java.nio.file.Files::createTempDirectory which uses the default file system and the system’s temporary directory as the parent directory. It passes junit- as the prefix string of the generated directory name to help identify it as a created by JUnit.

The following example defines a factory that uses the test name as the directory name prefix instead of the junit constant value.

A test class with a temporary directory having the test name as the directory name prefix
class TempDirFactoryDemo {

	@Test
	void factoryTest(@TempDir(factory = Factory.class) Path tempDir) {
		assertTrue(tempDir.getFileName().toString().startsWith("factoryTest"));
	}

	static class Factory implements TempDirFactory {

		@Override
		public Path createTempDirectory(AnnotatedElementContext elementContext, ExtensionContext extensionContext)
				throws IOException {
			return Files.createTempDirectory(extensionContext.getRequiredTestMethod().getName());
		}

	}

}

It is also possible to use an in-memory file system like Jimfs for the creation of the temporary directory. The following example demonstrates how to achieve that.

A test class with a temporary directory created with the Jimfs in-memory file system
class InMemoryTempDirDemo {

	@Test
	void test(@TempDir(factory = JimfsTempDirFactory.class) Path tempDir) {
		// perform test
	}

	static class JimfsTempDirFactory implements TempDirFactory {

		private final FileSystem fileSystem = Jimfs.newFileSystem(Configuration.unix());

		@Override
		public Path createTempDirectory(AnnotatedElementContext elementContext, ExtensionContext extensionContext)
				throws IOException {
			return Files.createTempDirectory(fileSystem.getPath("/"), "junit-");
		}

		@Override
		public void close() throws IOException {
			fileSystem.close();
		}

	}

}

@TempDir can also be used as a meta-annotation to reduce repetition. The following code listing shows how to create a custom @JimfsTempDir annotation that can be used as a drop-in replacement for @TempDir(factory = JimfsTempDirFactory.class).

A custom annotation meta-annotated with @TempDir
@Target({ ElementType.ANNOTATION_TYPE, ElementType.FIELD, ElementType.PARAMETER })
@Retention(RetentionPolicy.RUNTIME)
@TempDir(factory = JimfsTempDirFactory.class)
@interface JimfsTempDir {
}

The following example demonstrates how to use the custom @JimfsTempDir annotation.

A test class using the custom annotation
class JimfsTempDirAnnotationDemo {

	@Test
	void test(@JimfsTempDir Path tempDir) {
		// perform test
	}

}

Meta-annotations or additional annotations on the field or parameter the TempDir annotation is declared on might expose additional attributes to configure the factory. Such annotations and related attributes can be accessed via the AnnotatedElementContext parameter of the createTempDirectory(…​) method.

You can use the junit.jupiter.tempdir.factory.default configuration parameter to specify the fully qualified class name of the TempDirFactory you would like to use by default. Just like for factories configured via the factory attribute of the @TempDir annotation, the supplied class has to implement the TempDirFactory interface. The default factory will be used for all @TempDir annotations unless the factory attribute of the annotation specifies a different factory.

In summary, the factory for a temporary directory is determined according to the following precedence rules:

  1. The factory attribute of the @TempDir annotation, if present

  2. The default TempDirFactory configured via the configuration parameter, if present

  3. Otherwise, org.junit.jupiter.api.io.TempDirFactory$Standard will be used.

The @AutoClose Extension

The AutoCloseExtension automatically closes resources associated with fields. It is registered by default. To use it, annotate a field in a test class with @AutoClose.

@AutoClose fields may be either static or non-static. If the value of an @AutoClose field is null when it is evaluated the field will be ignored, but a warning message will be logged to inform you.

By default, @AutoClose expects the value of the annotated field to implement a close() method that will be invoked to close the resource. However, developers can customize the name of the close method via the value attribute. For example, @AutoClose("shutdown") instructs JUnit to look for a shutdown() method to close the resource.

@AutoClose fields are inherited from superclasses. Furthermore, @AutoClose fields from subclasses will be closed before @AutoClose fields in superclasses.

When multiple @AutoClose fields exist within a given test class, the order in which the resources are closed depends on an algorithm that is deterministic but intentionally nonobvious. This ensures that subsequent runs of a test suite close resources in the same order, thereby allowing for repeatable builds.

The AutoCloseExtension implements the AfterAllCallback and TestInstancePreDestroyCallback extension APIs. Consequently, a static @AutoClose field will be closed after all tests in the current test class have completed, effectively after @AfterAll methods have executed for the test class. A non-static @AutoClose field will be closed before the current test class instance is destroyed. Specifically, if the test class is configured with @TestInstance(Lifecycle.PER_METHOD) semantics, a non-static @AutoClose field will be closed after the execution of each test method, test factory method, or test template method. However, if the test class is configured with @TestInstance(Lifecycle.PER_CLASS) semantics, a non-static @AutoClose field will not be closed until the current test class instance is no longer needed, which means after @AfterAll methods and after all static @AutoClose fields have been closed.

The following example demonstrates how to annotate an instance field with @AutoClose so that the resource is automatically closed after test execution. In this example, we assume that the default @TestInstance(Lifecycle.PER_METHOD) semantics apply.

A test class using @AutoClose to close a resource
class AutoCloseDemo {

	@AutoClose (1)
	WebClient webClient = new WebClient(); (2)

	String serverUrl = // specify server URL ...

	@Test
	void getProductList() {
		// Use WebClient to connect to web server and verify response
		assertEquals(200, webClient.get(serverUrl + "/products").getResponseStatus());
	}

}
1 Annotate an instance field with @AutoClose.
2 WebClient implements java.lang.AutoCloseable which defines a close() method that will be invoked after each @Test method.

The @DefaultLocale and @DefaultTimeZone Extensions

The @DefaultLocale and @DefaultTimeZone annotations can be used to change the values returned from Locale.getDefault() and TimeZone.getDefault(), respectively, which are often used implicitly when no specific locale or time zone is chosen. Both annotations work on the test class level and on the test method level, and are inherited from higher-level containers. After the annotated element has been executed, the initial default value is restored.

@DefaultLocale

The default Locale can be specified using an IETF BCP 47 language tag string.

@Test
@DefaultLocale("zh-Hant-TW")
void test_with_language() {
	assertThat(Locale.getDefault()).isEqualTo(Locale.forLanguageTag("zh-Hant-TW"));
}

Alternatively, the default Locale can be created using the following attributes from which a Locale.Builder can create an instance:

  • language

  • language and country

  • language, country, and variant

The variant needs to be a string which follows the IETF BCP 47 / RFC 5646 syntax.
@Test
@DefaultLocale(language = "en")
void test_with_language_only() {
	assertThat(Locale.getDefault()).isEqualTo(new Locale.Builder().setLanguage("en").build());
}

@Test
@DefaultLocale(language = "en", country = "EN")
void test_with_language_and_country() {
	assertThat(Locale.getDefault()).isEqualTo(new Locale.Builder().setLanguage("en").setRegion("EN").build());
}

@Test
@DefaultLocale(language = "ja", country = "JP", variant = "japanese")
void test_with_language_and_country_and_vairant() {
	assertThat(Locale.getDefault()).isEqualTo(
		new Locale.Builder().setLanguage("ja").setRegion("JP").setVariant("japanese").build());
}

Mixing language tag configuration (via the annotation’s value attribute) and attribute-based configuration will cause an exception to be thrown. Furthermore, a variant can only be specified if country is also specified. Otherwise, an exception will be thrown.

Method-level @DefaultLocale configuration overrides class-level configuration.

@DefaultLocale(language = "fr")
class MyLocaleTests {

	@Test
	void test_with_class_level_configuration() {
		assertThat(Locale.getDefault()).isEqualTo(new Locale.Builder().setLanguage("fr").build());
	}

	@Test
	@DefaultLocale(language = "en")
	void test_with_method_level_configuration() {
		assertThat(Locale.getDefault()).isEqualTo(new Locale.Builder().setLanguage("en").build());
	}

}
With class-level configuration, the specified locale is set before and reset after each individual test in the annotated class.

If your use case is not covered, you can implement the LocaleProvider interface.

@Test
@DefaultLocale(localeProvider = EnglishProvider.class)
void test_with_locale_provider() {
	assertThat(Locale.getDefault()).isEqualTo(new Locale.Builder().setLanguage("en").build());
}

static class EnglishProvider implements LocaleProvider {
	@Override
	public Locale get() {
		return Locale.ENGLISH;
	}
}
The provider implementation must have a no-args (or default) constructor.

@DefaultTimeZone

The default TimeZone is specified according to the TimeZone.getTimeZone(String) method.

@Test
@DefaultTimeZone("CET")
void test_with_short_zone_id() {
	assertThat(TimeZone.getDefault()).isEqualTo(TimeZone.getTimeZone("CET"));
}

@Test
@DefaultTimeZone("Africa/Juba")
void test_with_long_zone_id() {
	assertThat(TimeZone.getDefault()).isEqualTo(TimeZone.getTimeZone("Africa/Juba"));
}

Method-level @DefaultTimeZone configuration overrides class-level configuration.

@DefaultTimeZone("CET")
class MyTimeZoneTests {

	@Test
	void test_with_class_level_configuration() {
		assertThat(TimeZone.getDefault()).isEqualTo(TimeZone.getTimeZone("CET"));
	}

	@Test
	@DefaultTimeZone("Africa/Juba")
	void test_with_method_level_configuration() {
		assertThat(TimeZone.getDefault()).isEqualTo(TimeZone.getTimeZone("Africa/Juba"));
	}

}
With class-level configuration, the specified time zone is set before and reset after each individual test in the annotated class.

If your use case is not covered, you can implement the TimeZoneProvider interface.

@Test
@DefaultTimeZone(timeZoneProvider = UtcTimeZoneProvider.class)
void test_with_time_zone_provider() {
	assertThat(TimeZone.getDefault()).isEqualTo(TimeZone.getTimeZone("UTC"));
}

static class UtcTimeZoneProvider implements TimeZoneProvider {
	@Override
	public TimeZone get() {
		return TimeZone.getTimeZone(ZoneOffset.UTC);
	}
}
The provider implementation must have a no-args (or default) constructor.

Thread Safety

Since the default locale and time zone are global state, reading and writing them during parallel test execution can lead to unpredictable results and flaky tests. The @DefaultLocale and @DefaultTimeZone extensions are prepared for that and tests annotated with them will never execute in parallel (thanks to @ResourceLock) to guarantee correct test results.

However, this does not cover all possible cases. Tested code that reads or writes the default locale or time zone independently of the extensions can still run in parallel and may thus behave erratically when, for example, such code unexpectedly reads a locale set by the extension in another thread. Consequently, tests that cover code that reads or writes the default locale or time zone need to be annotated with one of the following respective annotations.

Tests annotated with one of the above annotations will never execute in parallel with tests annotated with @DefaultLocale or @DefaultTimeZone.

The System Properties Extension

The system properties extension supports a set of annotations that work together to clear, set, and restore JVM system properties.

@ClearSystemProperty and @SetSystemProperty

The @ClearSystemProperty and @SetSystemProperty annotations can be used to clear and set, respectively, the values of JVM system properties for test execution. Both annotations work on the test method and class level and are repeatable, combinable, and inherited from higher-level containers. After the annotated method has been executed, the properties configured in the annotation will be restored to their original value or the value of the higher-level container, or will be cleared if they did not previously have a value. Other system properties that are changed during the test are not restored (unless restoration is explicitly enabled via @RestoreSystemProperties).

For example, clearing a system property for test execution can be done as follows.

@Test
@ClearSystemProperty(key = "some property")
void testClearingProperty() {
	assertThat(System.getProperty("some property")).isNull();
}

The following demonstrates how to set a system property for test execution.

@Test
@SetSystemProperty(key = "some property", value = "new value")
void testSettingProperty() {
	assertThat(System.getProperty("some property")).isEqualTo("new value");
}

As mentioned before, both annotations are repeatable, and they can also be combined.

@Test
@ClearSystemProperty(key = "1st property")
@ClearSystemProperty(key = "2nd property")
@SetSystemProperty(key = "3rd property", value = "new value")
void testClearingAndSettingProperty() {
	assertThat(System.getProperty("1st property")).isNull();
	assertThat(System.getProperty("2nd property")).isNull();
	assertThat(System.getProperty("3rd property")).isEqualTo("new value");
}

Note that class-level configuration is overridden by method-level configuration.

@ClearSystemProperty(key = "some property")
class MySystemPropertyTest {

	@Test
	@SetSystemProperty(key = "some property", value = "new value")
	void clearedAtClasslevel() {
		assertThat(System.getProperty("some property")).isEqualTo("new value");
	}

}

Method-level configuration is visible in both @BeforeEach methods and @AfterEach methods (see user code and extension code execution order).

With class-level configuration, the specified system properties are cleared or set before and reset after each individual test in the annotated class.

@RestoreSystemProperties

The @RestoreSystemProperties annotation can be used to restore changes to system properties made directly in the test or in the code being tested. Although @ClearSystemProperty and @SetSystemProperty clear or set properties and values that are statically declared, they do not allow property values to be calculated dynamically. Thus, there are times you may want to directly set properties in your test code. @RestoreSystemProperties can be placed on test methods or test classes and will completely restore all system properties to their original state after the test or test class has finished.

During the execution of the annotated scope, the JVM system properties are set to a clone of the original Properties object. However, the clone does not include the defaults from the original.

Consequently, the extension will perform a best effort attempt to detect default properties and fail if any were detected. For classes that extend Properties it is assumed that clone() is implemented with sufficient fidelity.

In the following example, @RestoreSystemProperties is used on a test method, ensuring any changes made in that method are restored.

@ParameterizedTest
@ValueSource(strings = { "foo", "bar" })
@RestoreSystemProperties
void parameterizedTest(String value) {
	System.setProperty("some parameterized property", value);
	System.setProperty("some other dynamic property", "my code calculates somehow");
}

When @RestoreSystemProperties is used on a test class, any changes to system properties during the entire lifecycle of the test class, including test methods, @BeforeAll, @BeforeEach, and 'after' methods, are restored after the lifecycle of the test class is complete. In addition, the annotation is inherited by each test method just as if each one were annotated with @RestoreSystemProperties.

In the following example, both test methods see the system property changes made in @BeforeAll and @BeforeEach; however, the test methods are isolated from each other (isolatedTest2 does not see changes made in isolatedTest1). As shown in the second example below, the class-level @RestoreSystemProperties annotation ensures that system property changes made within the annotated class are completely restored after the class’s lifecycle, ensuring that changes are not visible to SomeOtherTestClass. Note that SomeOtherTestClass uses the @ReadsSystemProperty annotation, which ensures that JUnit does not schedule the class to run during any test known to modify system properties (see Thread Safety).

@RestoreSystemProperties
class MySystemPropertyRestoreTest {

	@BeforeAll
	void beforeAll() {
		System.setProperty("A", "A value");
	}

	@BeforeEach
	void beforeEach() {
		System.setProperty("B", "B value");
	}

	@Test
	void isolatedTest1() {
		System.setProperty("C", "C value");
	}

	@Test
	void isolatedTest2() {
		assertThat(System.getProperty("A")).isEqualTo("A value");
		assertThat(System.getProperty("B")).isEqualTo("B value");

		// Class-level @RestoreSystemProperties restores "C" to original state
		assertThat(System.getProperty("C")).isNull();
	}

}

Some other test class, running later:

@ReadsSystemProperty
class SomeOtherTestClass {

	@Test
	void isolatedTest() {
		assertThat(System.getProperty("A")).isNull();
		assertThat(System.getProperty("B")).isNull();
		assertThat(System.getProperty("C")).isNull();
	}

}

Combining @ClearSystemProperty, @SetSystemProperty, and @RestoreSystemProperties

The three system property annotations can be combined, which can be useful when some system properties are set dynamically in code and others are not. For instance, imagine you need to test an image generation utility that takes configuration from system properties. Basic configuration can be specified declaratively using the Clear and Set annotations, and the image size could be set programmatically.

@ParameterizedTest
@ValueSource(ints = { 100, 500, 1000 })
@RestoreSystemProperties
@SetSystemProperty(key = "DISABLE_CACHE", value = "TRUE")
@ClearSystemProperty(key = "COPYWRITE_OVERLAY_TEXT")
void imageGenerationTest(int imageSize) {
	System.setProperty("IMAGE_SIZE", String.valueOf(imageSize)); // Requires restore

	// Test your image generation utility with the current system properties
}

Using @RestoreSystemProperties is not necessary to restore system properties modified via @ClearSystemProperty or @SetSystemProperty since they both automatically restore the referenced properties. @RestoreSystemProperties is only needed if system properties are modified during a test in some way other than via the Clear and Set annotations.

Thread Safety

Since system properties are global state, reading and writing them during parallel execution can lead to unpredictable results and flaky tests. The system property extension is prepared for that and tests annotated with @ClearSystemProperty, @SetSystemProperty, or @RestoreSystemProperties will never execute in parallel (thanks to resource locks) to guarantee correct test results.

However, this does not cover all possible cases. Tested code that reads or writes system properties independently of the extension can still run in parallel to it and may thus behave erratically when, for example, it unexpectedly reads a property set by the extension in another thread. Tests that cover code that reads or writes system properties need to be annotated with the respective annotation:

Tests annotated in this way will never execute in parallel with tests annotated with @ClearSystemProperty, @SetSystemProperty, or @RestoreSystemProperties.