Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add extension API for reusable initialization of temporary directories #1786

Open
1 task
sijie123 opened this issue Feb 18, 2019 · 18 comments
Open
1 task

Comments

@sijie123
Copy link

sijie123 commented Feb 18, 2019

I have a bunch of tests that require me to initialise a temporary directory with some files, so that the methods in my tests can read and write to it.
Currently, the solution is to implement a BeforeAll/BeforeEach tag and initialise the temporary directory for each test class.
As this is a common routine, it would be more reuse friendly if we could write an extension that encapsulates this routine, and which only needs to receive an input of a Path to initialise a temporary directory with all the files.
I envision the extension to look something like

DirectoryInitExtension.java

public class DirectoryInitExtension implements BeforeEachCallback {

    private Path TEST_DATA_FOLDER;

    @TempDir
    public Path tempDir;

    public static DirectoryInitExtension initializeWith(Path folder) {
        return new DirectoryInitExtension(folder);
    }

    @Override
    public void beforeEach(ExtensionContext extensionContext) throws IOException {
        // Copy files from TEST_DATA_FOLDER into tempDir, then return this tempDir as part of the test context.
    }
}

MainTest.java

 @RegisterExtension
    public static DirectoryResetExtension tempDir = DirectoryResetExtension.initializeWith(TEST_DATA_FOLDER);

However, as I soon learnt (thanks to a fast response here), TempDirs are not supported in extensions.

I wonder if it is a possibility to enable the same capabilities of TempDirs in extensions.

Deliverables

  • Support TempDir in extensions? (Sorry, not too sure what to write here)
@sormuras
Copy link
Member

Extensions should not depend on other extension - not even on those built-in extensions that Jupiter provides.

Why don't you just use Files.createTempDirectory(...) in beforeEach and delete that directory in a afterEach callback? The latter may profit from exposing the "delete directory tree recursively" helper by Jupiter in some way.

@sormuras
Copy link
Member

sormuras commented Feb 18, 2019

