JUnit @AutoClose Annotation: Close Test Resources (Examples)

JUnit @AutoClose closes test fields after each test or class. Learn custom close methods, static vs instance timing, PER_CLASS, null and failing close().

Two timelines. Default PER_METHOD lifecycle: test 1, @AfterEach, close instance fields, test 2, @AfterEach, close instance fields, @AfterAll, close static fields. PER_CLASS: test 1, @AfterEach, test 2, @AfterEach, @AfterAll, close static fields, close instance fields

The JUnit @AutoClose annotation marks a field that JUnit closes for us after the test or after the test class, so we do not write an @AfterEach or @AfterAll method only to release it. We use it for HTTP clients, thread pools, streams, database connections and any other resource that a test class keeps in a field. The annotation arrived in JUnit 5.11 and works the same way in JUnit 6.

The following example declares three closeable fields for an order pricing test, including one that JUnit ends by calling disconnect() instead of close().

@AutoClose
HttpClient httpClient = HttpClient.newHttpClient();              // close() after each test

@AutoClose
static ExecutorService pool = Executors.newFixedThreadPool(2);  // close() after all tests

@AutoClose("disconnect")
PriceFeed priceFeed = new PriceFeed();                          // disconnect() after each test
PriceFeed.disconnect()
PriceFeed.disconnect()

Notice that the class has two tests and disconnect() ran twice, once after each test, without any cleanup method in the test class. The example project uses JUnit 6.1.3 on Java 25.

We look at the rules first and at the exact moment JUnit closes instance and static fields. After that, we cover fields assigned in @BeforeEach, inheritance, the error cases and a comparison with the other cleanup options.

1. @AutoClose Rules and the Closing Method

@AutoClose is handled by the built-in AutoCloseExtension, which JUnit registers by default, so no @ExtendWith is needed. When the test or the class is done, the extension reads the field value and calls its closing method.

  • By default, JUnit calls close(). The field type does not have to implement AutoCloseable, it only needs a close() method without parameters.
  • The annotation value names a different method, so @AutoClose(“shutdown”) calls shutdown() and @AutoClose(“disconnect”) calls disconnect().
  • The field can be an instance field or a static field, which decides when JUnit closes it.
  • Fields are inherited from superclasses, and JUnit closes subclass fields before superclass fields.
  • When a class has several @AutoClose fields, the closing order is deterministic but intentionally non-obvious, so it does not follow the declaration order.
  • The annotation targets fields only (and other annotations, as a meta-annotation), so method parameters and local variables cannot use it.

The annotation was experimental in JUnit 5.11 and has the stable API status MAINTAINED since JUnit 5.13.3, so we can use it in production test code on JUnit 5.13 and later, including JUnit 6.

Two timelines. Default PER_METHOD lifecycle: test 1, @AfterEach, close instance fields, test 2, @AfterEach, close instance fields, @AfterAll, close static fields. PER_CLASS: test 1, @AfterEach, test 2, @AfterEach, @AfterAll, close static fields, close instance fields
Instance fields close with their test instance, static fields after @AfterAll. With PER_CLASS, instance fields close last.

2. @AutoClose Example With an HTTP Client and a Thread Pool

Say an online shop prices an order by asking a market price service for each item, and the pricing runs on a thread pool. The test class needs a PriceFeed client, an ExecutorService and a HttpClient. Both JDK types implement AutoCloseable, HttpClient since Java 21 and ExecutorService since Java 19, while PriceFeed has only a disconnect() method. The junit-autoclose folder on GitHub has the complete project.

@AutoClose
HttpClient httpClient = HttpClient.newHttpClient();              // close() after each test

@AutoClose
static ExecutorService pool = Executors.newFixedThreadPool(2);  // close() after all tests

@AutoClose("disconnect")
PriceFeed priceFeed = new PriceFeed();                          // disconnect() after each test

@Test
void pricesItemsInParallel() throws Exception {
  Future<BigDecimal> apple = pool.submit(() -> priceFeed.price("apple"));
  Future<BigDecimal> banana = pool.submit(() -> priceFeed.price("banana"));
  assertEquals(new BigDecimal("1.70"), apple.get().add(banana.get()));
}

