diff --git a/spring-test/src/main/java/org/springframework/test/context/event/EventPublishingTestExecutionListener.java b/spring-test/src/main/java/org/springframework/test/context/event/EventPublishingTestExecutionListener.java
index d6b5c9cef4..953b5710ce 100644
--- a/spring-test/src/main/java/org/springframework/test/context/event/EventPublishingTestExecutionListener.java
+++ b/spring-test/src/main/java/org/springframework/test/context/event/EventPublishingTestExecutionListener.java
@@ -63,7 +63,10 @@ import org.springframework.test.context.support.AbstractTestExecutionListener;
* register a {@code TestExecutionListener} that loads the {@code ApplicationContext}
* in the {@link org.springframework.test.context.TestExecutionListener#beforeTestClass
* beforeTestClass} callback, and that {@code TestExecutionListener} must be registered
- * before the {@code EventPublishingTestExecutionListener}.
+ * before the {@code EventPublishingTestExecutionListener}. Similarly, if
+ * {@code @DirtiesContext} is used to remove the {@code ApplicationContext} from
+ * the context cache after the last test method in a given test class, the
+ * {@code AfterTestClassEvent} will not be published for that test class.
*
*
Exception Handling
* By default, if a test event listener throws an exception while consuming
diff --git a/spring-test/src/test/java/org/springframework/test/context/event/DirtiesContextEventPublishingTests.java b/spring-test/src/test/java/org/springframework/test/context/event/DirtiesContextEventPublishingTests.java
new file mode 100644
index 0000000000..e6dab3c4f7
--- /dev/null
+++ b/spring-test/src/test/java/org/springframework/test/context/event/DirtiesContextEventPublishingTests.java
@@ -0,0 +1,237 @@
+/*
+ * Copyright 2002-2022 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.test.context.event;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.MethodOrderer.DisplayName;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.TestMethodOrder;
+import org.junit.platform.testkit.engine.EngineTestKit;
+
+import org.springframework.context.annotation.Configuration;
+import org.springframework.test.annotation.DirtiesContext;
+import org.springframework.test.annotation.DirtiesContext.MethodMode;
+import org.springframework.test.context.TestPropertySource;
+import org.springframework.test.context.event.annotation.AfterTestClass;
+import org.springframework.test.context.event.annotation.AfterTestExecution;
+import org.springframework.test.context.event.annotation.AfterTestMethod;
+import org.springframework.test.context.event.annotation.BeforeTestClass;
+import org.springframework.test.context.event.annotation.BeforeTestExecution;
+import org.springframework.test.context.event.annotation.BeforeTestMethod;
+import org.springframework.test.context.event.annotation.PrepareTestInstance;
+import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.junit.platform.engine.discovery.DiscoverySelectors.selectClass;
+
+/**
+ * Tests for the {@link EventPublishingTestExecutionListener} which verify
+ * behavior for test context events when {@link DirtiesContext @DirtiesContext}
+ * is used.
+ *
+ * @author Sam Brannen
+ * @since 5.3.17
+ * @see https://github.com/spring-projects/spring-framework/issues/27757
+ */
+class DirtiesContextEventPublishingTests {
+
+ private static final List> events = new ArrayList<>();
+
+
+ @BeforeEach
+ @AfterEach
+ void resetEvents() {
+ events.clear();
+ }
+
+ @Test
+ void classLevelDirtiesContext() {
+ EngineTestKit.engine("junit-jupiter")//
+ .selectors(selectClass(ClassLevelDirtiesContextTestCase.class))//
+ .execute()//
+ .testEvents()//
+ .assertStatistics(stats -> stats.started(1).succeeded(1).failed(0));
+
+ assertThat(events).containsExactly(//
+ // BeforeTestClassEvent.class -- always missing for 1st test class by default
+ PrepareTestInstanceEvent.class, //
+ BeforeTestMethodEvent.class, //
+ BeforeTestExecutionEvent.class, //
+ AfterTestExecutionEvent.class, //
+ AfterTestMethodEvent.class, //
+ AfterTestClassEvent.class //
+ );
+ }
+
+ @Test
+ void methodLevelAfterMethodDirtiesContext() {
+ EngineTestKit.engine("junit-jupiter")//
+ .selectors(selectClass(MethodLevelAfterMethodDirtiesContextTestCase.class))//
+ .execute()//
+ .testEvents()//
+ .assertStatistics(stats -> stats.started(1).succeeded(1).failed(0));
+
+ assertThat(events).containsExactly(//
+ // BeforeTestClassEvent.class -- always missing for 1st test class by default
+ PrepareTestInstanceEvent.class, //
+ BeforeTestMethodEvent.class, //
+ BeforeTestExecutionEvent.class, //
+ AfterTestExecutionEvent.class, //
+ AfterTestMethodEvent.class //
+ // AfterTestClassEvent.class -- missing b/c of @DirtiestContext "after method" at the method level
+ );
+ }
+
+ @Test
+ void methodLevelAfterMethodDirtiesContextWithSubsequentTestMethod() {
+ EngineTestKit.engine("junit-jupiter")//
+ .selectors(selectClass(MethodLevelAfterMethodDirtiesContextWithSubsequentTestMethodTestCase.class))//
+ .execute()//
+ .testEvents()//
+ .assertStatistics(stats -> stats.started(2).succeeded(2).failed(0));
+
+ assertThat(events).containsExactly(//
+ // BeforeTestClassEvent.class -- always missing for 1st test class by default
+ // test1()
+ PrepareTestInstanceEvent.class, //
+ BeforeTestMethodEvent.class, //
+ BeforeTestExecutionEvent.class, //
+ AfterTestExecutionEvent.class, //
+ AfterTestMethodEvent.class, //
+ // test2()
+ PrepareTestInstanceEvent.class, //
+ BeforeTestMethodEvent.class, //
+ BeforeTestExecutionEvent.class, //
+ AfterTestExecutionEvent.class, //
+ AfterTestMethodEvent.class, //
+ AfterTestClassEvent.class // b/c @DirtiestContext is not applied for test2()
+ );
+ }
+
+ @Test
+ void methodLevelBeforeMethodDirtiesContext() {
+ EngineTestKit.engine("junit-jupiter")//
+ .selectors(selectClass(MethodLevelBeforeMethodDirtiesContextTestCase.class))//
+ .execute()//
+ .testEvents()//
+ .assertStatistics(stats -> stats.started(1).succeeded(1).failed(0));
+
+ assertThat(events).containsExactly(//
+ // BeforeTestClassEvent.class -- always missing for 1st test class by default
+ PrepareTestInstanceEvent.class, //
+ BeforeTestMethodEvent.class, //
+ BeforeTestExecutionEvent.class, //
+ AfterTestExecutionEvent.class, //
+ AfterTestMethodEvent.class, //
+ AfterTestClassEvent.class // b/c @DirtiestContext happens "before method" at the method level
+ );
+ }
+
+ @SpringJUnitConfig(Config.class)
+ // add unique property to get a unique ApplicationContext
+ @TestPropertySource(properties = "DirtiesContextEventPublishingTests.key = class-level")
+ @DirtiesContext
+ static class ClassLevelDirtiesContextTestCase {
+
+ @Test
+ void test() {
+ }
+ }
+
+ @SpringJUnitConfig(Config.class)
+ // add unique property to get a unique ApplicationContext
+ @TestPropertySource(properties = "DirtiesContextEventPublishingTests.key = method-level-after-method")
+ static class MethodLevelAfterMethodDirtiesContextTestCase {
+
+ @Test
+ @DirtiesContext
+ void test1() {
+ }
+ }
+
+ @SpringJUnitConfig(Config.class)
+ // add unique property to get a unique ApplicationContext
+ @TestPropertySource(properties = "DirtiesContextEventPublishingTests.key = method-level-after-method-with-subsequent-test-method")
+ @TestMethodOrder(DisplayName.class)
+ static class MethodLevelAfterMethodDirtiesContextWithSubsequentTestMethodTestCase {
+
+ @Test
+ @DirtiesContext
+ void test1() {
+ }
+
+ @Test
+ void test2() {
+ }
+ }
+
+ @SpringJUnitConfig(Config.class)
+ // add unique property to get a unique ApplicationContext
+ @TestPropertySource(properties = "DirtiesContextEventPublishingTests.key = method-level-before-method")
+ static class MethodLevelBeforeMethodDirtiesContextTestCase {
+
+ @Test
+ @DirtiesContext(methodMode = MethodMode.BEFORE_METHOD)
+ void test() {
+ }
+ }
+
+ @Configuration
+ static class Config {
+
+ @BeforeTestClass
+ public void beforeTestClass(BeforeTestClassEvent e) {
+ events.add(e.getClass());
+ }
+
+ @PrepareTestInstance
+ public void prepareTestInstance(PrepareTestInstanceEvent e) {
+ events.add(e.getClass());
+ }
+
+ @BeforeTestMethod
+ public void beforeTestMethod(BeforeTestMethodEvent e) {
+ events.add(e.getClass());
+ }
+
+ @BeforeTestExecution
+ public void beforeTestExecution(BeforeTestExecutionEvent e) {
+ events.add(e.getClass());
+ }
+
+ @AfterTestExecution
+ public void afterTestExecution(AfterTestExecutionEvent e) {
+ events.add(e.getClass());
+ }
+
+ @AfterTestMethod
+ public void afterTestMethod(AfterTestMethodEvent e) {
+ events.add(e.getClass());
+ }
+
+ @AfterTestClass
+ public void afterTestClass(AfterTestClassEvent e) {
+ events.add(e.getClass());
+ }
+
+ }
+
+}
diff --git a/src/docs/asciidoc/testing.adoc b/src/docs/asciidoc/testing.adoc
index 9a3dbf598e..61727e9b0b 100644
--- a/src/docs/asciidoc/testing.adoc
+++ b/src/docs/asciidoc/testing.adoc
@@ -2725,6 +2725,10 @@ If you wish to ensure that a `BeforeTestClassEvent` is always published for ever
class, you need to register a `TestExecutionListener` that loads the `ApplicationContext`
in the `beforeTestClass` callback, and that `TestExecutionListener` must be registered
_before_ the `EventPublishingTestExecutionListener`.
+
+Similarly, if `@DirtiesContext` is used to remove the `ApplicationContext` from the
+context cache after the last test method in a given test class, the `AfterTestClassEvent`
+will not be published for that test class.
====
In order to listen to test execution events, a Spring bean may choose to implement the