skill/check

Junit 기반 백엔드 테스트 스위트

have a nice day :D 2025. 10. 15. 08:21
반응형

Spring Boot 기준으로 JUnit 5(Jupiter) 를 사용하는 백엔드 테스트 스위트 구성을 “바로 복붙 가능한” 예제로 정리했습니다. (Java 예제지만, Kotlin도 동일한 구조·어노테이션으로 동작합니다.)


---

1) 기본 의존성 & 디렉터리

Maven (pom.xml)

<dependencies>
  <!-- JUnit 5 -->
  <dependency>
    <groupId>org.junit.jupiter</groupId>
    <artifactId>junit-jupiter</artifactId>
    <version>5.10.2</version>
    <scope>test</scope>
  </dependency>

  <!-- Spring Boot Test (MockMvc, @SpringBootTest 등 포함) -->
  <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-test</artifactId>
    <scope>test</scope>
    <exclusions>
      <!-- Vintage 제외 (JUnit4 미사용 시) -->
      <exclusion>
        <groupId>org.junit.vintage</groupId>
        <artifactId>junit-vintage-engine</artifactId>
      </exclusion>
    </exclusions>
  </dependency>

  <!-- Mockito (선택: 명시적 사용 시) -->
  <dependency>
    <groupId>org.mockito</groupId>
    <artifactId>mockito-junit-jupiter</artifactId>
    <version>5.13.0</version>
    <scope>test</scope>
  </dependency>

  <!-- Testcontainers (통합 테스트용, DB는 예시로 Postgres) -->
  <dependency>
    <groupId>org.testcontainers</groupId>
    <artifactId>junit-jupiter</artifactId>
    <version>1.20.1</version>
    <scope>test</scope>
  </dependency>
  <dependency>
    <groupId>org.testcontainers</groupId>
    <artifactId>postgresql</artifactId>
    <version>1.20.1</version>
    <scope>test</scope>
  </dependency>
  <dependency>
    <groupId>org.postgresql</groupId>
    <artifactId>postgresql</artifactId>
    <version>42.7.4</version>
    <scope>test</scope>
  </dependency>
</dependencies>

<build>
  <plugins>
    <!-- Unit 테스트: surefire / 통합 테스트: failsafe 권장 -->
    <plugin>
      <groupId>org.apache.maven.plugins</groupId>
      <artifactId>maven-surefire-plugin</artifactId>
      <version>3.5.0</version>
      <configuration>
        <useModulePath>false</useModulePath>
        <!-- 태그 기반 실행 예시: 기본은 unit만 -->
        <!-- <groups>unit</groups> -->
      </configuration>
    </plugin>
    <plugin>
      <groupId>org.apache.maven.plugins</groupId>
      <artifactId>maven-failsafe-plugin</artifactId>
      <version>3.5.0</version>
      <configuration>
        <!-- 통합 테스트만 돌리고 싶으면 integration 태그 지정 -->
        <groups>integration</groups>
      </configuration>
      <executions>
        <execution>
          <goals>
            <goal>integration-test</goal>
            <goal>verify</goal>
          </goals>
        </execution>
      </executions>
    </plugin>
  </plugins>
</build>

디렉터리 레이아웃

src
├─ main/java/... (프로덕션 코드)
└─ test/java/...
     ├─ unit/...            (순수 단위 테스트: @Tag("unit"))
     ├─ slice/web/...       (@WebMvcTest)
     ├─ slice/jpa/...       (@DataJpaTest)
     ├─ integration/...     (@SpringBootTest + Testcontainers: @Tag("integration"))
     └─ suite/...           (JUnit Platform Suite 모음)


---

2) 프로파일 & 테스트 DB

src/test/resources/application-test.yml (통합/슬라이스 공용 테스트 설정)

