Some tests need "resources", which need to be cleaned up when finished, and sometimes, many tests need to access these resources. Furthermore, you may be running your JUnit Jupiter tests in parallel, which makes sharing these resources flaky. For example, you might want to share a temporary directory. This can be a problem if the directory isn’t deleted after your tests, or your tests try to read and write files at the same time.

This extension separates parsing annotations, injecting new or shared resources, and registering them for getting cleaned up (which is needed for many kinds of resources) from actually creating and closing them (which is specific to each kind of resource).

Note

This article describes the general mechanisms shared by different resource extensions but not their specifics. Check the individual documentations for that:

The first part of this article describes how to use a resource with this extension. The second part describes how to integrate your own kind of resource with this mechanism. In both cases, the temporary directory extension will be used as an example, but what’s described here applies to other resources as well.

Using Resources

There are two different approaches to using a resource:

  • Creating a new one for a given test.

  • Sharing one between several tests.

Creating a New Resource

To create a new resource for a given test:

@Test
void test1(@New(TemporaryDirectory.class) Path tempDir) {
    // Test code goes here, e.g.,
    assertThat(tempDir).exists();
}

@Test
void test2(@New(TemporaryDirectory.class) Path tempDir) {
    // This temporary directory is different to the first one.
}

(TemporaryDirectory is a built-in resource for creating temporary directories.)

This will create a brand-new resource for each test, and each resource will be "closed" at the end of its associated test.

So in this case, a new temporary directory will be created for test1 and another one will be created for test2. The temporary directory for test1 will be deleted when the test is finished. Likewise, the temporary directory for test2 will also be deleted right after its test.

Creating a New Resource with Arguments

Some resources accept string arguments to control their behaviour.

For example, TemporaryDirectory may accept a String argument to set the prefix of the name of the temporary directory that is created:

@Test
void testWithArg(
        @New(value = TemporaryDirectory.class, arguments = "customPrefix")
        Path tempDir) {
    // Test code goes here, e.g.,
    Path rootTempDir = Paths.get(System.getProperty("java.io.tmpdir"));
    assertThat(rootTempDir.relativize(tempDir)).asString().startsWith("customPrefix");
}

Sharing a Resource

To create a resource that is shared by multiple tests:

@Test
void sharedResourceTest1(
        @Shared(factory = TemporaryDirectory.class, name = "sharedTempDir")
        Path sharedTempDir) {
    // Test code goes here, e.g.,
    assertThat(sharedTempDir).exists();
}

@Test
void sharedResourceTest2(
        @Shared(factory = TemporaryDirectory.class, name = "sharedTempDir")
        Path sharedTempDir) {
    // "sharedTempDir" is shared with the temporary directory of
    // the same name in test "sharedResourceTest1", so any created
    // subdirectories and files will be shared.
}

(TemporaryDirectory is a built-in resource for creating temporary directories.)

This will create a single resource instance that will be injected into all the tests. It will be "closed" when all the tests are finished. (See Scope of a Shared Resource for a caveat on this.)

So in this case, a single temporary directory will be shared across all tests, and it will only be deleted when all the tests have run.

Note

When sharing resources, you may want to force your tests to run in a certain order, so that e.g. files written to a temporary directory in one test can be read from another test. Use JUnit Jupiter’s Test Execution Order feature to do this.

Sharing Multiple Resources

The following code snippet shows an example of creating two shared resources.

In this case, tests firstSharedResource1 and firstSharedResource2 use the same temporary directory, and test secondSharedResource uses a different temporary directory.

@Test
void firstSharedResource1(
        @Shared(factory = TemporaryDirectory.class, name = "first")
        Path first) {
    // Test code working with first shared resource...
}

@Test
void firstSharedResource2(
        @Shared(factory = TemporaryDirectory.class, name = "first")
        Path first) {
    // Test code working with first shared resource...
}

@Test
void secondSharedResource(
        @Shared(factory = TemporaryDirectory.class, name = "second")
        Path second) {
    // This shared resource is different!
}

Scope of a Shared Resource

By default, a shared resource will be closed when the test file using it has finished. This means that if a resource is shared across two or more test files, it will be closed and re-created for each test file.