@Test
void createsHttpClientPerTest() {
  assertEquals(HttpClient.Version.HTTP_2, httpClient.version());
}

The pool is static, so both tests share it and JUnit closes it once at the end. ExecutorService.close() waits for submitted tasks to finish before it returns, so no pricing task is cut off. The PriceFeed and HttpClient fields belong to the test instance, and every test gets new ones.

3. When JUnit Closes an @AutoClose Field

The timing follows the JUnit test lifecycle. JUnit closes instance fields when it is done with the test instance, which comes after the @AfterEach methods, and closes static fields after the @AfterAll methods. The class CloseTimingTest has one field of each kind, and the AuditLog class prints a line from its close() method.

@AutoClose
AuditLog perTestLog = new AuditLog("instance field");

@AutoClose
static AuditLog sharedLog = new AuditLog("static field");

@AfterEach
void afterEach() {
  System.out.println("@AfterEach");
}

@AfterAll
static void afterAll() {
  System.out.println("@AfterAll");
}
@Test cancelsOrder
@AfterEach
close(): instance field
@Test placesOrder
@AfterEach
close(): instance field
@AfterAll
close(): static field

We can see that @AfterEach code can still use the resource, because JUnit closes the field after it. The same holds for static fields and @AfterAll.

With @TestInstance(PER_CLASS), JUnit keeps one test instance for the whole class. The instance fields stay open until the instance is no longer needed, which is after @AfterAll and after the static fields.

@Test cancelsOrder
@AfterEach
@Test placesOrder
@AfterEach
@AfterAll
close(): static field (PER_CLASS)
close(): instance field (PER_CLASS)

So an instance field is closed after each test only under the default per-method lifecycle. Under PER_CLASS, a resource such as a connection stays open for all tests of the class, and the tests must not rely on a fresh one.

4. Fields Assigned in @BeforeEach

The field does not need an initializer. JUnit reads the value at closing time, so a field that @BeforeEach assigns is closed as well. For example, Files.lines() returns a Stream that keeps the file open until the stream is closed, and Stream has a close() method.

@TempDir
Path dir;

@AutoClose
Stream<String> lines;                                  // Files.lines() keeps the file open

@BeforeEach
void openOrderFile() throws IOException {
  Path file = dir.resolve("orders.csv");
  Files.write(file, List.of("apple,3", "banana,5", "apple,1"));
  lines = Files.lines(file);
}

@Test
void countsAppleOrders() {
  long appleOrders = lines.filter(line -> line.startsWith("apple")).count();
  assertEquals(2, appleOrders);
}

The @TempDir field gives each test its own folder, and JUnit deletes it after the test. An open file handle can block that deletion on Windows, which is one more reason to close the stream.

5. @AutoClose Fields in a Superclass

A base test class can declare shared resources with @AutoClose, and every subclass inherits them. JUnit closes the subclass fields first and the superclass fields last, the same order it uses for @AfterEach methods.

abstract class AbstractAuditTest {

  @AutoClose
  AuditLog baseLog = new AuditLog("superclass field");
}
class RefundAuditTest extends AbstractAuditTest {

  @AutoClose
  AuditLog refundLog = new AuditLog("subclass field");

  @Test
  void refundsOrder() {
    System.out.println("@Test refundsOrder");
  }
}
@Test refundsOrder
close(): subclass field
close(): superclass field

6. Null Fields, Failing close() and Invalid Fields

JUnit treats a missing value and a failing closing method differently. A field whose value is null at closing time is skipped, and JUnit only logs a warning. That happens when a test class declares a resource that only some setups assign.

@AutoClose("disconnect")
PriceFeed priceFeed;                                   // never assigned
@Test worksOffline
WARNING: Cannot @AutoClose field com.howtodoinjava.junit.autoclose.demo.NullFieldDemo.priceFeed because it is null.
[INFO] Tests run: 1, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.507 s -- in com.howtodoinjava.junit.autoclose.demo.NullFieldDemo

