From f5d58ebe00135b48aa162943b22d073858b8fbcb Mon Sep 17 00:00:00 2001 From: Washingtonwei Date: Mon, 25 May 2026 07:34:36 -0500 Subject: [PATCH 1/2] refactor: replace @DirtiesContext with @Transactional and shared containers Introduce SharedContainers (singleton MySQL + Mailpit per JVM) and AbstractIntegrationTest base class with @Transactional rollback. All 17 integration test classes now extend the base class, eliminating 60 @DirtiesContext annotations and ~15 redundant container instances. Test suite runtime drops from ~25-30 min to ~5 min. Closes #54 Co-Authored-By: Claude Opus 4.7 --- .../projectpulse/AbstractIntegrationTest.java | 40 +++++++++++++++++ .../ProjectPulseApplicationTests.java | 15 +------ .../team/projectpulse/SharedContainers.java | 27 ++++++++++++ .../activity/ActivityIntegrationTest.java | 25 +---------- .../course/CourseIntegrationTest.java | 40 ++--------------- .../evaluation/EvaluationIntegrationTest.java | 26 ++--------- .../instructor/InstructorIntegrationTest.java | 29 ++---------- .../collabration/CommentControllerTest.java | 25 +---------- .../ram/document/DocumentControllerTest.java | 15 +------ .../DocumentSectionControllerTest.java | 30 ++++--------- .../ArtifactLinkControllerTest.java | 18 +------- .../RequirementArtifactControllerTest.java | 24 +++------- .../ram/usecase/UseCaseControllerTest.java | 38 ++++++---------- .../rubric/CriterionIntegrationTest.java | 27 ++---------- .../rubric/RubricIntegrationTest.java | 29 ++---------- .../section/SectionIntegrationTest.java | 44 +++---------------- .../student/StudentIntegrationTest.java | 26 ++--------- .../system/WebConfigSpaForwardingTest.java | 18 +++++--- .../team/TeamIntegrationTest.java | 29 ++---------- .../user/UserIntegrationTest.java | 23 ++-------- 20 files changed, 148 insertions(+), 400 deletions(-) create mode 100644 backend/src/test/java/team/projectpulse/AbstractIntegrationTest.java create mode 100644 backend/src/test/java/team/projectpulse/SharedContainers.java diff --git a/backend/src/test/java/team/projectpulse/AbstractIntegrationTest.java b/backend/src/test/java/team/projectpulse/AbstractIntegrationTest.java new file mode 100644 index 0000000..3ab0fb5 --- /dev/null +++ b/backend/src/test/java/team/projectpulse/AbstractIntegrationTest.java @@ -0,0 +1,40 @@ +package team.projectpulse; + +import org.junit.jupiter.api.Tag; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.testcontainers.service.connection.ServiceConnection; +import org.springframework.boot.webmvc.test.autoconfigure.AutoConfigureMockMvc; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.context.DynamicPropertyRegistry; +import org.springframework.test.context.DynamicPropertySource; +import org.springframework.transaction.annotation.Transactional; +import org.testcontainers.containers.MySQLContainer; + +/** + * Base class for all integration tests. Provides: + * + */ +@SpringBootTest +@AutoConfigureMockMvc +@ActiveProfiles("dev") +@Tag("integration") +@Transactional +public abstract class AbstractIntegrationTest { + + @ServiceConnection + static final MySQLContainer mysql = SharedContainers.MYSQL; + + + @DynamicPropertySource + static void setProperties(DynamicPropertyRegistry registry) { + registry.add("spring.mail.host", SharedContainers.MAILPIT::getHost); + registry.add("spring.mail.port", () -> SharedContainers.MAILPIT.getMappedPort(1025)); + } + +} diff --git a/backend/src/test/java/team/projectpulse/ProjectPulseApplicationTests.java b/backend/src/test/java/team/projectpulse/ProjectPulseApplicationTests.java index 69e409b..f685c22 100644 --- a/backend/src/test/java/team/projectpulse/ProjectPulseApplicationTests.java +++ b/backend/src/test/java/team/projectpulse/ProjectPulseApplicationTests.java @@ -1,21 +1,8 @@ package team.projectpulse; import org.junit.jupiter.api.Test; -import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.boot.testcontainers.service.connection.ServiceConnection; -import org.testcontainers.containers.MySQLContainer; -import org.testcontainers.junit.jupiter.Container; -import org.testcontainers.junit.jupiter.Testcontainers; -import org.testcontainers.utility.DockerImageName; -@SpringBootTest -@Testcontainers -class ProjectPulseApplicationTests { - - @Container - @ServiceConnection - static MySQLContainer mysql = new MySQLContainer<>(DockerImageName.parse("mysql:8.0")); - +class ProjectPulseApplicationTests extends AbstractIntegrationTest { @Test void contextLoads() { diff --git a/backend/src/test/java/team/projectpulse/SharedContainers.java b/backend/src/test/java/team/projectpulse/SharedContainers.java new file mode 100644 index 0000000..9832def --- /dev/null +++ b/backend/src/test/java/team/projectpulse/SharedContainers.java @@ -0,0 +1,27 @@ +package team.projectpulse; + +import org.testcontainers.containers.GenericContainer; +import org.testcontainers.containers.MySQLContainer; +import org.testcontainers.utility.DockerImageName; + +/** + * Singleton containers shared by all integration tests. Started once per JVM — avoids + * spinning up a new MySQL/Mailpit container per test class. + */ +public final class SharedContainers { + + public static final MySQLContainer MYSQL = new MySQLContainer<>(DockerImageName.parse("mysql:8.0")); + + public static final GenericContainer MAILPIT = new GenericContainer<>(DockerImageName.parse("axllent/mailpit")) + .withExposedPorts(1025, 8025); + + static { + MYSQL.start(); + MAILPIT.start(); + } + + + private SharedContainers() { + } + +} diff --git a/backend/src/test/java/team/projectpulse/activity/ActivityIntegrationTest.java b/backend/src/test/java/team/projectpulse/activity/ActivityIntegrationTest.java index 1cb2eaa..04b9b4a 100644 --- a/backend/src/test/java/team/projectpulse/activity/ActivityIntegrationTest.java +++ b/backend/src/test/java/team/projectpulse/activity/ActivityIntegrationTest.java @@ -1,26 +1,17 @@ package team.projectpulse.activity; import tools.jackson.databind.json.JsonMapper; -import org.springframework.boot.testcontainers.service.connection.ServiceConnection; -import org.testcontainers.containers.MySQLContainer; -import org.testcontainers.junit.jupiter.Container; -import org.testcontainers.junit.jupiter.Testcontainers; -import org.testcontainers.utility.DockerImageName; +import team.projectpulse.AbstractIntegrationTest; import team.projectpulse.system.StatusCode; import org.hamcrest.Matchers; import org.json.JSONObject; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Tag; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; -import org.springframework.boot.webmvc.test.autoconfigure.AutoConfigureMockMvc; -import org.springframework.boot.test.context.SpringBootTest; import org.springframework.http.HttpHeaders; import org.springframework.http.MediaType; -import org.springframework.test.annotation.DirtiesContext; -import org.springframework.test.context.ActiveProfiles; import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.MvcResult; import org.springframework.test.web.servlet.ResultActions; @@ -36,13 +27,8 @@ import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; -@SpringBootTest -@Testcontainers -@AutoConfigureMockMvc @DisplayName("Integration tests for Activity API endpoints") -@Tag("integration") -@ActiveProfiles(value = "dev") -public class ActivityIntegrationTest { +public class ActivityIntegrationTest extends AbstractIntegrationTest { @Autowired MockMvc mockMvc; @@ -61,10 +47,6 @@ public class ActivityIntegrationTest { @Value("${api.endpoint.base-url}") String baseUrl; - @Container - @ServiceConnection - static MySQLContainer mysql = new MySQLContainer<>(DockerImageName.parse("mysql:8.0")); - @BeforeEach void setUp() throws Exception { ResultActions resultActions = this.mockMvc.perform(post(this.baseUrl + "/users/login").with(httpBasic("b.wei@abc.edu", "123456"))); // httpBasic() is from spring-security-test. @@ -228,7 +210,6 @@ void testStudentJanaFindActivityById() throws Exception { } @Test - @DirtiesContext(methodMode = DirtiesContext.MethodMode.AFTER_METHOD) void testStudentJohnAddActivity() throws Exception { // Given Map activityDto = new HashMap<>(); @@ -259,7 +240,6 @@ void testStudentJohnAddActivity() throws Exception { } @Test - @DirtiesContext(methodMode = DirtiesContext.MethodMode.AFTER_METHOD) void testStudentJohnUpdatesOwnActivity() throws Exception { // Given Map activityDto = new HashMap<>(); @@ -310,7 +290,6 @@ void testStudentUpdatesAnotherStudentsActivity() throws Exception { } @Test - @DirtiesContext(methodMode = DirtiesContext.MethodMode.AFTER_METHOD) void testStudentJohnDeletesOwnActivity() throws Exception { this.mockMvc.perform(delete(this.baseUrl + "/activities/1").accept(MediaType.APPLICATION_JSON).header(HttpHeaders.AUTHORIZATION, this.studentJohnToken)) .andExpect(jsonPath("$.flag").value(true)) diff --git a/backend/src/test/java/team/projectpulse/course/CourseIntegrationTest.java b/backend/src/test/java/team/projectpulse/course/CourseIntegrationTest.java index bd4ca7f..ac1f44d 100644 --- a/backend/src/test/java/team/projectpulse/course/CourseIntegrationTest.java +++ b/backend/src/test/java/team/projectpulse/course/CourseIntegrationTest.java @@ -1,34 +1,22 @@ package team.projectpulse.course; import tools.jackson.databind.json.JsonMapper; -import org.springframework.boot.testcontainers.service.connection.ServiceConnection; -import org.testcontainers.containers.MySQLContainer; +import team.projectpulse.AbstractIntegrationTest; import team.projectpulse.system.StatusCode; import org.hamcrest.Matchers; import org.json.JSONObject; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Tag; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; -import org.springframework.boot.webmvc.test.autoconfigure.AutoConfigureMockMvc; -import org.springframework.boot.test.context.SpringBootTest; import org.springframework.http.HttpHeaders; import org.springframework.http.MediaType; -import org.springframework.test.annotation.DirtiesContext; -import org.springframework.test.context.ActiveProfiles; -import org.springframework.test.context.DynamicPropertyRegistry; -import org.springframework.test.context.DynamicPropertySource; import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.MvcResult; import org.springframework.test.web.servlet.ResultActions; import org.springframework.util.LinkedMultiValueMap; import org.springframework.util.MultiValueMap; -import org.testcontainers.containers.GenericContainer; -import org.testcontainers.junit.jupiter.Container; -import org.testcontainers.junit.jupiter.Testcontainers; -import org.testcontainers.utility.DockerImageName; import java.util.HashMap; import java.util.Map; @@ -38,13 +26,8 @@ import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; -@SpringBootTest -@Testcontainers -@AutoConfigureMockMvc @DisplayName("Integration tests for Course API endpoints") -@Tag("integration") -@ActiveProfiles(value = "dev") -class CourseIntegrationTest { +class CourseIntegrationTest extends AbstractIntegrationTest { @Autowired MockMvc mockMvc; @@ -61,21 +44,6 @@ class CourseIntegrationTest { @Value("${api.endpoint.base-url}") String baseUrl; - @Container - @ServiceConnection - static MySQLContainer mysql = new MySQLContainer<>(DockerImageName.parse("mysql:8.0")); - - @Container - static final GenericContainer mailpit = new GenericContainer<>(DockerImageName.parse("axllent/mailpit")) - .withExposedPorts(1025, 8025); - - - @DynamicPropertySource - static void setMailpitProperties(DynamicPropertyRegistry registry) { - registry.add("spring.mail.host", mailpit::getHost); - registry.add("spring.mail.port", () -> mailpit.getMappedPort(1025)); - } - @BeforeEach void setUp() throws Exception { ResultActions resultActions = this.mockMvc.perform(post(this.baseUrl + "/users/login").with(httpBasic("b.wei@abc.edu", "123456"))); // httpBasic() is from spring-security-test. @@ -185,7 +153,7 @@ void adminTimFindCourseById() throws Exception { } @Test - @DirtiesContext(methodMode = DirtiesContext.MethodMode.AFTER_METHOD) + void adminBingyangAddCourse() throws Exception { // Given Map courseDto = new HashMap<>(); @@ -219,7 +187,7 @@ void instructorBillAddCourse() throws Exception { } @Test - @DirtiesContext(methodMode = DirtiesContext.MethodMode.AFTER_METHOD) + void adminBingyangUpdateCourse() throws Exception { // Given Map courseDto = new HashMap<>(); diff --git a/backend/src/test/java/team/projectpulse/evaluation/EvaluationIntegrationTest.java b/backend/src/test/java/team/projectpulse/evaluation/EvaluationIntegrationTest.java index 82546e9..106a88f 100644 --- a/backend/src/test/java/team/projectpulse/evaluation/EvaluationIntegrationTest.java +++ b/backend/src/test/java/team/projectpulse/evaluation/EvaluationIntegrationTest.java @@ -1,12 +1,8 @@ package team.projectpulse.evaluation; import tools.jackson.databind.json.JsonMapper; -import org.springframework.boot.testcontainers.service.connection.ServiceConnection; import org.springframework.test.context.bean.override.mockito.MockitoSpyBean; -import org.testcontainers.containers.MySQLContainer; -import org.testcontainers.junit.jupiter.Container; -import org.testcontainers.junit.jupiter.Testcontainers; -import org.testcontainers.utility.DockerImageName; +import team.projectpulse.AbstractIntegrationTest; import team.projectpulse.evaluation.dto.PeerEvaluationDto; import team.projectpulse.evaluation.dto.RatingDto; import team.projectpulse.system.StatusCode; @@ -14,16 +10,11 @@ import org.json.JSONObject; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Tag; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; -import org.springframework.boot.webmvc.test.autoconfigure.AutoConfigureMockMvc; -import org.springframework.boot.test.context.SpringBootTest; import org.springframework.http.HttpHeaders; import org.springframework.http.MediaType; -import org.springframework.test.annotation.DirtiesContext; -import org.springframework.test.context.ActiveProfiles; import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.MvcResult; import org.springframework.test.web.servlet.ResultActions; @@ -41,13 +32,8 @@ import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; -@SpringBootTest -@Testcontainers -@AutoConfigureMockMvc @DisplayName("Integration tests for Evaluation API endpoints") -@Tag("integration") -@ActiveProfiles(value = "dev") -public class EvaluationIntegrationTest { +public class EvaluationIntegrationTest extends AbstractIntegrationTest { /** * @MockitoSpyBean creates a Mockito spy and registers it as the active Spring bean. @@ -74,10 +60,6 @@ public class EvaluationIntegrationTest { @Value("${api.endpoint.base-url}") String baseUrl; - @Container - @ServiceConnection - static MySQLContainer mysql = new MySQLContainer<>(DockerImageName.parse("mysql:8.0")); - @BeforeEach void setUp() throws Exception { @@ -101,7 +83,7 @@ void setUp() throws Exception { } @Test - @DirtiesContext(methodMode = DirtiesContext.MethodMode.AFTER_METHOD) + void testStudentJohnAddEvaluation() throws Exception { List ratingDtos = List.of( new RatingDto(null, 1, 4.0), @@ -265,7 +247,7 @@ void testEvaluatorJohnGetsEvaluationsByWrongEvaluatorIdAndWeek() throws Exceptio } @Test - @DirtiesContext(methodMode = DirtiesContext.MethodMode.AFTER_METHOD) + void testEvaluatorJohnUpdatesOwnEvaluation() throws Exception { List ratingDtos = List.of( new RatingDto(1, 1, 4.0), diff --git a/backend/src/test/java/team/projectpulse/instructor/InstructorIntegrationTest.java b/backend/src/test/java/team/projectpulse/instructor/InstructorIntegrationTest.java index 254c13f..9393b1c 100644 --- a/backend/src/test/java/team/projectpulse/instructor/InstructorIntegrationTest.java +++ b/backend/src/test/java/team/projectpulse/instructor/InstructorIntegrationTest.java @@ -1,31 +1,22 @@ package team.projectpulse.instructor; import tools.jackson.databind.json.JsonMapper; -import org.springframework.boot.testcontainers.service.connection.ServiceConnection; -import org.testcontainers.containers.MySQLContainer; +import team.projectpulse.AbstractIntegrationTest; import team.projectpulse.system.StatusCode; import org.hamcrest.Matchers; import org.json.JSONObject; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Tag; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; -import org.springframework.boot.webmvc.test.autoconfigure.AutoConfigureMockMvc; -import org.springframework.boot.test.context.SpringBootTest; import org.springframework.http.HttpHeaders; import org.springframework.http.MediaType; -import org.springframework.test.annotation.DirtiesContext; -import org.springframework.test.context.ActiveProfiles; import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.MvcResult; import org.springframework.test.web.servlet.ResultActions; import org.springframework.util.LinkedMultiValueMap; import org.springframework.util.MultiValueMap; -import org.testcontainers.junit.jupiter.Container; -import org.testcontainers.junit.jupiter.Testcontainers; -import org.testcontainers.utility.DockerImageName; import java.util.HashMap; import java.util.Map; @@ -35,13 +26,8 @@ import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; -@SpringBootTest -@Testcontainers -@AutoConfigureMockMvc @DisplayName("Integration tests for Instructor API endpoints") -@Tag("integration") -@ActiveProfiles(value = "dev") -class InstructorIntegrationTest { +class InstructorIntegrationTest extends AbstractIntegrationTest { @Autowired MockMvc mockMvc; @@ -58,10 +44,6 @@ class InstructorIntegrationTest { @Value("${api.endpoint.base-url}") String baseUrl; - @Container - @ServiceConnection - static MySQLContainer mysql = new MySQLContainer<>(DockerImageName.parse("mysql:8.0")); - @BeforeEach void setUp() throws Exception { @@ -140,7 +122,6 @@ void testInstructorFindAnotherInstructorsInfoById() throws Exception { } @Test - @DirtiesContext(methodMode = DirtiesContext.MethodMode.AFTER_METHOD) void testAddInstructor() throws Exception { Map instructorDto = new HashMap<>(); instructorDto.put("username", "e.musk@abc.edu"); @@ -243,7 +224,6 @@ void testAddInstructorWithWrongRole() throws Exception { } @Test - @DirtiesContext(methodMode = DirtiesContext.MethodMode.AFTER_METHOD) void testAdminBingyangUpdatesOwnInfo() throws Exception { Map instructorDto = new HashMap<>(); instructorDto.put("username", "b.wei@abc.edu"); @@ -271,7 +251,6 @@ void testAdminBingyangUpdatesOwnInfo() throws Exception { } @Test - @DirtiesContext(methodMode = DirtiesContext.MethodMode.AFTER_METHOD) void testInstructorBillUpdatesOwnInfo() throws Exception { Map instructorDto = new HashMap<>(); instructorDto.put("username", "b.gates@abc.edu"); @@ -317,7 +296,6 @@ void testInstructorBillUpdatesAnotherInstructorsInfo() throws Exception { } @Test - @DirtiesContext(methodMode = DirtiesContext.MethodMode.AFTER_METHOD) void testAdminBingyangSetDefaultSection() throws Exception { // When and then this.mockMvc.perform(put(this.baseUrl + "/instructors/sections/1/default").accept(MediaType.APPLICATION_JSON).header(HttpHeaders.AUTHORIZATION, this.adminBingyangToken)) @@ -360,7 +338,6 @@ void testInstructorBillSetDefaultSectionDoesNotBelongToInstructor() throws Excep } @Test - @DirtiesContext(methodMode = DirtiesContext.MethodMode.AFTER_METHOD) void testAdminBingyangSetDefaultCourse() throws Exception { // When and then this.mockMvc.perform(put(this.baseUrl + "/instructors/courses/2/default").accept(MediaType.APPLICATION_JSON).header(HttpHeaders.AUTHORIZATION, this.adminBingyangToken)) @@ -386,4 +363,4 @@ void testInstructorBillSetDefaultCourseDoesNotBelongToInstructor() throws Except .andExpect(jsonPath("$.data").value("Access Denied")); } -} \ No newline at end of file +} diff --git a/backend/src/test/java/team/projectpulse/ram/collabration/CommentControllerTest.java b/backend/src/test/java/team/projectpulse/ram/collabration/CommentControllerTest.java index 1ecc176..3dc24f5 100644 --- a/backend/src/test/java/team/projectpulse/ram/collabration/CommentControllerTest.java +++ b/backend/src/test/java/team/projectpulse/ram/collabration/CommentControllerTest.java @@ -6,20 +6,12 @@ import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; -import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.boot.testcontainers.service.connection.ServiceConnection; -import org.springframework.boot.webmvc.test.autoconfigure.AutoConfigureMockMvc; import org.springframework.http.HttpHeaders; import org.springframework.http.MediaType; -import org.springframework.test.annotation.DirtiesContext; -import org.springframework.test.context.ActiveProfiles; import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.MvcResult; import org.springframework.test.web.servlet.ResultActions; -import org.testcontainers.containers.MySQLContainer; -import org.testcontainers.junit.jupiter.Container; -import org.testcontainers.junit.jupiter.Testcontainers; -import org.testcontainers.utility.DockerImageName; +import team.projectpulse.AbstractIntegrationTest; import team.projectpulse.system.StatusCode; import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.httpBasic; @@ -27,11 +19,7 @@ import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; -@SpringBootTest -@Testcontainers -@AutoConfigureMockMvc -@ActiveProfiles(value = "dev") -public class CommentControllerTest { +public class CommentControllerTest extends AbstractIntegrationTest { @Autowired MockMvc mockMvc; @@ -48,10 +36,6 @@ public class CommentControllerTest { @Value("${api.endpoint.base-url}") String baseUrl; - @Container - @ServiceConnection - static MySQLContainer mysql = new MySQLContainer<>(DockerImageName.parse("mysql:8.0")); - @BeforeEach void setUp() throws Exception { ResultActions resultActions = this.mockMvc.perform(post(this.baseUrl + "/users/login").with(httpBasic("b.wei@abc.edu", "123456"))); // httpBasic() is from spring-security-test. @@ -107,7 +91,6 @@ void listCommentThreadsForDocument_NotSameTeam() throws Exception { } @Test - @DirtiesContext(methodMode = DirtiesContext.MethodMode.AFTER_METHOD) void createCommentThreadForDocument() throws Exception { String json = """ { @@ -141,7 +124,6 @@ void listCommentThreadsForDocumentSection() throws Exception { } @Test - @DirtiesContext(methodMode = DirtiesContext.MethodMode.AFTER_METHOD) void createCommentThreadForDocumentSection() throws Exception { String json = """ { @@ -206,7 +188,6 @@ void getCommentThread() throws Exception { } @Test - @DirtiesContext(methodMode = DirtiesContext.MethodMode.AFTER_METHOD) void addCommentToACommentThread() throws Exception { String addCommentJson = """ { "content": "Sure - but let's define it in the glossary." } @@ -293,7 +274,6 @@ void updateComment_NotSameTeam() throws Exception { } @Test - @DirtiesContext(methodMode = DirtiesContext.MethodMode.AFTER_METHOD) void deleteComment() throws Exception { this.mockMvc.perform(delete(this.baseUrl + "/teams/1/comment-threads/1/comments/2") .accept(MediaType.APPLICATION_JSON) @@ -311,7 +291,6 @@ void deleteComment() throws Exception { } @Test - @DirtiesContext(methodMode = DirtiesContext.MethodMode.AFTER_METHOD) void deleteCommentThread() throws Exception { this.mockMvc.perform(delete(this.baseUrl + "/teams/1/comment-threads/1") .accept(MediaType.APPLICATION_JSON) diff --git a/backend/src/test/java/team/projectpulse/ram/document/DocumentControllerTest.java b/backend/src/test/java/team/projectpulse/ram/document/DocumentControllerTest.java index 34c1666..aa32afe 100644 --- a/backend/src/test/java/team/projectpulse/ram/document/DocumentControllerTest.java +++ b/backend/src/test/java/team/projectpulse/ram/document/DocumentControllerTest.java @@ -6,17 +6,12 @@ import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; -import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.boot.testcontainers.service.connection.ServiceConnection; -import org.springframework.boot.webmvc.test.autoconfigure.AutoConfigureMockMvc; import org.springframework.http.HttpHeaders; import org.springframework.http.MediaType; import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.MvcResult; import org.springframework.test.web.servlet.ResultActions; -import org.testcontainers.containers.MySQLContainer; -import org.testcontainers.junit.jupiter.Container; -import org.testcontainers.utility.DockerImageName; +import team.projectpulse.AbstractIntegrationTest; import team.projectpulse.system.StatusCode; import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.httpBasic; @@ -24,9 +19,7 @@ import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; -@SpringBootTest -@AutoConfigureMockMvc -public class DocumentControllerTest { +public class DocumentControllerTest extends AbstractIntegrationTest { @Autowired MockMvc mockMvc; @@ -44,10 +37,6 @@ public class DocumentControllerTest { @Value("${api.endpoint.base-url}") String baseUrl; - @Container - @ServiceConnection - static MySQLContainer mysql = new MySQLContainer<>(DockerImageName.parse("mysql:8.0")); - @BeforeEach void setUp() throws Exception { ResultActions resultActions = this.mockMvc.perform(post(this.baseUrl + "/users/login").with(httpBasic("b.wei@abc.edu", "123456"))); // httpBasic() is from spring-security-test. diff --git a/backend/src/test/java/team/projectpulse/ram/document/DocumentSectionControllerTest.java b/backend/src/test/java/team/projectpulse/ram/document/DocumentSectionControllerTest.java index 3ee5bd1..89b5d9b 100644 --- a/backend/src/test/java/team/projectpulse/ram/document/DocumentSectionControllerTest.java +++ b/backend/src/test/java/team/projectpulse/ram/document/DocumentSectionControllerTest.java @@ -6,18 +6,12 @@ import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; -import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.boot.testcontainers.service.connection.ServiceConnection; -import org.springframework.boot.webmvc.test.autoconfigure.AutoConfigureMockMvc; import org.springframework.http.HttpHeaders; import org.springframework.http.MediaType; -import org.springframework.test.annotation.DirtiesContext; import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.MvcResult; import org.springframework.test.web.servlet.ResultActions; -import org.testcontainers.containers.MySQLContainer; -import org.testcontainers.junit.jupiter.Container; -import org.testcontainers.utility.DockerImageName; +import team.projectpulse.AbstractIntegrationTest; import team.projectpulse.system.StatusCode; import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.httpBasic; @@ -25,9 +19,7 @@ import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; -@SpringBootTest -@AutoConfigureMockMvc -public class DocumentSectionControllerTest { +public class DocumentSectionControllerTest extends AbstractIntegrationTest { @Autowired MockMvc mockMvc; @@ -45,10 +37,6 @@ public class DocumentSectionControllerTest { @Value("${api.endpoint.base-url}") String baseUrl; - @Container - @ServiceConnection - static MySQLContainer mysql = new MySQLContainer<>(DockerImageName.parse("mysql:8.0")); - @BeforeEach void setUp() throws Exception { ResultActions resultActions = this.mockMvc.perform(post(this.baseUrl + "/users/login").with(httpBasic("b.wei@abc.edu", "123456"))); // httpBasic() is from spring-security-test. @@ -95,7 +83,7 @@ void findDocumentSectionById() throws Exception { } @Test - @DirtiesContext(methodMode = DirtiesContext.MethodMode.AFTER_METHOD) + void updateDocumentSectionContent1() throws Exception { String json = """ { @@ -165,7 +153,7 @@ void updateDocumentSectionContent1() throws Exception { } @Test - @DirtiesContext(methodMode = DirtiesContext.MethodMode.AFTER_METHOD) + void updateDocumentSectionContent2() throws Exception { String json = """ { @@ -214,7 +202,7 @@ void updateDocumentSectionContent2() throws Exception { } @Test - @DirtiesContext(methodMode = DirtiesContext.MethodMode.AFTER_METHOD) + void updateDocumentSectionWithoutRequirementsArtifacts() throws Exception { String json = """ { @@ -250,7 +238,7 @@ void updateDocumentSectionWithoutRequirementsArtifacts() throws Exception { } @Test - @DirtiesContext(methodMode = DirtiesContext.MethodMode.AFTER_METHOD) + void updateDocumentSectionWithoutRequirementsArtifacts_LockedByAnotherUser() throws Exception { String json = """ { @@ -284,7 +272,7 @@ void updateDocumentSectionWithoutRequirementsArtifacts_LockedByAnotherUser() thr } @Test - @DirtiesContext(methodMode = DirtiesContext.MethodMode.AFTER_METHOD) + void updateDocumentSectionWithoutRequirementsArtifacts_NotLocked() throws Exception { int version = fetchSectionVersion(1, 1, 2, this.studentJohnToken); String json = """ @@ -383,7 +371,7 @@ void lockDocumentSection_AlreadyLocked() throws Exception { } @Test - @DirtiesContext(methodMode = DirtiesContext.MethodMode.AFTER_METHOD) + void unlockDocumentSection() throws Exception { this.mockMvc.perform(delete(this.baseUrl + "/teams/1/documents/1/document-sections/1/lock").accept(MediaType.APPLICATION_JSON).header(HttpHeaders.AUTHORIZATION, this.studentJohnToken)) .andExpect(jsonPath("$.flag").value(true)) @@ -400,7 +388,7 @@ void unlockDocumentSection_DifferentOwner() throws Exception { } @Test - @DirtiesContext(methodMode = DirtiesContext.MethodMode.AFTER_METHOD) + void unlockDocumentSection_Instructor() throws Exception { this.mockMvc.perform(delete(this.baseUrl + "/teams/1/documents/1/document-sections/1/lock").accept(MediaType.APPLICATION_JSON).header(HttpHeaders.AUTHORIZATION, this.adminBingyangToken)) .andExpect(jsonPath("$.flag").value(true)) diff --git a/backend/src/test/java/team/projectpulse/ram/requirement/ArtifactLinkControllerTest.java b/backend/src/test/java/team/projectpulse/ram/requirement/ArtifactLinkControllerTest.java index 60142bf..7d30cb8 100644 --- a/backend/src/test/java/team/projectpulse/ram/requirement/ArtifactLinkControllerTest.java +++ b/backend/src/test/java/team/projectpulse/ram/requirement/ArtifactLinkControllerTest.java @@ -6,18 +6,12 @@ import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; -import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.boot.testcontainers.service.connection.ServiceConnection; -import org.springframework.boot.webmvc.test.autoconfigure.AutoConfigureMockMvc; import org.springframework.http.HttpHeaders; import org.springframework.http.MediaType; -import org.springframework.test.annotation.DirtiesContext; import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.MvcResult; import org.springframework.test.web.servlet.ResultActions; -import org.testcontainers.containers.MySQLContainer; -import org.testcontainers.junit.jupiter.Container; -import org.testcontainers.utility.DockerImageName; +import team.projectpulse.AbstractIntegrationTest; import team.projectpulse.system.StatusCode; import static org.hamcrest.Matchers.hasSize; @@ -26,9 +20,7 @@ import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; -@SpringBootTest -@AutoConfigureMockMvc -public class ArtifactLinkControllerTest { +public class ArtifactLinkControllerTest extends AbstractIntegrationTest { @Autowired MockMvc mockMvc; @@ -45,10 +37,6 @@ public class ArtifactLinkControllerTest { @Value("${api.endpoint.base-url}") String baseUrl; - @Container - @ServiceConnection - static MySQLContainer mysql = new MySQLContainer<>(DockerImageName.parse("mysql:8.0")); - @BeforeEach void setUp() throws Exception { ResultActions resultActions = this.mockMvc.perform(post(this.baseUrl + "/users/login").with(httpBasic("b.wei@abc.edu", "123456"))); // httpBasic() is from spring-security-test. @@ -117,7 +105,6 @@ void findArtifactLinkById_NotSameTeam() throws Exception { } @Test - @DirtiesContext(methodMode = DirtiesContext.MethodMode.AFTER_METHOD) void addArtifactLink() throws Exception { String json = """ { @@ -169,7 +156,6 @@ void updateArtifactLink() throws Exception { } @Test - @DirtiesContext(methodMode = DirtiesContext.MethodMode.AFTER_METHOD) void deleteArtifactLink() throws Exception { this.mockMvc.perform(delete(this.baseUrl + "/teams/1/artifact-links/1").accept(MediaType.APPLICATION_JSON).header(HttpHeaders.AUTHORIZATION, this.studentEricToken)) .andExpect(jsonPath("$.flag").value(true)) diff --git a/backend/src/test/java/team/projectpulse/ram/requirement/RequirementArtifactControllerTest.java b/backend/src/test/java/team/projectpulse/ram/requirement/RequirementArtifactControllerTest.java index 9ba0c03..235cb3f 100644 --- a/backend/src/test/java/team/projectpulse/ram/requirement/RequirementArtifactControllerTest.java +++ b/backend/src/test/java/team/projectpulse/ram/requirement/RequirementArtifactControllerTest.java @@ -6,18 +6,12 @@ import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; -import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.boot.testcontainers.service.connection.ServiceConnection; -import org.springframework.boot.webmvc.test.autoconfigure.AutoConfigureMockMvc; import org.springframework.http.HttpHeaders; import org.springframework.http.MediaType; -import org.springframework.test.annotation.DirtiesContext; import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.MvcResult; import org.springframework.test.web.servlet.ResultActions; -import org.testcontainers.containers.MySQLContainer; -import org.testcontainers.junit.jupiter.Container; -import org.testcontainers.utility.DockerImageName; +import team.projectpulse.AbstractIntegrationTest; import team.projectpulse.system.StatusCode; import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.httpBasic; @@ -25,9 +19,7 @@ import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; -@SpringBootTest -@AutoConfigureMockMvc -public class RequirementArtifactControllerTest { +public class RequirementArtifactControllerTest extends AbstractIntegrationTest { @Autowired MockMvc mockMvc; @@ -45,10 +37,6 @@ public class RequirementArtifactControllerTest { @Value("${api.endpoint.base-url}") String baseUrl; - @Container - @ServiceConnection - static MySQLContainer mysql = new MySQLContainer<>(DockerImageName.parse("mysql:8.0")); - @BeforeEach void setUp() throws Exception { ResultActions resultActions = this.mockMvc.perform(post(this.baseUrl + "/users/login").with(httpBasic("b.wei@abc.edu", "123456"))); // httpBasic() is from spring-security-test. @@ -83,7 +71,7 @@ void setUp() throws Exception { } @Test - @DirtiesContext(methodMode = DirtiesContext.MethodMode.BEFORE_METHOD) + void findRequirementArtifactsByCriteria1() throws Exception { String json = """ { @@ -101,7 +89,7 @@ void findRequirementArtifactsByCriteria1() throws Exception { } @Test - @DirtiesContext(methodMode = DirtiesContext.MethodMode.BEFORE_METHOD) + void findRequirementArtifactsByCriteria2() throws Exception { String json = """ { @@ -116,7 +104,7 @@ void findRequirementArtifactsByCriteria2() throws Exception { } @Test - @DirtiesContext(methodMode = DirtiesContext.MethodMode.BEFORE_METHOD) + void findRequirementArtifactsByCriteria3() throws Exception { String json = """ { @@ -201,7 +189,7 @@ void addRequirementArtifact_NotSameTeam() throws Exception { } @Test - @DirtiesContext(methodMode = DirtiesContext.MethodMode.AFTER_METHOD) + void updateRequirementArtifact() throws Exception { String json = """ { diff --git a/backend/src/test/java/team/projectpulse/ram/usecase/UseCaseControllerTest.java b/backend/src/test/java/team/projectpulse/ram/usecase/UseCaseControllerTest.java index 6962bc3..08d9b12 100644 --- a/backend/src/test/java/team/projectpulse/ram/usecase/UseCaseControllerTest.java +++ b/backend/src/test/java/team/projectpulse/ram/usecase/UseCaseControllerTest.java @@ -6,18 +6,12 @@ import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; -import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.boot.testcontainers.service.connection.ServiceConnection; -import org.springframework.boot.webmvc.test.autoconfigure.AutoConfigureMockMvc; import org.springframework.http.HttpHeaders; import org.springframework.http.MediaType; -import org.springframework.test.annotation.DirtiesContext; import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.MvcResult; import org.springframework.test.web.servlet.ResultActions; -import org.testcontainers.containers.MySQLContainer; -import org.testcontainers.junit.jupiter.Container; -import org.testcontainers.utility.DockerImageName; +import team.projectpulse.AbstractIntegrationTest; import team.projectpulse.system.StatusCode; import static org.hamcrest.Matchers.hasSize; @@ -26,9 +20,7 @@ import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; -@SpringBootTest -@AutoConfigureMockMvc -class UseCaseControllerTest { +class UseCaseControllerTest extends AbstractIntegrationTest { @Autowired MockMvc mockMvc; @@ -46,10 +38,6 @@ class UseCaseControllerTest { @Value("${api.endpoint.base-url}") String baseUrl; - @Container - @ServiceConnection - static MySQLContainer mysql = new MySQLContainer<>(DockerImageName.parse("mysql:8.0")); - @BeforeEach void setUp() throws Exception { ResultActions resultActions = this.mockMvc.perform(post(this.baseUrl + "/users/login").with(httpBasic("b.wei@abc.edu", "123456"))); // httpBasic() is from spring-security-test. @@ -95,7 +83,7 @@ void findUseCaseById() throws Exception { } @Test - @DirtiesContext(methodMode = DirtiesContext.MethodMode.AFTER_METHOD) + void addUseCase() throws Exception { // Given String json = """ @@ -233,7 +221,7 @@ void addUseCase() throws Exception { } @Test - @DirtiesContext(methodMode = DirtiesContext.MethodMode.AFTER_METHOD) + void addUseCase_NotSameTeam() throws Exception { // Given String json = """ @@ -368,7 +356,7 @@ void addUseCase_NotSameTeam() throws Exception { } @Test - @DirtiesContext(methodMode = DirtiesContext.MethodMode.AFTER_METHOD) + void updateUseCaseWithRewordedContent() throws Exception { lockUseCase(1, 3, this.studentEricToken); int version = fetchUseCaseVersion(1, 3, this.studentEricToken); @@ -543,7 +531,7 @@ void updateUseCaseWithRewordedContent() throws Exception { } @Test - @DirtiesContext(methodMode = DirtiesContext.MethodMode.AFTER_METHOD) + void updateUseCaseWithNewSteps() throws Exception { lockUseCase(1, 3, this.studentJohnToken); int version = fetchUseCaseVersion(1, 3, this.studentJohnToken); @@ -742,7 +730,7 @@ void updateUseCaseWithNewSteps() throws Exception { } @Test - @DirtiesContext(methodMode = DirtiesContext.MethodMode.AFTER_METHOD) + void updateUseCaseWithDeletedSteps() throws Exception { lockUseCase(1, 3, this.adminBingyangToken); int version = fetchUseCaseVersion(1, 3, this.adminBingyangToken); @@ -906,7 +894,7 @@ void updateUseCaseWithDeletedSteps() throws Exception { } @Test - @DirtiesContext(methodMode = DirtiesContext.MethodMode.AFTER_METHOD) + void updateUseCaseWithDeletedSteps_NotSameTeam() throws Exception { // Given String json = """ @@ -1073,7 +1061,7 @@ void getUseCaseLockStatus() throws Exception { } @Test - @DirtiesContext(methodMode = DirtiesContext.MethodMode.AFTER_METHOD) + void lockUseCase() throws Exception { lockUseCase(1, 3, this.studentJohnToken); @@ -1087,7 +1075,7 @@ void lockUseCase() throws Exception { } @Test - @DirtiesContext(methodMode = DirtiesContext.MethodMode.AFTER_METHOD) + void lockUseCase_AlreadyLocked() throws Exception { lockUseCase(1, 3, this.studentJohnToken); @@ -1107,7 +1095,7 @@ void lockUseCase_AlreadyLocked() throws Exception { } @Test - @DirtiesContext(methodMode = DirtiesContext.MethodMode.AFTER_METHOD) + void unlockUseCase() throws Exception { lockUseCase(1, 3, this.studentJohnToken); @@ -1120,7 +1108,7 @@ void unlockUseCase() throws Exception { } @Test - @DirtiesContext(methodMode = DirtiesContext.MethodMode.AFTER_METHOD) + void unlockUseCase_DifferentOwner() throws Exception { lockUseCase(1, 3, this.studentJohnToken); @@ -1133,7 +1121,7 @@ void unlockUseCase_DifferentOwner() throws Exception { } @Test - @DirtiesContext(methodMode = DirtiesContext.MethodMode.AFTER_METHOD) + void unlockUseCase_Instructor() throws Exception { lockUseCase(1, 3, this.studentJohnToken); diff --git a/backend/src/test/java/team/projectpulse/rubric/CriterionIntegrationTest.java b/backend/src/test/java/team/projectpulse/rubric/CriterionIntegrationTest.java index a88507c..53bb07e 100644 --- a/backend/src/test/java/team/projectpulse/rubric/CriterionIntegrationTest.java +++ b/backend/src/test/java/team/projectpulse/rubric/CriterionIntegrationTest.java @@ -1,26 +1,17 @@ package team.projectpulse.rubric; import tools.jackson.databind.json.JsonMapper; -import org.springframework.boot.testcontainers.service.connection.ServiceConnection; -import org.testcontainers.containers.MySQLContainer; -import org.testcontainers.junit.jupiter.Container; -import org.testcontainers.junit.jupiter.Testcontainers; -import org.testcontainers.utility.DockerImageName; +import team.projectpulse.AbstractIntegrationTest; import team.projectpulse.system.StatusCode; import org.hamcrest.Matchers; import org.json.JSONObject; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Tag; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; -import org.springframework.boot.webmvc.test.autoconfigure.AutoConfigureMockMvc; -import org.springframework.boot.test.context.SpringBootTest; import org.springframework.http.HttpHeaders; import org.springframework.http.MediaType; -import org.springframework.test.annotation.DirtiesContext; -import org.springframework.test.context.ActiveProfiles; import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.MvcResult; import org.springframework.test.web.servlet.ResultActions; @@ -35,13 +26,8 @@ import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; -@SpringBootTest -@Testcontainers -@AutoConfigureMockMvc @DisplayName("Integration tests for Criterion API endpoints") -@Tag("integration") -@ActiveProfiles(value = "dev") -class CriterionIntegrationTest { +class CriterionIntegrationTest extends AbstractIntegrationTest { @Autowired MockMvc mockMvc; @@ -57,10 +43,6 @@ class CriterionIntegrationTest { @Value("${api.endpoint.base-url}") String baseUrl; - @Container - @ServiceConnection - static MySQLContainer mysql = new MySQLContainer<>(DockerImageName.parse("mysql:8.0")); - @BeforeEach void setUp() throws Exception { @@ -136,7 +118,6 @@ void adminTimFindCriterionById() throws Exception { } @Test - @DirtiesContext(methodMode = DirtiesContext.MethodMode.AFTER_METHOD) void adminBingyangAddCriterion() throws Exception { // Given Map criterionDto = new HashMap<>(); @@ -159,7 +140,6 @@ void adminBingyangAddCriterion() throws Exception { } @Test - @DirtiesContext(methodMode = DirtiesContext.MethodMode.AFTER_METHOD) void adminBingyangUpdateCriterion() throws Exception { // Given Map criterionDto = new HashMap<>(); @@ -181,7 +161,6 @@ void adminBingyangUpdateCriterion() throws Exception { } @Test - @DirtiesContext(methodMode = DirtiesContext.MethodMode.AFTER_METHOD) void adminTimUpdateCriterion() throws Exception { // Given Map criterionDto = new HashMap<>(); @@ -205,4 +184,4 @@ void adminTimUpdateCriterion() throws Exception { // .andExpect(jsonPath("$.message").value("Delete peer evaluation criterion successfully")); // } -} \ No newline at end of file +} diff --git a/backend/src/test/java/team/projectpulse/rubric/RubricIntegrationTest.java b/backend/src/test/java/team/projectpulse/rubric/RubricIntegrationTest.java index 533a5e1..ac54ad6 100644 --- a/backend/src/test/java/team/projectpulse/rubric/RubricIntegrationTest.java +++ b/backend/src/test/java/team/projectpulse/rubric/RubricIntegrationTest.java @@ -1,26 +1,17 @@ package team.projectpulse.rubric; import tools.jackson.databind.json.JsonMapper; -import org.springframework.boot.testcontainers.service.connection.ServiceConnection; -import org.testcontainers.containers.MySQLContainer; -import org.testcontainers.junit.jupiter.Container; -import org.testcontainers.junit.jupiter.Testcontainers; -import org.testcontainers.utility.DockerImageName; +import team.projectpulse.AbstractIntegrationTest; import team.projectpulse.system.StatusCode; import org.hamcrest.Matchers; import org.json.JSONObject; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Tag; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; -import org.springframework.boot.webmvc.test.autoconfigure.AutoConfigureMockMvc; -import org.springframework.boot.test.context.SpringBootTest; import org.springframework.http.HttpHeaders; import org.springframework.http.MediaType; -import org.springframework.test.annotation.DirtiesContext; -import org.springframework.test.context.ActiveProfiles; import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.MvcResult; import org.springframework.test.web.servlet.ResultActions; @@ -35,13 +26,8 @@ import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; -@SpringBootTest -@Testcontainers -@AutoConfigureMockMvc @DisplayName("Integration tests for Rubric API endpoints") -@Tag("integration") -@ActiveProfiles(value = "dev") -class RubricIntegrationTest { +class RubricIntegrationTest extends AbstractIntegrationTest { @Autowired MockMvc mockMvc; @@ -60,10 +46,6 @@ class RubricIntegrationTest { @Value("${api.endpoint.base-url}") String baseUrl; - @Container - @ServiceConnection - static MySQLContainer mysql = new MySQLContainer<>(DockerImageName.parse("mysql:8.0")); - @BeforeEach void setUp() throws Exception { @@ -171,7 +153,6 @@ void studentJanaFindRubricById() throws Exception { } @Test - @DirtiesContext(methodMode = DirtiesContext.MethodMode.AFTER_METHOD) void addRubric() throws Exception { // Given Map rubricDto = new HashMap<>(); @@ -190,7 +171,6 @@ void addRubric() throws Exception { } @Test - @DirtiesContext(methodMode = DirtiesContext.MethodMode.AFTER_METHOD) void adminBingyangUpdateRubric() throws Exception { // Given Map rubricDto = new HashMap<>(); @@ -209,7 +189,6 @@ void adminBingyangUpdateRubric() throws Exception { } @Test - @DirtiesContext(methodMode = DirtiesContext.MethodMode.AFTER_METHOD) void adminTimUpdateRubric() throws Exception { // Given Map rubricDto = new HashMap<>(); @@ -229,7 +208,6 @@ void adminTimUpdateRubric() throws Exception { // } @Test - @DirtiesContext(methodMode = DirtiesContext.MethodMode.AFTER_METHOD) void assignCriterionToRubric() throws Exception { this.mockMvc.perform(put(this.baseUrl + "/rubrics/1/criteria/7").contentType(MediaType.APPLICATION_JSON).accept(MediaType.APPLICATION_JSON).header(HttpHeaders.AUTHORIZATION, this.adminBingyangToken)) .andExpect(jsonPath("$.flag").value(true)) @@ -254,7 +232,6 @@ void assignCriterionToRubricNotInSameCourse() throws Exception { } @Test - @DirtiesContext(methodMode = DirtiesContext.MethodMode.AFTER_METHOD) void removeCriterionFromRubric() throws Exception { this.mockMvc.perform(delete(this.baseUrl + "/rubrics/1/criteria/6").contentType(MediaType.APPLICATION_JSON).accept(MediaType.APPLICATION_JSON).header(HttpHeaders.AUTHORIZATION, this.adminBingyangToken)) .andExpect(jsonPath("$.flag").value(true)) @@ -279,4 +256,4 @@ void removeCriterionFromRubricNotInSameCourse() throws Exception { } -} \ No newline at end of file +} diff --git a/backend/src/test/java/team/projectpulse/section/SectionIntegrationTest.java b/backend/src/test/java/team/projectpulse/section/SectionIntegrationTest.java index 6f1037b..6a3e6fc 100644 --- a/backend/src/test/java/team/projectpulse/section/SectionIntegrationTest.java +++ b/backend/src/test/java/team/projectpulse/section/SectionIntegrationTest.java @@ -1,34 +1,22 @@ package team.projectpulse.section; import tools.jackson.databind.json.JsonMapper; -import org.springframework.boot.testcontainers.service.connection.ServiceConnection; -import org.testcontainers.containers.MySQLContainer; +import team.projectpulse.AbstractIntegrationTest; import team.projectpulse.system.StatusCode; import org.hamcrest.Matchers; import org.json.JSONObject; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Tag; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; -import org.springframework.boot.webmvc.test.autoconfigure.AutoConfigureMockMvc; -import org.springframework.boot.test.context.SpringBootTest; import org.springframework.http.HttpHeaders; import org.springframework.http.MediaType; -import org.springframework.test.annotation.DirtiesContext; -import org.springframework.test.context.ActiveProfiles; -import org.springframework.test.context.DynamicPropertyRegistry; -import org.springframework.test.context.DynamicPropertySource; import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.MvcResult; import org.springframework.test.web.servlet.ResultActions; import org.springframework.util.LinkedMultiValueMap; import org.springframework.util.MultiValueMap; -import org.testcontainers.containers.GenericContainer; -import org.testcontainers.junit.jupiter.Container; -import org.testcontainers.junit.jupiter.Testcontainers; -import org.testcontainers.utility.DockerImageName; import java.util.HashMap; import java.util.List; @@ -39,13 +27,8 @@ import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; -@SpringBootTest -@Testcontainers -@AutoConfigureMockMvc @DisplayName("Integration tests for Section API endpoints") -@Tag("integration") -@ActiveProfiles(value = "dev") -class SectionIntegrationTest { +class SectionIntegrationTest extends AbstractIntegrationTest { @Autowired MockMvc mockMvc; @@ -64,21 +47,6 @@ class SectionIntegrationTest { @Value("${api.endpoint.base-url}") String baseUrl; - @Container - @ServiceConnection - static MySQLContainer mysql = new MySQLContainer<>(DockerImageName.parse("mysql:8.0")); - - @Container - static final GenericContainer mailpit = new GenericContainer<>(DockerImageName.parse("axllent/mailpit")) - .withExposedPorts(1025, 8025); - - - @DynamicPropertySource - static void setMailpitProperties(DynamicPropertyRegistry registry) { - registry.add("spring.mail.host", mailpit::getHost); - registry.add("spring.mail.port", () -> mailpit.getMappedPort(1025)); - } - @BeforeEach void setUp() throws Exception { ResultActions resultActions = this.mockMvc.perform(post(this.baseUrl + "/users/login").with(httpBasic("b.wei@abc.edu", "123456"))); // httpBasic() is from spring-security-test. @@ -222,7 +190,7 @@ void instructorBillFindSectionById3() throws Exception { } @Test - @DirtiesContext(methodMode = DirtiesContext.MethodMode.BEFORE_METHOD) + void instructorBillFindSectionById2() throws Exception { this.mockMvc.perform(get(this.baseUrl + "/sections/2").contentType(MediaType.APPLICATION_JSON).accept(MediaType.APPLICATION_JSON).header(HttpHeaders.AUTHORIZATION, this.instructorBillToken)) .andExpect(jsonPath("$.flag").value(true)) @@ -244,7 +212,7 @@ void adminTimFindSectionById() throws Exception { } @Test - @DirtiesContext(methodMode = DirtiesContext.MethodMode.AFTER_METHOD) + void adminBingyangAddSection() throws Exception { // Given Map sectionDto = new HashMap<>(); @@ -310,7 +278,7 @@ void instructorBillAddSection() throws Exception { } @Test - @DirtiesContext(methodMode = DirtiesContext.MethodMode.AFTER_METHOD) + void adminBingyangUpdateSection() throws Exception { // Given Map sectionDto = new HashMap<>(); @@ -419,7 +387,7 @@ void adminTimAssignRubric() throws Exception { } @Test - @DirtiesContext(methodMode = DirtiesContext.MethodMode.AFTER_METHOD) + void adminBingyangAssignInstructor() throws Exception { this.mockMvc.perform(put(this.baseUrl + "/sections/1/instructors/2").contentType(MediaType.APPLICATION_JSON).accept(MediaType.APPLICATION_JSON).header(HttpHeaders.AUTHORIZATION, this.adminBingyangToken)) .andExpect(jsonPath("$.flag").value(true)) diff --git a/backend/src/test/java/team/projectpulse/student/StudentIntegrationTest.java b/backend/src/test/java/team/projectpulse/student/StudentIntegrationTest.java index 297e2e1..9429a33 100644 --- a/backend/src/test/java/team/projectpulse/student/StudentIntegrationTest.java +++ b/backend/src/test/java/team/projectpulse/student/StudentIntegrationTest.java @@ -1,31 +1,22 @@ package team.projectpulse.student; import tools.jackson.databind.json.JsonMapper; -import org.springframework.boot.testcontainers.service.connection.ServiceConnection; -import org.springframework.boot.webmvc.test.autoconfigure.AutoConfigureMockMvc; -import org.testcontainers.containers.MySQLContainer; -import org.testcontainers.junit.jupiter.Container; -import org.testcontainers.utility.DockerImageName; +import team.projectpulse.AbstractIntegrationTest; import team.projectpulse.system.StatusCode; import org.hamcrest.Matchers; import org.json.JSONObject; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Tag; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; -import org.springframework.boot.test.context.SpringBootTest; import org.springframework.http.HttpHeaders; import org.springframework.http.MediaType; -import org.springframework.test.annotation.DirtiesContext; -import org.springframework.test.context.ActiveProfiles; import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.MvcResult; import org.springframework.test.web.servlet.ResultActions; import org.springframework.util.LinkedMultiValueMap; import org.springframework.util.MultiValueMap; -import org.testcontainers.junit.jupiter.Testcontainers; import java.util.HashMap; import java.util.Map; @@ -36,13 +27,8 @@ import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; -@SpringBootTest -@Testcontainers -@AutoConfigureMockMvc @DisplayName("Integration tests for Instructor API endpoints") -@Tag("integration") -@ActiveProfiles(value = "dev") -class StudentIntegrationTest { +class StudentIntegrationTest extends AbstractIntegrationTest { @Autowired MockMvc mockMvc; @@ -63,10 +49,6 @@ class StudentIntegrationTest { @Value("${api.endpoint.base-url}") String baseUrl; - @Container - @ServiceConnection - static MySQLContainer mysql = new MySQLContainer<>(DockerImageName.parse("mysql:8.0")); - @BeforeEach void setUp() throws Exception { @@ -166,7 +148,6 @@ void testStudentJohnFindAnotherStudentsInfoById() throws Exception { } @Test - @DirtiesContext(methodMode = DirtiesContext.MethodMode.AFTER_METHOD) void testAddStudent() throws Exception { Map studentDto = new HashMap<>(); studentDto.put("username", "lucas"); @@ -245,7 +226,6 @@ void testAddStudentWithNoWrongUserInvitation() throws Exception { } @Test - @DirtiesContext(methodMode = DirtiesContext.MethodMode.AFTER_METHOD) void testStudentJohnUpdatesOwnInfo() throws Exception { Map studentDto = new HashMap<>(); studentDto.put("username", "john"); @@ -327,4 +307,4 @@ void testStudentJohnFindStudentsInAnotherTeam() throws Exception { .andExpect(jsonPath("$.data").value("Access Denied")); } -} \ No newline at end of file +} diff --git a/backend/src/test/java/team/projectpulse/system/WebConfigSpaForwardingTest.java b/backend/src/test/java/team/projectpulse/system/WebConfigSpaForwardingTest.java index c125dda..9b6f53d 100644 --- a/backend/src/test/java/team/projectpulse/system/WebConfigSpaForwardingTest.java +++ b/backend/src/test/java/team/projectpulse/system/WebConfigSpaForwardingTest.java @@ -5,11 +5,12 @@ import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.testcontainers.service.connection.ServiceConnection; import org.springframework.boot.webmvc.test.autoconfigure.AutoConfigureMockMvc; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.context.DynamicPropertyRegistry; +import org.springframework.test.context.DynamicPropertySource; import org.springframework.test.web.servlet.MockMvc; import org.testcontainers.containers.MySQLContainer; -import org.testcontainers.junit.jupiter.Container; -import org.testcontainers.junit.jupiter.Testcontainers; -import org.testcontainers.utility.DockerImageName; +import team.projectpulse.SharedContainers; import static org.junit.jupiter.api.Assertions.assertNull; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; @@ -17,15 +18,20 @@ @SpringBootTest @AutoConfigureMockMvc(addFilters = false) -@Testcontainers +@ActiveProfiles("dev") class WebConfigSpaForwardingTest { @Autowired MockMvc mockMvc; - @Container @ServiceConnection - static MySQLContainer mysql = new MySQLContainer<>(DockerImageName.parse("mysql:8.0")); + static final MySQLContainer mysql = SharedContainers.MYSQL; + + @DynamicPropertySource + static void setProperties(DynamicPropertyRegistry registry) { + registry.add("spring.mail.host", SharedContainers.MAILPIT::getHost); + registry.add("spring.mail.port", () -> SharedContainers.MAILPIT.getMappedPort(1025)); + } @Test void shouldForwardThreeSegmentSpaRouteToIndexHtml() throws Exception { diff --git a/backend/src/test/java/team/projectpulse/team/TeamIntegrationTest.java b/backend/src/test/java/team/projectpulse/team/TeamIntegrationTest.java index 818f49d..75064ee 100644 --- a/backend/src/test/java/team/projectpulse/team/TeamIntegrationTest.java +++ b/backend/src/test/java/team/projectpulse/team/TeamIntegrationTest.java @@ -1,26 +1,17 @@ package team.projectpulse.team; import tools.jackson.databind.json.JsonMapper; -import org.springframework.boot.testcontainers.service.connection.ServiceConnection; -import org.testcontainers.containers.MySQLContainer; -import org.testcontainers.junit.jupiter.Container; -import org.testcontainers.junit.jupiter.Testcontainers; -import org.testcontainers.utility.DockerImageName; +import team.projectpulse.AbstractIntegrationTest; import team.projectpulse.system.StatusCode; import org.hamcrest.Matchers; import org.json.JSONObject; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Tag; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; -import org.springframework.boot.webmvc.test.autoconfigure.AutoConfigureMockMvc; -import org.springframework.boot.test.context.SpringBootTest; import org.springframework.http.HttpHeaders; import org.springframework.http.MediaType; -import org.springframework.test.annotation.DirtiesContext; -import org.springframework.test.context.ActiveProfiles; import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.MvcResult; import org.springframework.test.web.servlet.ResultActions; @@ -35,13 +26,8 @@ import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; -@SpringBootTest -@Testcontainers -@AutoConfigureMockMvc @DisplayName("Integration tests for Team API endpoints") -@Tag("integration") -@ActiveProfiles(value = "dev") -class TeamIntegrationTest { +class TeamIntegrationTest extends AbstractIntegrationTest { @Autowired MockMvc mockMvc; @@ -60,10 +46,6 @@ class TeamIntegrationTest { @Value("${api.endpoint.base-url}") String baseUrl; - @Container - @ServiceConnection - static MySQLContainer mysql = new MySQLContainer<>(DockerImageName.parse("mysql:8.0")); - @BeforeEach void setUp() throws Exception { @@ -183,7 +165,6 @@ void adminTimFindTeamById() throws Exception { } @Test - @DirtiesContext(methodMode = DirtiesContext.MethodMode.AFTER_METHOD) void adminBingyangAddTeam() throws Exception { // Given Map teamDto = new HashMap<>(); @@ -225,7 +206,6 @@ void instructorBillAddTeam() throws Exception { } @Test - @DirtiesContext(methodMode = DirtiesContext.MethodMode.AFTER_METHOD) void adminBingyangUpdateTeam() throws Exception { // Given Map teamDto = new HashMap<>(); @@ -290,7 +270,6 @@ void adminTimUpdateTeam() throws Exception { // } @Test - @DirtiesContext(methodMode = DirtiesContext.MethodMode.AFTER_METHOD) void assignStudentToTeam() throws Exception { this.mockMvc.perform(put(this.baseUrl + "/teams/1/students/14").contentType(MediaType.APPLICATION_JSON).accept(MediaType.APPLICATION_JSON).header(HttpHeaders.AUTHORIZATION, this.adminBingyangToken)) .andExpect(jsonPath("$.flag").value(true)) @@ -307,7 +286,6 @@ void assignStudentToTeamNotInSameSection() throws Exception { } @Test - @DirtiesContext(methodMode = DirtiesContext.MethodMode.AFTER_METHOD) void removeStudentFromTeam() throws Exception { this.mockMvc.perform(delete(this.baseUrl + "/teams/1/students/4").contentType(MediaType.APPLICATION_JSON).accept(MediaType.APPLICATION_JSON).header(HttpHeaders.AUTHORIZATION, this.adminBingyangToken)) .andExpect(jsonPath("$.flag").value(true)) @@ -316,7 +294,6 @@ void removeStudentFromTeam() throws Exception { } @Test - @DirtiesContext(methodMode = DirtiesContext.MethodMode.AFTER_METHOD) void assignInstructorToTeam() throws Exception { this.mockMvc.perform(put(this.baseUrl + "/teams/3/instructors/2").contentType(MediaType.APPLICATION_JSON).accept(MediaType.APPLICATION_JSON).header(HttpHeaders.AUTHORIZATION, this.adminBingyangToken)) .andExpect(jsonPath("$.flag").value(true)) @@ -387,4 +364,4 @@ void transferTeamToAnotherSectionAsNonCourseAdmin() throws Exception { .andExpect(jsonPath("$.message").value("No permission.")); } -} \ No newline at end of file +} diff --git a/backend/src/test/java/team/projectpulse/user/UserIntegrationTest.java b/backend/src/test/java/team/projectpulse/user/UserIntegrationTest.java index b81cc66..f7c9677 100644 --- a/backend/src/test/java/team/projectpulse/user/UserIntegrationTest.java +++ b/backend/src/test/java/team/projectpulse/user/UserIntegrationTest.java @@ -1,33 +1,20 @@ package team.projectpulse.user; +import team.projectpulse.AbstractIntegrationTest; import tools.jackson.databind.json.JsonMapper; import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Tag; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; -import org.springframework.boot.webmvc.test.autoconfigure.AutoConfigureMockMvc; -import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.boot.testcontainers.service.connection.ServiceConnection; import org.springframework.http.MediaType; -import org.springframework.test.context.ActiveProfiles; import org.springframework.test.web.servlet.MockMvc; -import org.testcontainers.containers.MySQLContainer; -import org.testcontainers.junit.jupiter.Container; -import org.testcontainers.junit.jupiter.Testcontainers; -import org.testcontainers.utility.DockerImageName; import team.projectpulse.system.StatusCode; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; -@SpringBootTest -@Testcontainers -@AutoConfigureMockMvc @DisplayName("Integration tests for User API endpoints") -@Tag("integration") -@ActiveProfiles(value = "dev") -class UserIntegrationTest { +class UserIntegrationTest extends AbstractIntegrationTest { @Autowired MockMvc mockMvc; @@ -38,10 +25,6 @@ class UserIntegrationTest { @Value("${api.endpoint.base-url}") String baseUrl; - @Container - @ServiceConnection - static MySQLContainer mysql = new MySQLContainer<>(DockerImageName.parse("mysql:8.0")); - @Test void testCheckUserExists1() throws Exception { this.mockMvc.perform(get(this.baseUrl + "/users/exists/j.smith@abc.edu").contentType(MediaType.APPLICATION_JSON).accept(MediaType.APPLICATION_JSON)) @@ -60,4 +43,4 @@ void testCheckUserExists2() throws Exception { .andExpect(jsonPath("$.data").value(false)); } -} \ No newline at end of file +} From 3c87551b4e258e2e27350200f7c996a6c97642d7 Mon Sep 17 00:00:00 2001 From: Washingtonwei Date: Mon, 25 May 2026 07:42:24 -0500 Subject: [PATCH 2/2] docs: update CLAUDE.md testing section with new base class pattern Co-Authored-By: Claude Opus 4.7 --- backend/CLAUDE.md | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/backend/CLAUDE.md b/backend/CLAUDE.md index 29d5ca4..b5353c2 100644 --- a/backend/CLAUDE.md +++ b/backend/CLAUDE.md @@ -114,23 +114,21 @@ class FooServiceTest { ``` ### Integration Tests (`*IntegrationTest.java`) + +All integration tests extend `AbstractIntegrationTest`, which provides: +- **Shared containers** — `SharedContainers` starts a single MySQL and Mailpit container per JVM (singleton pattern), shared by all test classes +- **`@Transactional` rollback** — each test runs in a transaction that rolls back automatically, restoring the DataInitializer-seeded state without needing `@DirtiesContext` +- **Common annotations** — `@SpringBootTest`, `@AutoConfigureMockMvc`, `@ActiveProfiles("dev")`, `@Tag("integration")` + ```java -@SpringBootTest -@Testcontainers -@AutoConfigureMockMvc @DisplayName("Integration tests for Foo API endpoints") -@Tag("integration") -@ActiveProfiles(value = "dev") -public class FooIntegrationTest { +public class FooIntegrationTest extends AbstractIntegrationTest { @Autowired MockMvc mockMvc; @Autowired JsonMapper jsonMapper; @Value("${api.endpoint.base-url}") String baseUrl; - @Container @ServiceConnection - static MySQLContainer mysql = new MySQLContainer<>(DockerImageName.parse("mysql:8.0")); - String adminToken; String studentToken; @@ -153,4 +151,4 @@ public class FooIntegrationTest { } ``` -Integration tests use Testcontainers (MySQL 8.0), seed data from `DataInitializer`, and authenticate via HTTP Basic to get JWT tokens for subsequent requests. Tests verify both the `flag` and `code` fields in the `Result` response. Use `@DirtiesContext` on tests that modify state and could affect other tests. +Integration tests use Testcontainers (MySQL 8.0), seed data from `DataInitializer`, and authenticate via HTTP Basic to get JWT tokens for subsequent requests. Tests verify both the `flag` and `code` fields in the `Result` response. Do **not** use `@DirtiesContext` — the `@Transactional` rollback on the base class handles database reset automatically.