If this behaviour isn’t what you want, you can change the "scope" of the resource to "global", which will keep the resource around until all test files have finished:

class FirstTest {

    @Test
    void test(
            @Shared(
                    factory = TemporaryDirectory.class,
                    name = "globalTempDir",
                    scope = Shared.Scope.GLOBAL)
            Path tempDir) {
        // Test code using the global shared resource...
    }

}
class SecondTest {

    @Test
    void test(
            @Shared(
                    factory = TemporaryDirectory.class,
                    name = "globalTempDir",
                    scope = Shared.Scope.GLOBAL)
            Path tempDir) {
        // Test code using the global shared resource...
    }

}
Note

You are not limited to singletons. You can create as many global shared resources as you want, as long as they have different names.

Sharing Resources with Arguments

Note

We do not support creating shared resources with arguments. This is because if a test refers to a shared resource with the name "Foo" without arguments, then later another test refers to it with one argument, there is no reasonable way to fulfill that request. Furthermore, even if this was supported, the behavior would change if the first and second tests ever ran in opposite order, which is very likely when tests are configured to run in parallel.

Cleaning Up Resources

Resources will be cleaned up, meaning close will be called on them and they will be made eligible for garbage collection, when they are no longer needed:

  • for a @New parameter, this happens immediately after the test

  • for a @Shared parameter with scope SOURCE_FILE, this happens when all tests in that source file were executed

  • for a @Shared parameter with scope GLOBAL, this happens when the entire test suite was executed

Resources will be cached in-memory until then.

Integrating Resources

This extension allows you to integrate your own kind of resource with the mechanisms described above.

To do that, you need to implement Resource<T> and ResourceFactory<T>, where T is the type of resource you want to provide (e.g. Path for temporary directories). Then you can reference the factory type in the @New and @Shared annotations.

Creating Factories

This extension will create a single ResourceFactory, which hence needs a parameterless constructor. If there’s no such constructor, the extension will throw an exception.

Creating Resources

The factory’s create method gets called when:

  • a test with a @New-annotated parameter is about to run

  • a test with a @Shared-annotated parameter is about to run and no shared resource with the configured name could be found in the configured scope

The extension will then populate the parameter with the contents of the returned Resource.

@New parameters may have associated string arguments. These will be passed to the factory’s ResourceFactory::create method as a List<String> when a resource needs to be created. You have full control over what these arguments mean and how they are used and should document these details in your resource factory’s JavaDoc.

Closing Resources and Factories

You can opt-in to cleaning resources and their factories up by implementing close methods on both of these types. The method Resource::close will be called as described above in Cleaning Up Resources. ResourceFactory::close will be called when all tests are finished.

Overriding these close() methods is optional - they will do nothing by default.

Examples

These examples show how to create a resource called InMemoryDirectory for an in-memory filesystem using Jimfs.

Set up the Factory

This example shows how to create the ResourceFactory for your resources.

public final class InMemoryDirectory implements ResourceFactory<Path> {

    private static final AtomicInteger DIRECTORY_NAME = new AtomicInteger();

    // The resource factory we want to create resources with.
    // In this case, an in-memory filesystem.
    private final FileSystem fileSystem;

    // NOTE: The constructor must be parameter-less.
    public InMemoryDirectory() {
        this.fileSystem = Jimfs.newFileSystem(Configuration.unix());
    }

    @Override
    public Resource<Path> create(List<String> arguments) throws Exception {
        // ...
    }

}

Close the Factory

This example shows how to delete, tear down or otherwise "close" everything associated with the factory when this extension is ready to call the factory’s ResourceFactory.close() method.

public final class InMemoryDirectory implements ResourceFactory<Path> {

    // ...

    private final FileSystem fileSystem;

    // ...

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

}

Create a Resource

This example shows how to create an actual Resource from your factory, with the ResourceFactory.create() method.

public final class InMemoryDirectory implements ResourceFactory<Path> {

    // ...

    private final FileSystem fileSystem;

    // ...