When the closing method throws, the exception is not suppressed. JUnit reports the test as failed with that exception, even though the test method passed, and it still closes the other @AutoClose fields of the class.

@AutoClose
Connection connection = new Connection();

@AutoClose
ReportFile report = new ReportFile();          // close() throws IllegalStateException
@Test writesReport passed
ReportFile.close() throws
Connection.close() still runs
[ERROR] Tests run: 1, Failures: 0, Errors: 1, Skipped: 0, Time elapsed: 0.252 s <<< FAILURE! -- in com.howtodoinjava.junit.autoclose.demo.FailingCloseDemo
[ERROR] com.howtodoinjava.junit.autoclose.demo.FailingCloseDemo.writesReport -- Time elapsed: 0.161 s <<< ERROR!
java.lang.IllegalStateException: Cannot flush report.csv

A field whose type has no method with the configured name fails the test with an ExtensionConfigurationException, and a primitive field fails it with a PreconditionViolationException. In both cases, the test method itself runs first, so the error appears after the test output.

org.junit.jupiter.api.extension.ExtensionConfigurationException: Cannot @AutoClose field com.howtodoinjava.junit.autoclose.demo.InvalidFieldDemo.customer because java.lang.String does not define method close().
org.junit.platform.commons.PreconditionViolationException: @AutoClose is not supported on primitive field com.howtodoinjava.junit.autoclose.demo.PrimitiveFieldDemo.retries.

7. @AutoClose vs @AfterEach vs try-with-resources

All three options close a resource, and they differ in scope and in how much code we write. @AutoClose fits resources held in fields, try-with-resources fits resources that one test opens and uses, and a cleanup method fits work that is more than a single method call.

OptionBest forCode we write
@AutoClose fieldClients, pools and streams kept in a fieldOne annotation per field
@AfterEach / @AfterAll methodCleanup with several steps or conditions, such as resetting static stateA method with the close calls and null checks
try-with-resources in the testA resource used by one test onlyA try block in that test
@TempDirTemporary files and foldersOne annotation; JUnit deletes the folder

Before JUnit 5.11, a test class with a reader and a writer needed an @AfterEach method with two null checks and two close() calls. With @AutoClose, the two field annotations replace that whole method.

8. JUnit @AutoClose FAQs

Before replacing cleanup methods with @AutoClose, teams check whether their JUnit version and their field types support it.

8.1. Which JUnit Version Added @AutoClose?

JUnit 5.11 added @AutoClose in the org.junit.jupiter.api package, and JUnit 6 keeps it unchanged. On JUnit 5.10 and older, we close resources in @AfterAll or @AfterEach methods.

8.2. Does the Field Have to Implement AutoCloseable?

No. JUnit calls the method by name, so any object with a close() method works, and @AutoClose(“name”) works for any other method without parameters, such as shutdown() or disconnect().

8.3. In Which Order Does JUnit Close Several Fields?

The order within one class is deterministic but intentionally non-obvious, and it is not the declaration order. When one resource depends on another, for example a statement on a connection, we close both in one @AfterEach method in the right order, or keep only the outer resource in an @AutoClose field.

8.4. Can We Use @AutoClose With JUnit 4?

No. @AutoClose is part of the Jupiter API, so it works only in tests that run on the Jupiter engine. JUnit 4 tests that run through the vintage engine keep their @After methods.

9. Conclusion

@AutoClose closes a field after the test or the class, by calling close() or the method named in the annotation. Instance fields close after each test with the default lifecycle, static fields close after @AfterAll, and under PER_CLASS instance fields close last.

A null field is skipped with a warning, while a failing close fails the test and the other fields still close. For multi-step cleanup we keep an @AfterEach method. Guides for the other test lifecycle annotations are listed in the JUnit tutorial.

10. References

Happy Learning !!

Source Code on Github

About Us

HowToDoInJava provides tutorials and how-to guides on Java and related technologies.

It also shares the best practices, algorithms & solutions and frequently asked interview questions.