spring:
  jpa:
    hibernate:
      ddl-auto: create-drop
  datasource:
    # @DataJpaTest는 H2 자동 구성 가능, Testcontainers 사용 시 동적으로 주입
  jackson:
    serialization:
      write-dates-as-timestamps: false

> 통합 테스트는 Testcontainers가 제공하는 JDBC URL로 덮어씁니다.




---

3) 단위(Unit) 테스트 (Mockito)

src/test/java/.../unit/UserServiceTest.java

package com.example.unit;

import com.example.user.User;
import com.example.user.UserRepository;
import com.example.user.UserService;
import org.junit.jupiter.api.Tag;
import org.junit.jupiter.api.Test;
import org.mockito.Mockito;

import java.util.Optional;

import static org.assertj.core.api.Assertions.*;
import static org.mockito.ArgumentMatchers.anyLong;

@Tag("unit")
class UserServiceTest {

    private final UserRepository repo = Mockito.mock(UserRepository.class);
    private final UserService service = new UserService(repo); // 생성자 주입 가정

    @Test
    void findById_returnsUser() {
        Mockito.when(repo.findById(anyLong()))
               .thenReturn(Optional.of(new User(1L, "jgkim")));

        User u = service.findById(1L);
        assertThat(u.getName()).isEqualTo("jgkim");
        Mockito.verify(repo).findById(1L);
    }
}


---

4) Web 슬라이스(@WebMvcTest) — 컨트롤러만 빠르게

src/test/java/.../slice/web/UserControllerWebTest.java

package com.example.slice.web;

import com.example.user.UserController;
import com.example.user.UserService;
import org.junit.jupiter.api.Tag;
import org.junit.jupiter.api.Test;
import org.mockito.Mockito;
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.beans.factory.annotation.Autowired;

import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;

@Tag("web")
@WebMvcTest(controllers = UserController.class)
class UserControllerWebTest {

    @Autowired
    MockMvc mockMvc;

    @MockBean
    UserService userService;

    @Test
    void getUser_returns200() throws Exception {
        Mockito.when(userService.getName(1L)).thenReturn("jgkim");

        mockMvc.perform(get("/api/users/1"))
               .andExpect(status().isOk())
               .andExpect(jsonPath("$.name").value("jgkim"));
    }
}


---

5) JPA 슬라이스(@DataJpaTest) — Repository만 빠르게

src/test/java/.../slice/jpa/UserRepositoryDataJpaTest.java

package com.example.slice.jpa;

import com.example.user.User;
import com.example.user.UserRepository;
import org.junit.jupiter.api.Tag;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest;

import static org.assertj.core.api.Assertions.assertThat;

@Tag("jpa")
@DataJpaTest
class UserRepositoryDataJpaTest {

    @Autowired
    UserRepository repository;

    @Test
    void save_and_find() {
        User saved = repository.save(new User(null, "jgkim"));
        assertThat(saved.getId()).isNotNull();

        assertThat(repository.findById(saved.getId()))
          .get()
          .extracting(User::getName)
          .isEqualTo("jgkim");
    }
}


---

6) 통합(Integration) 테스트 — Testcontainers

src/test/java/.../integration/DatabaseIntegrationTest.java

package com.example.integration;

import com.example.user.User;
import com.example.user.UserRepository;
import org.junit.jupiter.api.Tag;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.AfterAll;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.ActiveProfiles;
import org.testcontainers.containers.PostgreSQLContainer;
import org.testcontainers.junit.jupiter.Testcontainers;

import static org.assertj.core.api.Assertions.assertThat;

@Tag("integration")
@Testcontainers
@SpringBootTest
@ActiveProfiles("test")
class DatabaseIntegrationTest {

    static PostgreSQLContainer<?> postgres =
        new PostgreSQLContainer<>("postgres:16-alpine");

    @BeforeAll
    static void start() {
        postgres.start();
        System.setProperty("spring.datasource.url", postgres.getJdbcUrl());
        System.setProperty("spring.datasource.username", postgres.getUsername());
        System.setProperty("spring.datasource.password", postgres.getPassword());
    }

