JUnit @AfterEach: Clean Up After Every Test (Examples)

JUnit @AfterEach runs cleanup after every test, even when it fails. See when it runs, why static state needs it, inheritance order and errors in JUnit 6.

Table diagram with three columns: what happens, whether @AfterEach runs, and the reported result. Passing test: runs, passed. Failed assertion: runs, failed. @BeforeEach throws: runs, failed. Assumption aborts the test: runs, skipped. @Disabled test: does not run, skipped. @AfterEach throws: test failed

The JUnit @AfterEach annotation marks a method that JUnit runs after every test method in the class, whether the test passed or failed. We use it for cleanup that the next test depends on, such as clearing a static cache, deleting exported files or restoring a JVM setting that a test changed.

The following example clears an app-wide report cache after each test, so the second test does not see the report that the first test stored.

@AfterEach
void clearCache() {
  ReportCache.clear();
  System.out.println("@AfterEach: cache cleared");
}

@Test
void cachesMonthlyReport() {
  ReportCache.put("march", "total 120");
  System.out.println("@Test cachesMonthlyReport: size " + ReportCache.size());
  assertEquals(1, ReportCache.size());          // passes
}

@Test
void cachesYearlyReport() {
  ReportCache.put("2026", "total 1450");
  System.out.println("@Test cachesYearlyReport: size " + ReportCache.size());
  assertEquals(1, ReportCache.size());          // passes, not 2
}
@Test cachesYearlyReport: size 1
@AfterEach: cache cleared
@Test cachesMonthlyReport: size 1
@AfterEach: cache cleared

Notice that each test prints size 1, because clearCache() empties the cache between the two tests. JUnit 6.1.3 on Java 25 runs these examples, and @AfterEach has not changed since JUnit 5.

In the rest of the post, we look at the cases where cleanup is needed even though JUnit creates a new test object for every test. After that, we cover the failure cases, the order in class hierarchies and the alternatives such as @TempDir and @AutoClose.

1. How @AfterEach Fits Into the Test Lifecycle

JUnit calls an @AfterEach method after each @Test, @RepeatedTest, @ParameterizedTest, @TestFactory and @TestTemplate method in the current class. Each repetition of a repeated test and each invocation of a parameterized test counts as a separate test, so the cleanup runs once per invocation. The method is the counterpart of @BeforeEach, and it replaces @After from JUnit 4.

JUnit checks the method signature when it discovers the test class.

  • Like @BeforeEach, it has a void return type and is never static, because JUnit calls it on the test instance.
  • The method can declare parameters such as TestInfo or TestReporter, and JUnit resolves them for the test that has finished.
  • A private @AfterEach method still runs in JUnit 6.1.3 with a warning, because private lifecycle methods will be disallowed in a future release.
  • Several @AfterEach methods in one class run in a deterministic but intentionally non-obvious order, so we keep cleanup steps that depend on each other in one method.

The decision of whether JUnit calls the method depends on how far the test got. A test that started always gets its @AfterEach call, and a disabled test never starts.

Table diagram with three columns: what happens, whether @AfterEach runs, and the reported result. Passing test: runs, passed. Failed assertion: runs, failed. @BeforeEach throws: runs, failed. Assumption aborts the test: runs, skipped. @Disabled test: does not run, skipped. @AfterEach throws: test failed
JUnit runs @AfterEach for every test that started, including failed and aborted tests, but not for disabled tests.

1.1. Why Cleanup Is Needed When Every Test Gets a New Instance

JUnit runs every test method on a fresh instance of the test class, as the JUnit test lifecycle shows. Instance fields therefore start fresh in every test, and resetting them in @AfterEach adds nothing. The new instance does not reset anything that lives outside the test object, so these are the cases that need @AfterEach.

  • Static fields and singletons, such as an app-wide cache or a registry.
  • JVM-wide settings, such as Locale.setDefault(), TimeZone.setDefault() and system properties.
  • Files and folders written to a fixed location instead of a temporary directory.
  • Rows in a shared test database, messages in an embedded broker, or stubs registered on a mock server.

The class NoCleanupDemo has the same two tests as the intro example but no @AfterEach method. The cache is static, so the second test sees two reports and fails.

@Test cachesYearlyReport: size 1
@Test cachesMonthlyReport: size 2
[ERROR] Tests run: 2, Failures: 1, Errors: 0, Skipped: 0, Time elapsed: 0.126 s <<< FAILURE! -- in com.howtodoinjava.junit.aftereach.demo.NoCleanupDemo
[ERROR] com.howtodoinjava.junit.aftereach.demo.NoCleanupDemo.cachesMonthlyReport -- Time elapsed: 0.029 s <<< FAILURE!
org.opentest4j.AssertionFailedError: expected: <1> but was: <2>

A test that passes alone and fails in the full run is the typical sign of missing cleanup. The result depends on the order in which JUnit runs the methods, so the failure can appear on a build server and not in the IDE.

