How to use Temporary files for Unit testing in Java

辰
Javarevisited
Published in
3 min readApr 24, 2019

--

Sometimes we need to create temporary files and use them in a unit test. And usually, I do like this.

@Test void testSome() {
File file;
try {
file = File.createTempFile("tmp", null);
} catch (IOException ioe) {
throw new RuntimeException(ioe);
}
//file.deleteOnExit(); // not recommended at all
try {
// do some with the file
} finally {
if (file.isFile() && !file.delete()) {
// failed to delete the (existing) file
}
}
}

Utility functions

We can, as the number of test cases is grown, use utility methods look like this.

static <R> applyTempFile(Function<File, R> function) {
File file;
try {
file = File.createTempFile("tmp", null);
} catch (IOException ioe) {
throw new RuntimeException(ioe);
}
try {
return function.apply(file);
} finally {
if (file.isFile() && !file.delete()) {
// failed to delete the (existing) file
}
}
}
static <R, U> applyTempFile(BiFunction<File, U, R> function,
Supplier<U> supplier) {
return applyTempFile(f -> function.apply(f, supplier.get()));
}
static void acceptTempFile(Consumer<File> consumer) {
applyTempFile(f -> {
consumer.accept(f);
return null;
});
}
static <U> void acceptTempFile(BiConsumer<File, U> consumer,
Supplier<U> supplier) {
acceptTempFile(f -> consumer.accept(f, supplier.get()));
}
// TODO: add methods for java.nio.file.Path

ParameterResolver

Another way is by using a ParameterResolver.

class TemporaryFileParameterResolver implements ParameterResolver {    @Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.PARAMETER})
@interface Temporary {
boolean deleteOnExit() default true;
}
@Override
public boolean supportsParameter(
ParameterContext parameterContext,
ExtensionContext extensionContext)
throws ParameterResolutionException {
if (parameterContext.isAnnotated(Temporary.class)) {
Parameter parameter = parameterContext.getParameter();
Class<?> parameterType = parameter.getType();
return File.class == parameterType
|| Path.class == parameterType;
}
return false;
}
@Override
public Object resolveParameter(
ParameterContext parameterContext,
ExtensionContext extensionContext)
throws ParameterResolutionException {
Parameter parameter = parameterContext.getParameter();
Temporary temporary =
parameter.getAnnotation(Temporary.class);
Class<?> parameterType = parameter.getType();
if (File.class == parameterType) {
File file;
try {
file = File.createTempFile("tmp", null);
} catch (final IOException ioe) {
throw new ParameterResolutionException(ioe);
}
if (temporary.deleteOnExit()) {
file.deleteOnExit();
}
return file;
}
if (Path.class == parameterType) {
Path path;
try {
path = Files.createTempFile(null, null);
} catch (final IOException ioe) {
throw new ParameterResolutionException(
"failed to create a temporary file", ioe);
}
if (temporary.deleteOnExit()) {
Runtime.getRuntime().addShutdownHook(
new Thread(() -> {
try {
Files.deleteIfExists(path);
} catch (final IOException ioe) {
throw new RuntimeException(ioe);
}
}));
}
return path;
}
throw new ParameterResolutionException(
"failed to resolve parameter for " + parameterContext);
}
}

Now we can use the resolver class like this.

@ExtendWith({TemporaryFileParameterResolver.class})
class SomeTest {
@Test void testWithFile(@Temporary File file) {
}
@Test void testWithPath(@Temporary Path path) {
}
}

Caution against deleteOnExit and addShutdownHook

According to the JDK source code, the deleteOnExit adds the file’s path to a Set and addShutdownHook adds the thread to a Map and clean them when the JVM is shutting down.

That means excessive files/references mat stay in storage/memory until a clean shutdown process.

And that’s why I added deleteOnExit element, in the first place, on the annotation.

@ExtendWith({TemporaryFileParameterResolver.class})
class SomeExcessiveFileTest {
@RepeatedTest(1048576)
void test(@Temporary(deleteOnExit = false) File file) {
try {
// do some with the file
} finally {
if (file.isFile() && !file.delete()) {
}
}
}
@RepeatedTest(1048576)
void test(@Temporary(deleteOnExit = false) Path path) {
try {
// do some with the path
} finally {
boolean deleted = Files.deleteIfExists(path);
}
}
}

That’s all about how to use temporary files for unit testing and how to clean up after test so that your application is always in a good state for new testing.

If you want to learn more about Unit testing in Java, here are some useful resources.

  1. Java Unit Tests for Beginners
  2. Learn Unit Testing with JUnit & Mockito in 30 Steps
  3. Test-Driven Development Practices in Java
  4. 10 Testing libraries Java Developers Should Know
  5. Top 5 JUnit and Unit testing Courses for Programmers

Closing Notes

Thanks for reading this article so far. This is a very useful tip for Java programmer. If you like this article then please consider the following our Publication on Medium.

If you’d like to be notified for every new post and don’t forget to follow javarevisited on Twitter!

--

--