    @Override
    public Resource<Path> create(List<String> arguments) throws Exception {
        // Create a new resource from the factory.
        // In this case, return a new directory from
        // the in-memory filesystem.

        Path newInMemoryDirectory = this.fileSystem.getPath("/" + DIRECTORY_NAME.getAndIncrement());
        Files.createDirectory(newInMemoryDirectory);

        return new Resource<Path>() {

            @Override
            public Path get() throws Exception {
                return newInMemoryDirectory;
            }

            // ...

        };
    }

}

Close a Resource

This example shows how to expand the previous resource to delete, tear down or otherwise "close" everything associated with it when this extension is ready to call the resource’s Resource.close() method.

public final class InMemoryDirectory implements ResourceFactory<Path> {

    // ...

    private final FileSystem fileSystem;

    // ...

    @Override
    public Resource<Path> create(List<String> arguments) throws Exception {

        Path newInMemoryDirectory = // ...
        // ...

        return new Resource<Path>() {

            // ...

            @Override
            public void close() throws Exception {
                Files.walkFileTree(newInMemoryDirectory, new SimpleFileVisitor<Path>() {

                    @Override
                    public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
                        Files.deleteIfExists(file);
                        return FileVisitResult.CONTINUE;
                    }

                    @Override
                    public FileVisitResult postVisitDirectory(Path dir, IOException exc) throws IOException {
                        Files.deleteIfExists(dir);
                        return FileVisitResult.CONTINUE;
                    }

                });
            }

        };
    }

}

Working with Arguments from @New

This example shows how to interpret the first argument to be the prefix of the name of the to-be-created in-memory directory resource.

public final class InMemoryDirectory implements ResourceFactory<Path> {

    // ...

    @Override
    public Resource<Path> create(List<String> arguments) throws Exception {
        String directoryPrefix = (arguments.size() == 1) ? arguments.get(0) : "";
        Path newInMemoryDirectory = this.fileSystem.getPath("/" + directoryPrefix + DIRECTORY_NAME.getAndIncrement());
        Files.createDirectory(newInMemoryDirectory);

        return new Resource<Path>() {

            @Override
            public Path get() throws Exception {
                return newInMemoryDirectory;
            }

            // ...

        };

    }
}

Putting It All Together

This example shows everything from the previous few sections in one big code snippet.

public final class InMemoryDirectory implements ResourceFactory<Path> {

    private static final AtomicInteger DIRECTORY_NAME = new AtomicInteger();

    private final FileSystem fileSystem;

    public InMemoryDirectory() {
        this.fileSystem = Jimfs.newFileSystem(Configuration.unix());
    }

    @Override
    public Resource<Path> create(List<String> arguments) throws Exception {
        String directoryPrefix = (arguments.size() == 1) ? arguments.get(0) : "";

        Path newInMemoryDirectory = this.fileSystem.getPath("/" + directoryPrefix + DIRECTORY_NAME.getAndIncrement());
        Files.createDirectory(newInMemoryDirectory);

        return new Resource<>() {

            @Override
            public Path get() {
                return newInMemoryDirectory;
            }

            @Override
            public void close() throws Exception {
                Files.walkFileTree(newInMemoryDirectory, new SimpleFileVisitor<>() {

                    @Override
                    public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
                        Files.deleteIfExists(file);
                        return FileVisitResult.CONTINUE;
                    }

                    @Override
                    public FileVisitResult postVisitDirectory(Path dir, IOException exc) throws IOException {
                        Files.deleteIfExists(dir);
                        return FileVisitResult.CONTINUE;
                    }

                });
            }

        };
    }

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

}

Thread-Safety

This extension is safe to use during parallel test execution.

Tests, test constructors, and lifecycle methods with @New resources will run in parallel.

Tests, test constructors, and lifecycle methods with @Shared resources will be forced to run sequentially, even if parallel execution has been enabled. This is because resources may be mutable, and if the tests were allowed to run in parallel, they could mutate the resources in a non-deterministic way. Temporary directories are a good example of this, as tests can create new subdirectories and files inside them.

Caution

Be careful not to save resources in fields from any test method, including @BeforeAll and @BeforeEach methods, as this extension cannot guarantee that such resources are read or mutated sequentially. Instead, use @Shared to reuse resources in multiple tests.