From e491214a499bc26c36e42ad34364ed013a6da260 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=EC=B5=9C=EC=A4=80=ED=98=B8?= <junho5336@gmail.com>
Date: Mon, 18 Dec 2023 16:48:06 +0900
Subject: [PATCH] =?UTF-8?q?test:=20TestContainer=20=EC=A0=81=EC=9A=A9=20(#?=
 =?UTF-8?q?27)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

* test: TestContainer 적용

* test: TestContainer MySQL 버전 5.7로 변경

* test: RestAssured port 선언조건 추가
---
 build.gradle                                  |  4 ++
 src/main/resources/application-test.yml       |  4 ++
 .../in/koreatech/koin/AcceptanceTest.java     | 58 ++++++++++++++++++
 .../koin/acceptance/TrackApiTest.java         | 23 ++-----
 .../koreatech/koin/support/DBInitializer.java | 61 +++++++++++++++++++
 5 files changed, 131 insertions(+), 19 deletions(-)
 create mode 100644 src/main/resources/application-test.yml
 create mode 100644 src/test/java/in/koreatech/koin/AcceptanceTest.java
 create mode 100644 src/test/java/in/koreatech/koin/support/DBInitializer.java

diff --git a/build.gradle b/build.gradle
index 7f5da45c9..2002a34e4 100644
--- a/build.gradle
+++ b/build.gradle
@@ -24,10 +24,14 @@ dependencies {
     implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
     implementation 'org.springframework.boot:spring-boot-starter-validation'
     implementation 'org.springframework.boot:spring-boot-starter-web'
+    implementation 'com.mysql:mysql-connector-j'
     compileOnly 'org.projectlombok:lombok'
     runtimeOnly 'com.h2database:h2'
     runtimeOnly 'com.mysql:mysql-connector-j'
     annotationProcessor 'org.projectlombok:lombok'
+    testImplementation 'org.testcontainers:testcontainers:1.19.3'
+    testImplementation 'org.testcontainers:junit-jupiter:1.19.3'
+    testImplementation 'org.testcontainers:mysql'
     testImplementation 'io.rest-assured:rest-assured:5.3.2'
     testImplementation 'org.springframework.boot:spring-boot-starter-test'
 }
diff --git a/src/main/resources/application-test.yml b/src/main/resources/application-test.yml
new file mode 100644
index 000000000..7212038ca
--- /dev/null
+++ b/src/main/resources/application-test.yml
@@ -0,0 +1,4 @@
+spring:
+  jpa:
+    hibernate:
+      ddl-auto: create
diff --git a/src/test/java/in/koreatech/koin/AcceptanceTest.java b/src/test/java/in/koreatech/koin/AcceptanceTest.java
new file mode 100644
index 000000000..e57d21854
--- /dev/null
+++ b/src/test/java/in/koreatech/koin/AcceptanceTest.java
@@ -0,0 +1,58 @@
+package in.koreatech.koin;
+
+import static org.springframework.boot.test.context.SpringBootTest.WebEnvironment.RANDOM_PORT;
+
+import in.koreatech.koin.support.DBInitializer;
+import io.restassured.RestAssured;
+import org.junit.jupiter.api.BeforeEach;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.test.context.SpringBootTest;
+import org.springframework.boot.test.web.server.LocalServerPort;
+import org.springframework.context.annotation.Import;
+import org.springframework.test.context.ActiveProfiles;
+import org.springframework.test.context.DynamicPropertyRegistry;
+import org.springframework.test.context.DynamicPropertySource;
+import org.testcontainers.containers.MySQLContainer;
+import org.testcontainers.junit.jupiter.Container;
+
+@SpringBootTest(webEnvironment = RANDOM_PORT)
+@Import(DBInitializer.class)
+@ActiveProfiles("test")
+public abstract class AcceptanceTest {
+
+    private static final String ROOT = "test";
+    private static final String ROOT_PASSWORD = "1234";
+
+    @LocalServerPort
+    protected int port;
+
+    @Autowired
+    private DBInitializer dataInitializer;
+
+    @Container
+    protected static MySQLContainer container;
+
+    @DynamicPropertySource
+    private static void configureProperties(final DynamicPropertyRegistry registry) {
+        registry.add("spring.datasource.url", container::getJdbcUrl);
+        registry.add("spring.datasource.username", () -> ROOT);
+        registry.add("spring.datasource.password", () -> ROOT_PASSWORD);
+    }
+
+    static {
+        container = (MySQLContainer) new MySQLContainer("mysql:5.7.34")
+            .withDatabaseName("test")
+            .withUsername(ROOT)
+            .withPassword(ROOT_PASSWORD)
+            .withCommand("--character-set-server=utf8mb4", "--collation-server=utf8mb4_unicode_ci");
+        container.start();
+    }
+
+    @BeforeEach
+    void delete() {
+        if (RestAssured.port == RestAssured.UNDEFINED_PORT) {
+            RestAssured.port = port;
+        }
+        dataInitializer.clear();
+    }
+}
diff --git a/src/test/java/in/koreatech/koin/acceptance/TrackApiTest.java b/src/test/java/in/koreatech/koin/acceptance/TrackApiTest.java
index c11cb9e57..afb96932f 100644
--- a/src/test/java/in/koreatech/koin/acceptance/TrackApiTest.java
+++ b/src/test/java/in/koreatech/koin/acceptance/TrackApiTest.java
@@ -1,8 +1,6 @@
 package in.koreatech.koin.acceptance;
 