A generic resource supplier SPI (#1672) could help here, methinks. With such an extension in place, test authors could write:

@Test
void test4711(@New(TestDataFolder.class) Path path) { ... }

Will try to implement and publish a spike in JUnit Pioneer, soon. Meanwhile, if you're running your tests on Java 11, you may try https://github.com/sormuras/brahms#resource-manager-extension

@sbrannen
Copy link
Member

Extensions should not depend on other extension - not even on those built-in extensions that Jupiter provides.

That's typically true; however, there are times when interactions between extensions is desirable. For example, the SpringExtension has a public static ApplicationContext getApplicationContext(ExtensionContext) method that is used internally by the SpringExtension but intentionally public for use by other extensions.

Another option for inter-extension communication/collaboration is the ExtensionContext.Store. The Store was originally designed with such inter-extension collaboration in mind.

However, for the use case in this issue, allowing fields in an extension to be annotated with @TempDir is not something we would want to do, for many reasons but especially since extensions should typically not be stateful.

Why don't you just use Files.createTempDirectory(...) in beforeEach and delete that directory in a afterEach callback?

I agree that managing such a temporary directory within the custom extension itself is the more robust approach, especially since the lifecycle of the temporary directory should be managed by that extension and not by the built-in TempDirectory extension.

The latter may profit from exposing the "delete directory tree recursively" helper by Jupiter in some way.

Yes, we could consider publishing that in something like a new IoSupport utility class.

@marcphilipp
Copy link
Member

marcphilipp commented Mar 9, 2019

Related to #1604. Tentatively slated for 5.5 M2 for team discussion

@marcphilipp
Copy link
Member

Team Decision: Store Path in ExtensionContext.Store and make it available to other extensions via a well-known key/namespace combination.

@mkobit
Copy link
Contributor

mkobit commented Mar 15, 2019

@marcphilipp I was trying to think around how that would work for other extensions. Aren't ParameterResolver extensions one of the "last" callbacks that get invoked? For example, if a BeforeEachCallback gets invoked and tried to retrieve from the ExtensionContext.Store with the well-known namespace key, wouldn't it see that it isn't in the store? How does ordering/dependent extensions hook into it? Or, is TempDir a little bit special since it is built into Jupiter?

@sbrannen
Copy link
Member

@mkobit, those are valid questions.

If an extension attempts to look up the temp dir in the Store before it has been created (by the internal TempDirectory extension), it simply won't be there.

So we probably need to rethink this and provide an on-demand mechanism for acquiring access to the temp dir. One of the potential problems with that is determining what the scope/lifecycle of the temp dir should be.

@junit-team/junit-lambda, looks like we need to brainstorm a bit more on this topic.

@marcphilipp
Copy link
Member

Indeed.

@mkobit
Copy link
Contributor

mkobit commented Mar 19, 2019

I've been trying to think of some possible ways for clients to use this, as it seems related to my request at #1802.

Maybe there could be a notion of "extending the Extension.Store" so that other extensions that request values out of the store can be produced on demand? Everything I think of keeps seeming like the eventuality of dependency injection, which could be a lot more complexity then is desired.

sijie123 added a commit to sijie123/CS2103-DeadlineManager that referenced this issue Mar 28, 2019
contents

Existing tests requiring TemporaryFolders initialise them
independently. Many classes, such as JsonAddressBookStorageTest and
JsonUserPrefsStorageTest, implement custom methods such as
addToTestDataPathIfNotNull(String) method, to dynamically include
resources in temporary directories.

This causes code duplication. Based on the DRY principle, these
duplicated methods can be refactored with the use of a helper class to
manage all directory initialisations.

Additionally, as we migrate to JUnit 5, TemporaryFolder rules are
replaced by TempDir extensions, and there is stronger support for
dynamic extensions that can initialise these temporary folders. Thus,
it is easier and neater to write an helper class extension that
manages the temporary folders.

Let's create a common DirectoryInitUtil class that manages the
initialisation of and file creation within temporary folders. This has
the benefit of ensuring that the temporary directory is properly reset
to a known, initialised state, at the start of every test method.

Design consideration: It would be ideal to implement this extension in
OOP style - abstracting away the idea of copying files and
initialising directories by encapsulating them into a
ManagedTempDirExtension class. However, this design is not possible
because the TempDir tag is not available in extensions [1] as of JUnit
5.4.0. It is slated for JUnit 5.5.0 M2 onwards [2], which is not
released at the time of this commit. Future developers can look into
converting DirectoryInitUtil into a Object Oriented Class.

Notes: this commit only introduces the DirectoryInitUtil extension
class. Extensions and the TempDir API can only be used by tests
using the JUnit 5 runner. Hence, we can only teach tests to use this
DirectoryInitUtil extension later after we migrate tests to use JUnit
5. This ensures that every commit in the meantime remains buildable.

[1] https://stackoverflow.com/questions/54647577/how-to-use-tempdir-in-extensions/54651578#54651578
[2] junit-team/junit5#1786
sijie123 added a commit to sijie123/CS2103-DeadlineManager that referenced this issue Apr 1, 2019
contents

Existing tests requiring TemporaryFolders initialise them
independently. Many classes, such as JsonAddressBookStorageTest and
JsonUserPrefsStorageTest, implement custom methods such as
addToTestDataPathIfNotNull(String) method, to dynamically include
resources in temporary directories.

This causes code duplication. Based on the DRY principle, these
duplicated methods can be refactored with the use of a helper class to
manage all directory initialisations.

Additionally, as we migrate to JUnit 5, TemporaryFolder rules are
replaced by TempDir extensions, and there is stronger support for
dynamic extensions that can initialise these temporary folders. Thus,
it is easier and neater to write an helper class extension that
manages the temporary folders.

Let's create a common DirectoryInitUtil class that manages the
initialisation of and file creation within temporary folders. This has
the benefit of ensuring that the temporary directory is properly reset
to a known, initialised state, at the start of every test method.

Design consideration: It would be ideal to implement this extension in
OOP style - abstracting away the idea of copying files and
initialising directories by encapsulating them into a
ManagedTempDirExtension class. However, this design is not possible
because the TempDir tag is not available in extensions [1] as of JUnit
5.4.0. It is slated for JUnit 5.5.0 M2 onwards [2], which is not
released at the time of this commit. Future developers can look into
converting DirectoryInitUtil into a Object Oriented Class.

Notes: this commit only introduces the DirectoryInitUtil extension
class. Extensions and the TempDir API can only be used by tests
using the JUnit 5 runner. Hence, we can only teach tests to use this
DirectoryInitUtil extension later after we migrate tests to use JUnit
5. This ensures that every commit in the meantime remains buildable.

[1] https://stackoverflow.com/questions/54647577/how-to-use-tempdir-in-extensions/54651578#54651578
[2] junit-team/junit5#1786
sijie123 added a commit to sijie123/CS2103-DeadlineManager that referenced this issue Apr 1, 2019
…ntents

Existing tests requiring TemporaryFolders initialise them
independently. Many classes, such as JsonAddressBookStorageTest and
JsonUserPrefsStorageTest, implement custom methods such as
addToTestDataPathIfNotNull(String) method, to dynamically include
resources in temporary directories.

This causes code duplication. Based on the DRY principle, these
duplicated methods can be refactored with the use of a helper class to
manage all directory initialisations.

Additionally, as we migrate to JUnit 5, TemporaryFolder rules are
replaced by TempDir extensions, and there is stronger support for
dynamic extensions that can initialise these temporary folders. Thus,
it is easier and neater to write an helper class extension that
manages the temporary folders.

Let's create a common DirectoryInitUtil class that manages the
initialisation of and file creation within temporary folders. This has
the benefit of ensuring that the temporary directory is properly reset
to a known, initialised state, at the start of every test method.

Design consideration: It would be ideal to implement this extension in
OOP style - abstracting away the idea of copying files and
initialising directories by encapsulating them into a
ManagedTempDirExtension class. However, this design is not possible
because the TempDir tag is not available in extensions [1] as of JUnit
5.4.0. It is slated for JUnit 5.5.0 M2 onwards [2], which is not
released at the time of this commit. Future developers can look into
converting DirectoryInitUtil into a Object Oriented Class.

Notes: this commit only introduces the DirectoryInitUtil extension
class. Extensions and the TempDir API can only be used by tests
using the JUnit 5 runner. Hence, we can only teach tests to use this
DirectoryInitUtil extension later after we migrate tests to use JUnit
5. This ensures that every commit in the meantime remains buildable.

[1] https://stackoverflow.com/questions/54647577/how-to-use-tempdir-in-extensions/54651578#54651578
[2] junit-team/junit5#1786
sijie123 added a commit to sijie123/CS2103-DeadlineManager that referenced this issue Apr 13, 2019
…ntents

Existing tests requiring TemporaryFolders initialise them
independently. Many classes, such as JsonAddressBookStorageTest and
JsonUserPrefsStorageTest, implement custom methods such as
addToTestDataPathIfNotNull(String) method, to dynamically include
resources in temporary directories.

This causes code duplication. Based on the DRY principle, these
duplicated methods can be refactored with the use of a helper class to
manage all directory initialisations.

Additionally, as we migrate to JUnit 5, TemporaryFolder rules are
replaced by TempDir extensions, and there is stronger support for
dynamic extensions that can initialise these temporary folders. Thus,
it is easier and neater to write an helper class extension that
manages the temporary folders.

Let's create a common DirectoryInitUtil class that manages the
initialisation of and file creation within temporary folders. This has
the benefit of ensuring that the temporary directory is properly reset
to a known, initialised state, at the start of every test method.

Design consideration: It would be ideal to implement this extension in
OOP style - abstracting away the idea of copying files and
initialising directories by encapsulating them into a
ManagedTempDirExtension class. However, this design is not possible
because the TempDir tag is not available in extensions [1] as of JUnit
5.4.0. It is slated for JUnit 5.5.0 M2 onwards [2], which is not
released at the time of this commit. Future developers can look into
converting DirectoryInitUtil into a Object Oriented Class.

Notes: this commit only introduces the DirectoryInitUtil extension
class. Extensions and the TempDir API can only be used by tests
using the JUnit 5 runner. Hence, we can only teach tests to use this
DirectoryInitUtil extension later after we migrate tests to use JUnit
5. This ensures that every commit in the meantime remains buildable.

[1] https://stackoverflow.com/questions/54647577/how-to-use-tempdir-in-extensions/54651578#54651578
[2] junit-team/junit5#1786
sijie123 added a commit to sijie123/CS2103-DeadlineManager that referenced this issue Apr 13, 2019
…ntents

Existing tests requiring TemporaryFolders initialise them
independently. Many classes, such as JsonAddressBookStorageTest and
JsonUserPrefsStorageTest, implement custom methods such as
addToTestDataPathIfNotNull(String) method, to dynamically include
resources in temporary directories.

This causes code duplication. Based on the DRY principle, these
duplicated methods can be refactored with the use of a helper class to
manage all directory initialisations.

Additionally, as we migrate to JUnit 5, TemporaryFolder rules are
replaced by TempDir extensions, and there is stronger support for
dynamic extensions that can initialise these temporary folders. Thus,
it is easier and neater to write an helper class extension that
manages the temporary folders.

Let's create a common DirectoryInitUtil class that manages the
initialisation of and file creation within temporary folders. This has
the benefit of ensuring that the temporary directory is properly reset
to a known, initialised state, at the start of every test method.

Design consideration: It would be ideal to implement this extension in
OOP style - abstracting away the idea of copying files and
initialising directories by encapsulating them into a
ManagedTempDirExtension class. However, this design is not possible
because the TempDir tag is not available in extensions [1] as of JUnit
5.4.0. It is slated for JUnit 5.5.0 M2 onwards [2], which is not
released at the time of this commit. Future developers can look into
converting DirectoryInitUtil into a Object Oriented Class.

Notes: this commit only introduces the DirectoryInitUtil extension
class. Extensions and the TempDir API can only be used by tests
using the JUnit 5 runner. Hence, we can only teach tests to use this
DirectoryInitUtil extension later after we migrate tests to use JUnit
5. This ensures that every commit in the meantime remains buildable.

[1] https://stackoverflow.com/questions/54647577/how-to-use-tempdir-in-extensions/54651578#54651578
[2] junit-team/junit5#1786
sijie123 added a commit to sijie123/CS2103-DeadlineManager that referenced this issue Apr 13, 2019
…ntents

Existing tests requiring TemporaryFolders initialise them
independently. Many classes, such as JsonAddressBookStorageTest and
JsonUserPrefsStorageTest, implement custom methods such as
addToTestDataPathIfNotNull(String) method, to dynamically include
resources in temporary directories.

This causes code duplication. Based on the DRY principle, these
duplicated methods can be refactored with the use of a helper class to
manage all directory initialisations.

Additionally, as we migrate to JUnit 5, TemporaryFolder rules are
replaced by TempDir extensions, and there is stronger support for
dynamic extensions that can initialise these temporary folders. Thus,
it is easier and neater to write an helper class extension that
manages the temporary folders.

Let's create a common DirectoryInitUtil class that manages the
initialisation of and file creation within temporary folders. This has
the benefit of ensuring that the temporary directory is properly reset
to a known, initialised state, at the start of every test method.

Design consideration: It would be ideal to implement this extension in
OOP style - abstracting away the idea of copying files and
initialising directories by encapsulating them into a
ManagedTempDirExtension class. However, this design is not possible
because the TempDir tag is not available in extensions [1] as of JUnit
5.4.0. It is slated for JUnit 5.5.0 M2 onwards [2], which is not
released at the time of this commit. Future developers can look into
converting DirectoryInitUtil into a Object Oriented Class.

Notes: this commit only introduces the DirectoryInitUtil extension
class. Extensions and the TempDir API can only be used by tests
using the JUnit 5 runner. Hence, we can only teach tests to use this
DirectoryInitUtil extension later after we migrate tests to use JUnit
5. This ensures that every commit in the meantime remains buildable.

[1] https://stackoverflow.com/questions/54647577/how-to-use-tempdir-in-extensions/54651578#54651578
[2] junit-team/junit5#1786
sijie123 added a commit to sijie123/CS2103-DeadlineManager that referenced this issue Apr 13, 2019
…ntents

Existing tests requiring TemporaryFolders initialise them
independently. Many classes, such as JsonAddressBookStorageTest and
JsonUserPrefsStorageTest, implement custom methods such as
addToTestDataPathIfNotNull(String) method, to dynamically include
resources in temporary directories.

This causes code duplication. Based on the DRY principle, these
duplicated methods can be refactored with the use of a helper class to
manage all directory initialisations.

Additionally, as we migrate to JUnit 5, TemporaryFolder rules are
replaced by TempDir extensions, and there is stronger support for
dynamic extensions that can initialise these temporary folders. Thus,
it is easier and neater to write an helper class extension that
manages the temporary folders.

Let's create a common DirectoryInitUtil class that manages the
initialisation of and file creation within temporary folders. This has
the benefit of ensuring that the temporary directory is properly reset
to a known, initialised state, at the start of every test method.

Design consideration: It would be ideal to implement this extension in
OOP style - abstracting away the idea of copying files and
initialising directories by encapsulating them into a
ManagedTempDirExtension class. However, this design is not possible
because the TempDir tag is not available in extensions [1] as of JUnit
5.4.0. It is slated for JUnit 5.5.0 M2 onwards [2], which is not
released at the time of this commit. Future developers can look into
converting DirectoryInitUtil into a Object Oriented Class.

Notes: this commit only introduces the DirectoryInitUtil extension
class. Extensions and the TempDir API can only be used by tests
using the JUnit 5 runner. Hence, we can only teach tests to use this
DirectoryInitUtil extension later after we migrate tests to use JUnit
5. This ensures that every commit in the meantime remains buildable.

[1] https://stackoverflow.com/questions/54647577/how-to-use-tempdir-in-extensions/54651578#54651578
[2] junit-team/junit5#1786
sijie123 added a commit to sijie123/CS2103-DeadlineManager that referenced this issue Apr 13, 2019
…ntents

Existing tests requiring TemporaryFolders initialise them
independently. Many classes, such as JsonAddressBookStorageTest and
JsonUserPrefsStorageTest, implement custom methods such as
addToTestDataPathIfNotNull(String) method, to dynamically include
resources in temporary directories.

This causes code duplication. Based on the DRY principle, these
duplicated methods can be refactored with the use of a helper class to
manage all directory initialisations.

Additionally, as we migrate to JUnit 5, TemporaryFolder rules are
replaced by TempDir extensions, and there is stronger support for
dynamic extensions that can initialise these temporary folders. Thus,
it is easier and neater to write an helper class extension that
manages the temporary folders.

Let's create a common DirectoryInitUtil class that manages the
initialisation of and file creation within temporary folders. This has
the benefit of ensuring that the temporary directory is properly reset
to a known, initialised state, at the start of every test method.

Design consideration: It would be ideal to implement this extension in
OOP style - abstracting away the idea of copying files and
initialising directories by encapsulating them into a
ManagedTempDirExtension class. However, this design is not possible
because the TempDir tag is not available in extensions [1] as of JUnit
5.4.0. It is slated for JUnit 5.5.0 M2 onwards [2], which is not
released at the time of this commit. Future developers can look into
converting DirectoryInitUtil into a Object Oriented Class.

Notes: this commit only introduces the DirectoryInitUtil extension
class. Extensions and the TempDir API can only be used by tests
using the JUnit 5 runner. Hence, we can only teach tests to use this
DirectoryInitUtil extension later after we migrate tests to use JUnit
5. This ensures that every commit in the meantime remains buildable.

[1] https://stackoverflow.com/questions/54647577/how-to-use-tempdir-in-extensions/54651578#54651578
[2] junit-team/junit5#1786
sijie123 added a commit to sijie123/CS2103-DeadlineManager that referenced this issue Apr 13, 2019
…ntents

Existing tests requiring TemporaryFolders initialise them
independently. Many classes, such as JsonAddressBookStorageTest and
JsonUserPrefsStorageTest, implement custom methods such as
addToTestDataPathIfNotNull(String) method, to dynamically include
resources in temporary directories.

This causes code duplication. Based on the DRY principle, these
duplicated methods can be refactored with the use of a helper class to
manage all directory initialisations.

Additionally, as we migrate to JUnit 5, TemporaryFolder rules are
replaced by TempDir extensions, and there is stronger support for
dynamic extensions that can initialise these temporary folders. Thus,
it is easier and neater to write an helper class extension that
manages the temporary folders.

Let's create a common DirectoryInitUtil class that manages the
initialisation of and file creation within temporary folders. This has
the benefit of ensuring that the temporary directory is properly reset
to a known, initialised state, at the start of every test method.

Design consideration: It would be ideal to implement this extension in
OOP style - abstracting away the idea of copying files and
initialising directories by encapsulating them into a
ManagedTempDirExtension class. However, this design is not possible
because the TempDir tag is not available in extensions [1] as of JUnit
5.4.0. It is slated for JUnit 5.5.0 M2 onwards [2], which is not
released at the time of this commit. Future developers can look into
converting DirectoryInitUtil into a Object Oriented Class.

Notes: this commit only introduces the DirectoryInitUtil extension
class. Extensions and the TempDir API can only be used by tests
using the JUnit 5 runner. Hence, we can only teach tests to use this
DirectoryInitUtil extension later after we migrate tests to use JUnit
5. This ensures that every commit in the meantime remains buildable.

[1] https://stackoverflow.com/questions/54647577/how-to-use-tempdir-in-extensions/54651578#54651578
[2] junit-team/junit5#1786
sijie123 added a commit to sijie123/CS2103-DeadlineManager that referenced this issue Apr 13, 2019
…ntents

Existing tests requiring TemporaryFolders initialise them
independently. Many classes, such as JsonAddressBookStorageTest and
JsonUserPrefsStorageTest, implement custom methods such as
addToTestDataPathIfNotNull(String) method, to dynamically include
resources in temporary directories.

This causes code duplication. Based on the DRY principle, these
duplicated methods can be refactored with the use of a helper class to
manage all directory initialisations.

Additionally, as we migrate to JUnit 5, TemporaryFolder rules are
replaced by TempDir extensions, and there is stronger support for
dynamic extensions that can initialise these temporary folders. Thus,
it is easier and neater to write an helper class extension that
manages the temporary folders.

Let's create a common DirectoryInitUtil class that manages the
initialisation of and file creation within temporary folders. This has
the benefit of ensuring that the temporary directory is properly reset
to a known, initialised state, at the start of every test method.

Design consideration: It would be ideal to implement this extension in
OOP style - abstracting away the idea of copying files and
initialising directories by encapsulating them into a
ManagedTempDirExtension class. However, this design is not possible
because the TempDir tag is not available in extensions [1] as of JUnit
5.4.0. It is slated for JUnit 5.5.0 M2 onwards [2], which is not
released at the time of this commit. Future developers can look into
converting DirectoryInitUtil into a Object Oriented Class.

Notes: this commit only introduces the DirectoryInitUtil extension
class. Extensions and the TempDir API can only be used by tests
using the JUnit 5 runner. Hence, we can only teach tests to use this
DirectoryInitUtil extension later after we migrate tests to use JUnit
5. This ensures that every commit in the meantime remains buildable.

[1] https://stackoverflow.com/questions/54647577/how-to-use-tempdir-in-extensions/54651578#54651578
[2] junit-team/junit5#1786
sijie123 added a commit to sijie123/CS2103-DeadlineManager that referenced this issue Apr 14, 2019
…ntents

Existing tests requiring TemporaryFolders initialise them
independently. Many classes, such as JsonAddressBookStorageTest and
JsonUserPrefsStorageTest, implement custom methods such as
addToTestDataPathIfNotNull(String) method, to dynamically include
resources in temporary directories.

This causes code duplication. Based on the DRY principle, these
duplicated methods can be refactored with the use of a helper class to
manage all directory initialisations.

Additionally, as we migrate to JUnit 5, TemporaryFolder rules are
replaced by TempDir extensions, and there is stronger support for
dynamic extensions that can initialise these temporary folders. Thus,
it is easier and neater to write an helper class extension that
manages the temporary folders.

Let's create a common DirectoryInitUtil class that manages the
initialisation of and file creation within temporary folders. This has
the benefit of ensuring that the temporary directory is properly reset
to a known, initialised state, at the start of every test method.

Design consideration: It would be ideal to implement this extension in
OOP style - abstracting away the idea of copying files and
initialising directories by encapsulating them into a
ManagedTempDirExtension class. However, this design is not possible
because the TempDir tag is not available in extensions [1] as of JUnit
5.4.0. It is slated for JUnit 5.5.0 M2 onwards [2], which is not
released at the time of this commit. Future developers can look into
converting DirectoryInitUtil into a Object Oriented Class.

Notes: this commit only introduces the DirectoryInitUtil extension
class. Extensions and the TempDir API can only be used by tests
using the JUnit 5 runner. Hence, we can only teach tests to use this
DirectoryInitUtil extension later after we migrate tests to use JUnit
5. This ensures that every commit in the meantime remains buildable.

[1] https://stackoverflow.com/questions/54647577/how-to-use-tempdir-in-extensions/54651578#54651578
[2] junit-team/junit5#1786
sijie123 added a commit to sijie123/CS2103-DeadlineManager that referenced this issue Apr 14, 2019
…ntents

Existing tests requiring TemporaryFolders initialise them
independently. Many classes, such as JsonAddressBookStorageTest and
JsonUserPrefsStorageTest, implement custom methods such as
addToTestDataPathIfNotNull(String) method, to dynamically include
resources in temporary directories.

This causes code duplication. Based on the DRY principle, these
duplicated methods can be refactored with the use of a helper class to
manage all directory initialisations.

Additionally, as we migrate to JUnit 5, TemporaryFolder rules are
replaced by TempDir extensions, and there is stronger support for
dynamic extensions that can initialise these temporary folders. Thus,
it is easier and neater to write an helper class extension that
manages the temporary folders.

Let's create a common DirectoryInitUtil class that manages the
initialisation of and file creation within temporary folders. This has
the benefit of ensuring that the temporary directory is properly reset
to a known, initialised state, at the start of every test method.

Design consideration: It would be ideal to implement this extension in
OOP style - abstracting away the idea of copying files and
initialising directories by encapsulating them into a
ManagedTempDirExtension class. However, this design is not possible
because the TempDir tag is not available in extensions [1] as of JUnit
5.4.0. It is slated for JUnit 5.5.0 M2 onwards [2], which is not
released at the time of this commit. Future developers can look into
converting DirectoryInitUtil into a Object Oriented Class.

Notes: this commit only introduces the DirectoryInitUtil extension
class. Extensions and the TempDir API can only be used by tests
using the JUnit 5 runner. Hence, we can only teach tests to use this
DirectoryInitUtil extension later after we migrate tests to use JUnit
5. This ensures that every commit in the meantime remains buildable.

[1] https://stackoverflow.com/questions/54647577/how-to-use-tempdir-in-extensions/54651578#54651578
[2] junit-team/junit5#1786
sijie123 added a commit to sijie123/CS2103-DeadlineManager that referenced this issue Apr 14, 2019
…ntents

Existing tests requiring TemporaryFolders initialise them
independently. Many classes, such as JsonAddressBookStorageTest and
JsonUserPrefsStorageTest, implement custom methods such as
addToTestDataPathIfNotNull(String) method, to dynamically include
resources in temporary directories.

This causes code duplication. Based on the DRY principle, these
duplicated methods can be refactored with the use of a helper class to
manage all directory initialisations.

Additionally, as we migrate to JUnit 5, TemporaryFolder rules are
replaced by TempDir extensions, and there is stronger support for
dynamic extensions that can initialise these temporary folders. Thus,
it is easier and neater to write an helper class extension that
manages the temporary folders.

Let's create a common DirectoryInitUtil class that manages the
initialisation of and file creation within temporary folders. This has
the benefit of ensuring that the temporary directory is properly reset
to a known, initialised state, at the start of every test method.

Design consideration: It would be ideal to implement this extension in
OOP style - abstracting away the idea of copying files and
initialising directories by encapsulating them into a
ManagedTempDirExtension class. However, this design is not possible
because the TempDir tag is not available in extensions [1] as of JUnit
5.4.0. It is slated for JUnit 5.5.0 M2 onwards [2], which is not
released at the time of this commit. Future developers can look into
converting DirectoryInitUtil into a Object Oriented Class.

Notes: this commit only introduces the DirectoryInitUtil extension
class. Extensions and the TempDir API can only be used by tests
using the JUnit 5 runner. Hence, we can only teach tests to use this
DirectoryInitUtil extension later after we migrate tests to use JUnit
5. This ensures that every commit in the meantime remains buildable.

[1] https://stackoverflow.com/questions/54647577/how-to-use-tempdir-in-extensions/54651578#54651578
[2] junit-team/junit5#1786
@marcphilipp marcphilipp modified the milestones: 5.5 M2, 5.5 Backlog Apr 18, 2019
@keturn
Copy link

keturn commented May 4, 2021

A function that returned a CloseableResource go a long way. Even just making this public:

private static CloseablePath createTempDir() {
try {
return new CloseablePath(Files.createTempDirectory(TEMP_DIR_PREFIX));

No need to do everything by annotations.

@juliette-derancourt juliette-derancourt modified the milestones: 5.8 Backlog, 5.8 M2 May 22, 2021
@marcphilipp marcphilipp removed this from the 5.8 M2 milestone Aug 13, 2021
@stale
Copy link

stale bot commented Aug 14, 2022

This issue has been automatically marked as stale because it has not had recent activity. Given the limited bandwidth of the team, it will be automatically closed if no further activity occurs. Thank you for your contribution.

@stale stale bot added the status: stale label Aug 14, 2022
@bmarwell
Copy link

Bump

@scordio
Copy link
Contributor

scordio commented Sep 17, 2022

I started working on a change that could solve this topic: #2958

@sbrannen
Copy link
Member

sbrannen commented Sep 17, 2022

Now, I'm not saying you should do this at home, but...

Since JUnit Jupiter 5.9, it is technically possible to use @TempDir in an extension (or access a parameter provided by any other registered ParameterResolver) with a hack like the following.

import static org.assertj.core.api.Assertions.assertThat;

import java.lang.reflect.Method;
import java.nio.file.Path;

import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.junit.jupiter.api.extension.ExtensionContext;
import org.junit.jupiter.api.extension.ParameterContext;
import org.junit.jupiter.api.extension.ParameterResolutionException;
import org.junit.jupiter.api.extension.ParameterResolver;
import org.junit.jupiter.api.io.TempDir;

class TempDirInExtensionTests {

	@Test
	@ExtendWith(MyExtension.class)
	void test(Path tempDir) {
		assertThat(tempDir).asString().contains("junit").endsWith("hack");
	}

	static class MyExtension implements ParameterResolver {

		@Override
		public boolean supportsParameter(ParameterContext parameterContext, ExtensionContext extensionContext) {
			return parameterContext.getParameter().getType() == Path.class;
		}

		@Override
		public Path resolveParameter(ParameterContext parameterContext, ExtensionContext extensionContext) {
			try {
				Method method = getClass().getDeclaredMethod("getTempDir", Path.class);
				return (Path) extensionContext.getExecutableInvoker().invoke(method, this);
			}
			catch (Exception ex) {
				throw new ParameterResolutionException("Failed to resolve @TempDir", ex);
			}
		}

		Path getTempDir(@TempDir Path path) {
			return path.resolve("hack");
		}
	}

}

And now I'll show myself out. 😇

@stale stale bot removed the status: stale label Sep 17, 2022
@peterdemaeyer
Copy link

IMO this request goes against the good design principles and should be discarded. Imagine the impact of this - expecting the @TempDir annotation to resolve a temporary directory from within an extension. The extension in your example implements BeforeEachCallback but not AfterEachCallback. However, you expect the nested extension to have its own before/after each/all rules, which is counterintuitive. Also, imagine another extension or even the test class itself to also have a @TempDir. They would all resolve to a different temp dir.

It is possible to write extension that depend on other extension, I do it all the time, and I don't need the ParameterResolver. Here is an example of how you can depend extension on other extensions using explicit dependency injection via constructor parameters. The example goes beyond @TempDir, it shows the general principle of depending extensions on each other.

class DependOnTempDirExtensionTest {

	@TempDir
	// @Order does not work here, @TempDir always comes before any other extension.
	static Path tempDir;

	@RegisterExtension
	@Order(1)
	// Use a Supplier for delayed evaluation.
	static DependOnTempDirExtension dependOnTempDirExtension = new DependOnTempDirExtension(() -> tempDir);

	@RegisterExtension
	@Order(2) // Order is important - make sure the dependency is initialized BEFORE this one.
	static DependOnExtensionExtension dependOnExtensionExtension = new DependOnExtensionExtension(dependOnTempDirExtension);

	@Test
	void test() {
		assertNotNull(dependOnTempDirExtension.tempDir);
	}

	static class DependOnTempDirExtension implements BeforeEachCallback, AfterEachCallback, BeforeAllCallback, AfterAllCallback {

		final Supplier<Path> tempDir;

		int state = 0;

                /**
                 * Inject a supplier which acts as a provider for the temp dir. Note that you cannot inject the temp dir directly because it hasn't been initialized at injection time.
                 */
		DependOnTempDirExtension(Supplier<Path> tempDir) {
			this.tempDir = tempDir;
		}

		@Override
		public void afterEach(ExtensionContext context) throws Exception {
			System.out.println("afterEach:tempDir = " + tempDir.get());
			state = 4;
		}

		@Override
		public void beforeEach(ExtensionContext context) throws Exception {
			System.out.println("beforeEach:tempDir = " + tempDir.get());
			state = 3;
		}

		@Override
		public void afterAll(ExtensionContext context) throws Exception {
			System.out.println("afterAll:tempDir = " + tempDir.get());
			state = 2;
		}

		@Override
		public void beforeAll(ExtensionContext context) throws Exception {
			System.out.println("beforeAll:tempDir = " + tempDir.get());
			state = 1;
		}
	}

	static class DependOnExtensionExtension implements BeforeEachCallback, AfterEachCallback, BeforeAllCallback, AfterAllCallback {

		final DependOnTempDirExtension extension;

                /**
                 * Inject another extension which acts as a provider for the state. Note that you cannot inject the state directly because it hasn't been initialized at injection time.
                 */
		DependOnExtensionExtension(DependOnTempDirExtension extension) {
			this.extension = extension;
		}

		@Override
		public void afterAll(ExtensionContext context) throws Exception {
			System.out.println("afterAll:state = " + extension.state);
		}

		@Override
		public void afterEach(ExtensionContext context) throws Exception {
			System.out.println("afterEach:state = " + extension.state);
		}

		@Override
		public void beforeAll(ExtensionContext context) throws Exception {
			System.out.println("beforeAll:state = " + extension.state);
		}

		@Override
		public void beforeEach(ExtensionContext context) throws Exception {
			System.out.println("beforeEach:state = " + extension.state);
		}
	}
}

@mkobit
Copy link
Contributor

mkobit commented Jan 16, 2024

The original requested goal here seems to have been accomplished with the introduction of TempDir#factory(), so this could potentially be closed?

Another idea potentially worth exploring for future extensions, is for the extensions themselves to support their own plugin model internally. Instead of requiring generalized support across Jupiter itself, experiment on a single extension and see if the pattern works in a more generalized way.

This may overcomplicate the extension and usage, but here is where my brainstorming went for @TempDir.

  • Follow the same familiar model as today for annotation-based extensions

  • Define an extension point for the @TempDir extension without adding additional fields to the current annotation type

  • Allow for further extension by developers, and composition of multiple customizations instead of 1 factory

  • TempDirCustomizer: theoretical org.junit.jupiter.api.io.TempDir extension point, executed at some point as part of parameter resolution

    @Inherited
    @Retention(RetentionPolicy.RUNTIME)
    @Target({ ElementType.ANNOTATION_TYPE, ElementType.FIELD, ElementType.PARAMETER })
    public @interface TempDirCustomizer {
        Class<? extends Customizer<?>> value();
        interface Customizer<T extends Annotation> {
            void apply(Path tempDir, T customization) throws IOException;
        }
    }
  • CopyResource: an example of a user created customization applied to a the directory injected by TempDir

    @Inherited
    @TempDirCustomizer(CopyResource.CopyResourceCustomizer.class)
    @Retention(RetentionPolicy.RUNTIME)
    @Target({ ElementType.ANNOTATION_TYPE, ElementType.FIELD, ElementType.PARAMETER })
    @Repeatable(CopyResources.class)
    public @interface CopyResource {
        String resourcesPath();
        boolean recursive() default true;
        String toPath() default ".";
        class CopyResourceCustomizer implements TempDirCustomizer.Customizer<CopyResource> {
            @Override
            public void apply(Path tempDir, CopyResource customization) throws IOException {
                // ...
            }
        }
    }
    
    @Inherited
    @Retention(RetentionPolicy.RUNTIME)
    @Target({ ElementType.ANNOTATION_TYPE, ElementType.FIELD, ElementType.PARAMETER })
    @interface CopyResources {
        CopyResource[] value();
    }
  • SimpleFile: user-created, straightforward, declarative TempDir manipulation

    @Inherited
    @TempDirCustomizer(SimpleFile.SimpleFileCustomizer.class)
    @Retention(RetentionPolicy.RUNTIME)
    @Target({ ElementType.ANNOTATION_TYPE, ElementType.FIELD, ElementType.PARAMETER })
    @Repeatable(SimpleFiles.class)
    public @interface SimpleFile {
        String path();
        String content();
    
        class SimpleFileCustomizer implements TempDirCustomizer.Customizer<SimpleFile> {
            @Override
            public void apply(Path tempDir, SimpleFile customization) throws IOException {
                // ...
            }
        }
    }
    
    @Inherited
    @Retention(RetentionPolicy.RUNTIME)
    @Target({ ElementType.ANNOTATION_TYPE, ElementType.FIELD, ElementType.PARAMETER })
    @interface SimpleFiles {
        SimpleFile[] value();
    }
  • BasicProject: example of a user created compositional customization

    @Inherited
    @CopyResource(resourcesPath = "com/mkobit/baseProject", toPath = "src")
    @CopyResource(resourcesPath = "com/mkobit/example/yaml", toPath = "resources/yaml")
    @SimpleFile(path = "resourcePath.txt", content = "./resources/yaml")
    @SimpleFile(path = "project.json", content = "{\"sources\": [\"src\"]}")
    @Retention(RetentionPolicy.RUNTIME)
    @Target({ ElementType.ANNOTATION_TYPE, ElementType.FIELD, ElementType.PARAMETER })
    public @interface BasicProject {
    }
  • And then a test might look something like this

    @Test
    void testingMyProject(@TempDir @BasicProject Path project) {
        // ...
    }

@scordio
Copy link
Contributor

scordio commented Jan 16, 2024

@mkobit for the sake of clarity, TempDirFactory does not solve the use case requested here (see #2958 (comment))

@marcphilipp
Copy link
Member

marcphilipp commented Apr 15, 2024

Coming back to this, while being a bit involved TempDirFactory can be used to achieve this:

class DemoTests {

	@Test
	void test(@TempDir(factory = InitializingTempDirFactory.class) Path tempDir) {
		// perform test
	}
	
	class InitializingTempDirFactory implements TempDirFactory {
		@Override
		public Path createTempDirectory(AnnotatedElementContext elementContext, ExtensionContext extensionContext)
				throws Exception {
			Path tempDir = Standard.INSTANCE.createTempDirectory(elementContext, extensionContext);
			// initialize tempDir...
			return tempDir;
		}
	}
}

A separate TempDirInitializer interface would be simpler to use:

public interface TempDirInitializer {
    void initializeTempDirectory(Path tempDir, AnnotatedElementContext elementContext, ExtensionContext extensionContext) throws Exception;
}

The test class would become:

class DemoTests {

	@Test
	void test(@TempDir(initializer = Initializer.class) Path tempDir) {
		// perform test
	}
	
	class Initializer implements TempDirInitializer {
		@Override
		public Path initializeTempDirectory(Path tempDir, AnnotatedElementContext elementContext, ExtensionContext extensionContext)
				throws Exception {
			// initialize tempDir...
		}
	}
}

@marcphilipp marcphilipp changed the title Support @TempDir tags in extensions Add extension API for reusable initialization of temporary directories Apr 15, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

10 participants