    @AfterAll
    static void stop() {
        postgres.stop();
    }

    @Autowired
    UserRepository repository;

    @Test
    void end_to_end_save_and_load() {
        User u = repository.save(new User(null, "container-user"));
        assertThat(repository.findById(u.getId())).get()
            .extracting(User::getName).isEqualTo("container-user");
    }
}

> 필요 시 @DynamicPropertySource 로 DataSource 속성을 주입해도 됩니다.




---

7) 스위트 구성(실행 묶음)

JUnit5는 전통적 @RunWith(Suite.class) 대신 JUnit Platform Suite 를 사용합니다.
원하는 태그/패키지를 기준으로 테스트 스위트를 정의합니다.

src/test/java/.../suite/SmokeSuite.java

package com.example.suite;

import org.junit.platform.suite.api.*;

@Suite
@IncludeTags({"unit","web"})
@SelectPackages({
    "com.example.unit",
    "com.example.slice.web"
})
public class SmokeSuite { }

src/test/java/.../suite/FullSuite.java

package com.example.suite;

import org.junit.platform.suite.api.*;

@Suite
@SelectPackages("com.example")
@ExcludeTags("slow")   // 느린 테스트 제외 등
public class FullSuite { }

> CI에서는 태그 기반 실행이 더 유연합니다.

단위만: mvn -Dgroups=unit test (Surefire)

통합만: mvn -Dgroups=integration verify (Failsafe)





---

8) 파라미터/계층/조건부 실행 — 실전 팁

ParameterizedTest


@ParameterizedTest
@CsvSource({"admin,true", "guest,false"})
void canAccess(String role, boolean expected) {
  boolean actual = authService.canAccess(role);
  assertThat(actual).isEqualTo(expected);
}

@Nested 로 시나리오 가독성 향상


@Nested
class WhenResourceExists {
  @Test void returns200() { /* ... */ }
}

조건부 실행


@EnabledIfEnvironmentVariable(named = "CI", matches = "true")
@Test void onlyOnCI() { }

Fixture/TestDataBuilder


class UserBuilder {
  Long id; String name="default";
  UserBuilder id(Long v){ this.id=v; return this; }
  UserBuilder name(String v){ this.name=v; return this; }
  User build(){ return new User(id, name); }
}


---

9) CI 예시 (GitHub Actions)

.github/workflows/test.yml

name: Test
on: [push, pull_request]
jobs:
  build:
    runs-on: ubuntu-latest
    services:
      postgres:
        image: postgres:16-alpine
        ports: ["5432:5432"]
        env:
          POSTGRES_PASSWORD: postgres
        options: >-
          --health-cmd="pg_isready -U postgres"
          --health-interval=10s
          --health-timeout=5s
          --health-retries=5
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-java@v4
        with:
          distribution: temurin
          java-version: 21
      - name: Unit/Web/JPA (fast)
        run: mvn -B -Dgroups="unit,web,jpa" test
      - name: Integration (Testcontainers)
        run: mvn -B -Dgroups="integration" verify


---

10) 운영 팁 체크리스트

테스트 분리: unit(초고속) ↔ integration(느림) 태그로 분리

슬라이스 테스트 적극 활용: @WebMvcTest, @DataJpaTest 로 범위 축소

Testcontainers 재사용: 로컬 개발 시 Ryuk/이미지 캐시로 속도 개선

고정 포트/시드 데이터 피하기: 병렬 실행 안정성 확보

Fixture/Builder 패턴: 테스트 가독성과 유지보수성 향상

실행 속도 가드레일: 1분 내 smoke, 5~10분 내 full



---

필요하시면 Kotlin 버전, 멀티 모듈(MSA)용 스위트 템플릿, Jenkins 파이프라인 예시도 바로 만들어 드릴게요.


반응형