-import static org.springframework.boot.test.context.SpringBootTest.WebEnvironment.RANDOM_PORT;
-import static org.springframework.test.annotation.DirtiesContext.ClassMode.BEFORE_EACH_TEST_METHOD;
-
+import in.koreatech.koin.AcceptanceTest;
 import in.koreatech.koin.domain.Member;
 import in.koreatech.koin.domain.TechStack;
 import in.koreatech.koin.domain.Track;
@@ -14,21 +12,12 @@
 import io.restassured.response.Response;
 import java.time.format.DateTimeFormatter;
 import org.assertj.core.api.SoftAssertions;
-import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.DisplayName;
 import org.junit.jupiter.api.Test;
 import org.springframework.beans.factory.annotation.Autowired;
-import org.springframework.boot.test.context.SpringBootTest;
-import org.springframework.boot.test.web.server.LocalServerPort;
 import org.springframework.http.HttpStatus;
-import org.springframework.test.annotation.DirtiesContext;
-
-@SpringBootTest(webEnvironment = RANDOM_PORT)
-@DirtiesContext(classMode = BEFORE_EACH_TEST_METHOD)
-class TrackApiTest {
 
-    @LocalServerPort
-    int port;
+class TrackApiTest extends AcceptanceTest {
 
     @Autowired
     private TrackRepository trackRepository;
@@ -39,11 +28,6 @@ class TrackApiTest {
     @Autowired
     private MemberRepository memberRepository;
 
-    @BeforeEach
-    void setUp() {
-        RestAssured.port = port;
-    }
-
     @Test
     @DisplayName("BCSDLab 트랙 정보를 조회한다")
     void findTracks() {
@@ -119,7 +103,8 @@ void findTrack() {
                 softly.assertThat(response.body().jsonPath().getString("TrackName")).isEqualTo(track.getName());
 
                 softly.assertThat(response.body().jsonPath().getList("Members")).hasSize(1);
-                softly.assertThat(response.body().jsonPath().getInt("Members[0].id")).isEqualTo(member.getId());
+                softly.assertThat(response.body().jsonPath().getInt("Members[0].id"))
+                    .isEqualTo(member.getId().longValue());
                 softly.assertThat(response.body().jsonPath().getString("Members[0].name")).isEqualTo(member.getName());
                 softly.assertThat(response.body().jsonPath().getString("Members[0].student_number"))
                     .isEqualTo(member.getStudentNumber());
diff --git a/src/test/java/in/koreatech/koin/support/DBInitializer.java b/src/test/java/in/koreatech/koin/support/DBInitializer.java
new file mode 100644
index 000000000..dfcfd2b28
--- /dev/null
+++ b/src/test/java/in/koreatech/koin/support/DBInitializer.java
@@ -0,0 +1,61 @@
+package in.koreatech.koin.support;
+
+import jakarta.persistence.EntityManager;
+import jakarta.persistence.PersistenceContext;
+import java.sql.ResultSet;
+import java.sql.Statement;
+import java.util.ArrayList;
+import java.util.List;
+import javax.sql.DataSource;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.test.context.TestComponent;
+import org.springframework.transaction.annotation.Transactional;
+
+@TestComponent
+public class DBInitializer {
+
+    private static final int OFF = 0;
+    private static final int ON = 1;
+    private static final int COLUMN_INDEX = 1;
+
+    private final List<String> tableNames = new ArrayList<>();
+
+    @Autowired
+    private DataSource dataSource;
+
+    @PersistenceContext
+    private EntityManager entityManager;
+
+    private void findDatabaseTableNames() {
+        try (final Statement statement = dataSource.getConnection().createStatement()) {
+            ResultSet resultSet = statement.executeQuery("SHOW TABLES");
+            while (resultSet.next()) {
+                final String tableName = resultSet.getString(COLUMN_INDEX);
+                tableNames.add(tableName);
+            }
+        } catch (Exception e) {
+            e.printStackTrace();
+        }
+    }
+
+    private void truncate() {
+        setForeignKeyCheck(OFF);
+        for (String tableName : tableNames) {
+            entityManager.createNativeQuery(String.format("TRUNCATE TABLE %s", tableName)).executeUpdate();
+        }
+        setForeignKeyCheck(ON);
+    }
+
+    private void setForeignKeyCheck(int mode) {
+        entityManager.createNativeQuery(String.format("SET FOREIGN_KEY_CHECKS = %d", mode)).executeUpdate();
+    }
+
+    @Transactional
+    public void clear() {
+        if (tableNames.isEmpty()) {
+            findDatabaseTableNames();
+        }
+        entityManager.clear();
+        truncate();
+    }
+}