2. Restoring a JVM Setting After Each Test

Say an invoicing app formats amounts with the default locale of the JVM, and one test checks the German format while another checks the US format. Each test calls Locale.setDefault(), which changes the setting for the whole JVM, including every test class that runs later in the same Maven fork. So we save the original value in @BeforeEach and put it back in @AfterEach.

Locale originalLocale;

@BeforeEach
void saveLocale() {
  originalLocale = Locale.getDefault();
}

@AfterEach
void restoreLocale() {
  Locale.setDefault(originalLocale);
}

@Test
void formatsGermanAmount() {
  Locale.setDefault(Locale.GERMANY);
  String text = InvoiceFormatter.format(new BigDecimal("1234.5"));
  assertEquals("1.234,50", text);
}

@Test
void formatsUsAmount() {
  Locale.setDefault(Locale.US);
  String text = InvoiceFormatter.format(new BigDecimal("1234.5"));
  assertEquals("1,234.50", text);
}

The same save-and-restore pattern works for TimeZone.setDefault() and for System.setProperty(), where we call System.clearProperty() in @AfterEach when the property did not exist before.

3. @AfterEach When a Test Fails, Aborts or Is Disabled

JUnit calls @AfterEach after a failed assertion too. In FailingTestDemo, the assertion expects two cached reports and finds one, and the cleanup method still prints its line before Surefire reports the failure.

@Test
void cachesReport() {
  ReportCache.put("march", "total 120");
  assertEquals(2, ReportCache.size(), "report count");
}

@AfterEach
void clearCache() {
  ReportCache.clear();
  System.out.println("@AfterEach clearCache() ran, size " + ReportCache.size());
}
@AfterEach clearCache() ran, size 0
[ERROR] Tests run: 1, Failures: 1, Errors: 0, Skipped: 0, Time elapsed: 0.154 s <<< FAILURE! -- in com.howtodoinjava.junit.aftereach.demo.FailingTestDemo
[ERROR] com.howtodoinjava.junit.aftereach.demo.FailingTestDemo.cachesReport -- Time elapsed: 0.094 s <<< FAILURE!
org.opentest4j.AssertionFailedError: report count ==> expected: <2> but was: <1>

A test that an assumption aborts has also started, so its @AfterEach method runs. A test marked with @Disabled never starts, and JUnit skips its @BeforeEach and @AfterEach methods as well. The class SkippedTestsDemo has one test of each kind.

@Test
void needsMailServer() {
  assumeTrue(System.getenv("MAIL_HOST") != null, "MAIL_HOST is not set");
  System.out.println("@Test needsMailServer");
}

@Disabled("Report format changes in the next release")
@Test
void exportsPdf() {
  System.out.println("@Test exportsPdf");
}

@AfterEach
void cleanUp(TestInfo testInfo) {
  System.out.println("@AfterEach after " + testInfo.getDisplayName());
}
@AfterEach after needsMailServer()
[WARNING] Tests run: 2, Failures: 0, Errors: 0, Skipped: 2, Time elapsed: 0.202 s -- in com.howtodoinjava.junit.aftereach.demo.SkippedTestsDemo

Surefire counts both tests as skipped, but only needsMailServer() printed the cleanup line.

4. An Exception Thrown by @AfterEach

An exception in @AfterEach fails the test, even when the test method itself passed. In FailingCleanupDemo, the test passes and the cleanup method throws IllegalStateException because it cannot delete a report file.

@Test
void cachesReport() {
  ReportCache.put("march", "total 120");
  System.out.println("@Test cachesReport passed");
}

@AfterEach
void deleteReportFile() {
  ReportCache.clear();
  throw new IllegalStateException("Cannot delete report file march.pdf");
}
@Test cachesReport passed
[ERROR] Tests run: 1, Failures: 0, Errors: 1, Skipped: 0, Time elapsed: 0.244 s <<< FAILURE! -- in com.howtodoinjava.junit.aftereach.demo.FailingCleanupDemo
[ERROR] com.howtodoinjava.junit.aftereach.demo.FailingCleanupDemo.cachesReport -- Time elapsed: 0.166 s <<< ERROR!
java.lang.IllegalStateException: Cannot delete report file march.pdf

Surefire reports the error under the test name cachesReport, so a red test does not always mean the production code is wrong. We read the stack trace first, and when it points at the cleanup method, the bug is in the test setup. When the test method and @AfterEach both throw, JUnit reports the test exception and adds the cleanup exception to it as a suppressed exception.

org.opentest4j.AssertionFailedError: report count ==> expected: <2> but was: <1>
	at com.howtodoinjava.junit.aftereach.demo.BothFailDemo.cachesReport(BothFailDemo.java:16)
	Suppressed: java.lang.IllegalStateException: Cannot delete report file march.pdf
		at com.howtodoinjava.junit.aftereach.demo.BothFailDemo.deleteReportFile(BothFailDemo.java:21)

Cleanup code also has to cope with a half-finished setup. If @BeforeEach throws before it assigns a field, the field is still null when @AfterEach runs, so we check it before calling methods on it, as the Files.notExists() check in the next section does.

5. @AfterEach in a Superclass and a Subclass

@AfterEach methods are inherited from superclasses and interface default methods, unless the subclass overrides them. JUnit runs the subclass @AfterEach methods first and the superclass @AfterEach methods last, which is the reverse of the @BeforeEach order. For example, an invoicing app writes CSV exports into target/exports, and every export test class extends one base class that deletes the files.

abstract class AbstractExportTest {

  protected static final Path EXPORT_DIR = Path.of("target", "exports");

  protected final InvoiceExporter exporter = new InvoiceExporter(EXPORT_DIR);

  @AfterEach
  void deleteExports() throws IOException {
    if (Files.notExists(EXPORT_DIR)) {
      return;
    }
    try (Stream<Path> files = Files.list(EXPORT_DIR)) {
      for (Path file : files.toList()) {
        Files.delete(file);
      }
    }
    System.out.println("2. AbstractExportTest.deleteExports()");
  }
}

The subclass logs how many files each test exported. Its @AfterEach method also takes a TestInfo parameter to print the name of the test that has finished.

class InvoiceExportTest extends AbstractExportTest {

  @Test
  void exportsTwoInvoices() throws IOException {
    exporter.export("alex", new BigDecimal("120.00"));
    exporter.export("maria", new BigDecimal("80.50"));
    assertEquals(2, countExports());
  }

  @AfterEach
  void logExports(TestInfo testInfo) throws IOException {
    System.out.println("1. " + testInfo.getDisplayName() + " exported " + countExports() + " file(s)");
  }
}
1. exportsTwoInvoices() exported 2 file(s)
2. AbstractExportTest.deleteExports()
1. exportsOneInvoice() exported 1 file(s)
2. AbstractExportTest.deleteExports()

We can see that the subclass method still finds the exported files, because the superclass deletes them afterwards. Without the cleanup, exportsOneInvoice() would count the files of the previous test and fail.

6. Choosing Between @AfterEach, @AfterAll, @TempDir and @AutoClose

@AfterEach is not the only cleanup tool in JUnit. For files and closeable objects, JUnit has annotations that do the cleanup for us, so the test class needs no cleanup method at all.

NeedToolWhen JUnit cleans up
Reset static state, JVM settings or external data@AfterEach methodAfter every test
Stop a server or a pool shared by all tests@AfterAll methodOnce, after the last test of the class
Delete files the test writes@TempDir field or parameterAfter the test (or the class, for a static field)
Close a client, stream or executor held in a field@AutoClose fieldAfter the test (instance field) or after the class (static field)
Close a resource used in one test onlytry-with-resources inside the testAt the end of the try block

Our export example writes to a fixed folder because the production code reads its path from configuration. When the code under test accepts a Path, we pass a @TempDir folder instead and delete the @AfterEach method.

7. @AfterEach FAQs

Static cleanup methods, the JUnit 4 name @After, parameterized tests and Mockito mocks cause most of the confusion around @AfterEach.

7.1. Can an @AfterEach Method Be Static?

No. JUnit reports a static @AfterEach method as a discovery error, and the JUnit Jupiter engine runs none of its tests, not even those in other classes.

(1) [ERROR] @AfterEach method 'static void com.howtodoinjava.junit.aftereach.demo.StaticAfterEachDemo.clearCache()' must not be static.

7.2. What Is the Difference Between @After and @AfterEach?

The two annotations do the same job in different JUnit versions. @After comes from org.junit in JUnit 4, and @AfterEach comes from org.junit.jupiter.api in JUnit 5 and 6. When we migrate a test class, we replace the annotation and its import, and the method body stays the same. The JUnit 5 vs JUnit 4 comparison lists the other renamed annotations.

7.3. Does @AfterEach Run After Each @ParameterizedTest Invocation?

Yes. Every invocation of a parameterized test and every repetition of a repeated test is a separate test for JUnit, so @BeforeEach and @AfterEach wrap each one.

7.4. Should We Reset Mockito Mocks in @AfterEach?

No, not when the mocks are fields of the test class and the class uses the default lifecycle. MockitoExtension creates new @Mock instances for every test instance, and JUnit creates a new test instance for every test. A reset is needed only for mocks kept in static fields or shared through @TestInstance(PER_CLASS).

8. Conclusion

@AfterEach runs after every test that started, including failed and aborted tests, so it is the place to undo what a test changed outside its own object. Instance fields need no cleanup with the default lifecycle, whereas static state, JVM settings and files in fixed folders do.

Superclass cleanup runs after subclass cleanup, and an exception in @AfterEach fails the test. For temporary files and closeable fields, @TempDir and @AutoClose remove the need for a cleanup method. The JUnit tutorial has the other lifecycle and assertion guides.

9. 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.