Theme:

"테스트 코드 안 짜도 잘 돌아가는데요?" — 맞습니다, 지금은요. 근데 6개월 뒤에 누군가(혹은 내가) 그 코드를 수정할 때, 뭘 건드렸는데 어디가 터질지 모르는 상황이 옵니다. 테스트는 미래의 나를 위한 안전망이에요. 이 글에서는 테스트의 종류부터 TDD, JUnit 5, Mockito, 커버리지까지 핵심 내용을 전부 정리합니다.

왜 테스트를 짜야 하나

테스트를 짜야 하는 이유를 한마디로? 코드를 바꿀 수 있는 용기를 주기 때문 입니다.

회귀 버그 방지

기능 A를 수정했는데 기능 B가 깨지는 걸 회귀 버그(regression bug)라고 합니다. 코드베이스가 커지면 사람 눈으로는 이걸 잡을 수 없어요. 테스트가 있으면 수정 후 돌려보기만 하면 어디가 깨졌는지 바로 알 수 있습니다.

리팩터링 안전망

리팩터링은 외부 동작을 바꾸지 않고 내부 구조를 개선하는 건데요. "외부 동작이 정말 안 바뀌었는지" 어떻게 확인할까요? 테스트가 다 통과하면 됩니다. 테스트 없는 리팩터링은 눈 감고 줄타기하는 거랑 같아요.

살아있는 문서

잘 짜인 테스트 코드는 "이 메서드가 어떤 입력을 받으면 어떤 결과를 내야 하는지"를 보여줍니다. 주석이나 API 문서보다 정확해요. 왜냐면 문서는 코드랑 싱크가 안 맞을 수 있는데, 테스트는 안 맞으면 실패하니까요.


테스트 피라미드

마이크 콘(Mike Cohn)이 제안한 테스트 피라미드는 테스트 전략의 기본입니다.

PLAINTEXT
         /  E2E  \          ← 느리고 비쌈, 적게
        /----------\
       / Integration \      ← 중간
      /----------------\
     /    Unit Tests     \  ← 빠르고 쌈, 많이
    /______________________\
계층범위속도비용비중
Unit메서드/클래스 하나밀리초낮음70~80%
Integration여러 컴포넌트 조합초 단위중간15~20%
E2E전체 시스템 (UI 포함)분 단위높음5~10%

핵심은 ** 아래로 갈수록 많이, 위로 갈수록 적게** 짜라는 겁니다. E2E만 잔뜩 짜면 피드백이 느리고 깨지기 쉬워요. 단위 테스트를 충분히 깔아놓고, 통합 테스트로 연결부를 확인하고, E2E는 핵심 시나리오만 커버하는 게 이상적입니다.


단위 테스트 (Unit Test)

단위 테스트는 하나의 메서드나 클래스가 ** 격리된 환경에서** 올바르게 동작하는지 확인하는 테스트입니다. 외부 의존성(DB, 네트워크, 파일 시스템)을 제거하고 순수하게 로직만 검증해요.

Given-When-Then 패턴

테스트 코드의 가독성을 높이는 구조화 패턴입니다.

JAVA
@Test
@DisplayName("주문 금액이 50,000원 이상이면 배송비 무료")
void freeShippingOver50000() {
    // Given — 사전 조건 설정
    Order order = new Order(List.of(
        new OrderItem("노트북 거치대", 55000)
    ));

    // When — 테스트할 동작 실행
    int shippingFee = order.calculateShippingFee();

    // Then — 결과 검증
    assertThat(shippingFee).isZero();
}

Given에서 테스트 상황을 만들고, When에서 실행하고, Then에서 확인합니다. 누가 봐도 이 테스트가 뭘 검증하는지 한눈에 들어와요.

좋은 단위 테스트의 특징 — F.I.R.S.T 원칙

원칙의미설명
Fast빠르게수천 개가 수 초 안에 끝나야 한다
Independent독립적테스트 간 순서 의존성이 없어야 한다
Repeatable반복 가능어떤 환경에서든 같은 결과
Self-validating자가 검증pass/fail이 명확해야 한다 (수동 확인 X)
Timely적시에프로덕션 코드를 짜기 전이나 직후에 작성

좋은 테스트의 조건이 뭔가요? 핵심만 정리하면 F.I.R.S.T 원칙입니다.


JUnit 5

Java 테스트의 사실상 표준입니다. JUnit 5는 크게 JUnit Platform, JUnit Jupiter, JUnit Vintage 세 모듈로 나뉘는데, 우리가 주로 쓰는 건 JUnit Jupiter예요.

기본 어노테이션

JAVA
class OrderServiceTest {

    private OrderService orderService;

    @BeforeEach
    void setUp() {
        // 각 테스트 메서드 실행 전에 호출
        orderService = new OrderService();
    }

    @AfterEach
    void tearDown() {
        // 각 테스트 메서드 실행 후에 호출
    }

    @Test
    @DisplayName("정상 주문이면 COMPLETED 상태를 반환한다")
    void orderCompletes() {
        OrderResult result = orderService.placeOrder(new OrderRequest("item-1", 2));

        assertEquals(OrderStatus.COMPLETED, result.getStatus());
        assertNotNull(result.getOrderId());
    }

    @Test
    @DisplayName("수량이 0 이하면 IllegalArgumentException 발생")
    void zeroQuantityThrows() {
        assertThrows(IllegalArgumentException.class, () ->
            orderService.placeOrder(new OrderRequest("item-1", 0))
        );
    }
}

@BeforeEach로 매 테스트마다 새 인스턴스를 만들어주는 게 포인트입니다. 테스트 간 상태 공유를 막아서 독립성을 보장해요.

@ParameterizedTest — 여러 입력 한 번에 테스트

같은 로직을 다른 입력값으로 반복 테스트할 때 유용합니다.

JAVA
@ParameterizedTest
@CsvSource({
    "10000, 3000",   // 10,000원 → 배송비 3,000원
    "30000, 3000",   // 30,000원 → 배송비 3,000원
    "50000, 0",      // 50,000원 → 배송비 무료
    "100000, 0"      // 100,000원 → 배송비 무료
})
@DisplayName("주문 금액별 배송비 계산")
void shippingFeeByAmount(int orderAmount, int expectedFee) {
    Order order = new Order(orderAmount);

    assertThat(order.calculateShippingFee()).isEqualTo(expectedFee);
}

테스트 케이스를 추가하고 싶으면 @CsvSource에 한 줄 추가하면 끝입니다. @ValueSource, @EnumSource, @MethodSource 등 다양한 소스도 지원해요.

주요 Assertions

JAVA
// 기본
assertEquals(expected, actual);
assertNotEquals(a, b);
assertTrue(condition);
assertFalse(condition);
assertNull(object);
assertNotNull(object);

// 예외 검증
assertThrows(IllegalStateException.class, () -> service.doSomething());

// 시간 제한
assertTimeout(Duration.ofSeconds(2), () -> service.heavyTask());

// 한 번에 여러 검증 (하나 실패해도 나머지도 실행)
assertAll(
    () -> assertEquals("홍길동", user.getName()),
    () -> assertEquals("hong@test.com", user.getEmail()),
    () -> assertTrue(user.isActive())
);

assertAll은 여러 검증을 묶어서 실행합니다. 일반적으로 첫 번째 assertion이 실패하면 거기서 멈추는데, assertAll을 쓰면 전부 실행한 뒤 실패한 것들을 한꺼번에 보여줘요.


Mockito

외부 의존성을 가짜 객체(Mock)로 대체해서 단위 테스트에서 격리를 달성하는 프레임워크입니다.

기본 사용법

JAVA
@ExtendWith(MockitoExtension.class)
class OrderServiceTest {

    @Mock
    private OrderRepository orderRepository;

    @Mock
    private PaymentGateway paymentGateway;

    @InjectMocks
    private OrderService orderService;

    @Test
    @DisplayName("결제 성공 시 주문이 저장된다")
    void saveOrderOnPaymentSuccess() {
        // Given
        Order order = new Order("item-1", 10000);
        when(paymentGateway.charge(anyInt())).thenReturn(PaymentResult.SUCCESS);
        when(orderRepository.save(any(Order.class))).thenReturn(order);

        // When
        OrderResult result = orderService.placeOrder(order);

        // Then
        assertThat(result.getStatus()).isEqualTo(OrderStatus.COMPLETED);
        verify(orderRepository, times(1)).save(any(Order.class));
        verify(paymentGateway).charge(10000);
    }

    @Test
    @DisplayName("결제 실패 시 주문이 저장되지 않는다")
    void noSaveOnPaymentFailure() {
        // Given
        Order order = new Order("item-1", 10000);
        when(paymentGateway.charge(anyInt())).thenReturn(PaymentResult.FAILURE);

        // When & Then
        assertThrows(PaymentFailedException.class,
            () -> orderService.placeOrder(order));

        verify(orderRepository, never()).save(any());
    }
}

@Mock이 가짜 객체를 만들고, @InjectMocks가 그 가짜 객체들을 주입한 대상 클래스를 생성해줍니다. when().thenReturn()으로 Mock의 동작을 정의하고, verify()로 특정 메서드가 호출됐는지 검증해요.

Mock vs Stub vs Spy

자주 헷갈리는 부분인데요. 전부 ** 테스트 더블(Test Double)**의 종류입니다.

종류역할예시
Dummy파라미터를 채우기 위해 전달만 함, 실제로 사용 안 됨인터페이스의 빈 구현체
Stub미리 정해진 값을 반환함. 호출 여부는 검증 안 함when().thenReturn()
Mock호출 여부와 횟수까지 검증함verify()
Spy실제 객체를 감싸서 일부만 가짜로 동작시킴@Spy, 실제 메서드 호출 + 일부 오버라이드
Fake실제 동작하는 간소화된 구현체인메모리 DB, HashMap 기반 Repository

Mockito에서 when().thenReturn()을 쓰면 Stub 역할을 하는 거고, verify()를 쓰면 Mock 역할을 하는 겁니다. 실무에서는 이 구분을 엄격하게 안 따지고 그냥 "Mock"이라고 부르는 경우가 많아요. 이 구분을 제대로 이해하고 있으면 좋습니다.

JAVA
// Spy — 실제 객체를 기반으로 일부만 오버라이드
@Spy
private OrderValidator orderValidator = new OrderValidator();

@Test
void spyExample() {
    // 실제 validate()는 그대로 호출되지만, 특정 조건만 오버라이드
    doReturn(true).when(orderValidator).isWeekend();

    // 나머지 메서드는 진짜 로직이 실행됨
    boolean result = orderValidator.validate(order);
    assertThat(result).isTrue();
}

Spy는 실제 객체를 감싸기 때문에 진짜 로직이 돌아갑니다. 일부 메서드만 동작을 바꾸고 싶을 때 쓰는데요. 다만 테스트가 실제 구현에 의존하게 되니까 남용하면 안 됩니다.


통합 테스트 (Integration Test)

단위 테스트가 메서드 하나하나를 검증한다면, 통합 테스트는 ** 여러 컴포넌트가 함께 동작할 때** 제대로 되는지 확인합니다. DB 연결, 트랜잭션, HTTP 요청-응답 흐름 같은 걸 테스트해요.

@SpringBootTest

스프링 컨텍스트를 전부 띄웁니다. 실제 빈들이 주입되고, 실제 설정이 적용돼요.

JAVA
@SpringBootTest
@Transactional // 테스트 끝나면 롤백
class OrderIntegrationTest {

    @Autowired
    private OrderService orderService;

    @Autowired
    private OrderRepository orderRepository;

    @Test
    @DisplayName("주문 생성 → 조회 흐름이 정상 동작한다")
    void createAndFind() {
        // Given
        OrderRequest request = new OrderRequest("item-1", 2, 15000);

        // When
        Long orderId = orderService.createOrder(request);

        // Then
        Order saved = orderRepository.findById(orderId).orElseThrow();
        assertThat(saved.getItemName()).isEqualTo("item-1");
        assertThat(saved.getTotalPrice()).isEqualTo(30000);
    }
}

@Transactional을 테스트 클래스에 붙이면 각 테스트가 끝날 때 자동으로 롤백됩니다. DB에 찌꺼기가 남지 않아서 테스트 간 독립성이 보장돼요.

@DataJpaTest

JPA 관련 빈만 로딩해서 Repository 테스트에 집중할 때 씁니다. @SpringBootTest보다 훨씬 빨라요.

JAVA
@DataJpaTest
class OrderRepositoryTest {

    @Autowired
    private OrderRepository orderRepository;

    @Test
    @DisplayName("상태별 주문 조회")
    void findByStatus() {
        orderRepository.save(new Order("item-1", OrderStatus.COMPLETED));
        orderRepository.save(new Order("item-2", OrderStatus.CANCELLED));
        orderRepository.save(new Order("item-3", OrderStatus.COMPLETED));

        List<Order> completed = orderRepository.findByStatus(OrderStatus.COMPLETED);

        assertThat(completed).hasSize(2);
        assertThat(completed).allMatch(o -> o.getStatus() == OrderStatus.COMPLETED);
    }
}

기본적으로 내장 H2 데이터베이스를 사용합니다. 실제 DB(MySQL, PostgreSQL)와 동작이 다를 수 있다는 게 단점인데, 이걸 해결하는 게 TestContainers예요.

TestContainers

Docker 컨테이너로 실제 DB를 띄워서 테스트하는 라이브러리입니다. H2 대신 진짜 MySQL이나 PostgreSQL을 쓸 수 있어요.

JAVA
@SpringBootTest
@Testcontainers
class OrderRepositoryRealDbTest {

    @Container
    static PostgreSQLContainer<?> postgres = new PostgreSQLContainer<>("postgres:15")
        .withDatabaseName("testdb")
        .withUsername("test")
        .withPassword("test");

    @DynamicPropertySource
    static void configureProperties(DynamicPropertyRegistry registry) {
        registry.add("spring.datasource.url", postgres::getJdbcUrl);
        registry.add("spring.datasource.username", postgres::getUsername);
        registry.add("spring.datasource.password", postgres::getPassword);
    }

    @Autowired
    private OrderRepository orderRepository;

    @Test
    @DisplayName("PostgreSQL 네이티브 쿼리가 정상 동작한다")
    void nativeQueryWorks() {
        // 실제 PostgreSQL에서 테스트
        orderRepository.save(new Order("item-1", 10000));
        List<Order> result = orderRepository.findExpensiveOrders(5000);
        assertThat(result).isNotEmpty();
    }
}

CI에서도 Docker만 돌릴 수 있으면 실제 DB 기반 테스트가 가능합니다. "H2에서는 통과했는데 운영 DB에서 안 돼요" 같은 문제를 원천 차단할 수 있어요.


E2E 테스트 (End-to-End Test)

사용자 시나리오를 처음부터 끝까지 검증합니다. 브라우저를 자동으로 띄워서 실제 클릭, 입력, 페이지 이동을 시뮬레이션하는 방식이에요.

Selenium

가장 오래된 브라우저 자동화 도구입니다. Java, Python, JavaScript 등 여러 언어를 지원해요.

JAVA
WebDriver driver = new ChromeDriver();
driver.get("http://localhost:8080/login");

driver.findElement(By.id("username")).sendKeys("testuser");
driver.findElement(By.id("password")).sendKeys("password123");
driver.findElement(By.id("login-btn")).click();

WebElement welcome = driver.findElement(By.className("welcome-message"));
assertEquals("환영합니다, testuser님!", welcome.getText());

driver.quit();

Playwright

Microsoft가 만든 비교적 최신 도구입니다. Chromium, Firefox, WebKit을 전부 지원하고, Selenium보다 안정적이고 빠르다는 평가를 받아요. API도 더 직관적입니다.

JAVA
// Playwright Java API
try (Playwright playwright = Playwright.create()) {
    Browser browser = playwright.chromium().launch();
    Page page = browser.newPage();

    page.navigate("http://localhost:8080/login");
    page.fill("#username", "testuser");
    page.fill("#password", "password123");
    page.click("#login-btn");

    assertThat(page.locator(".welcome-message"))
        .hasText("환영합니다, testuser님!");
}

E2E 테스트는 느리고 깨지기 쉬워서 ** 핵심 시나리오 **만 작성하는 게 원칙입니다. 로그인, 결제, 회원가입 같은 크리티컬한 플로우만 커버하고, 세부 로직은 단위·통합 테스트에 맡기는 게 좋아요.


TDD (Test-Driven Development)

테스트를 먼저 짜고, 그 테스트를 통과하는 코드를 작성하는 개발 방법론입니다.

Red-Green-Refactor 사이클

PLAINTEXT
1. Red    → 실패하는 테스트를 먼저 작성한다
2. Green  → 테스트를 통과하는 최소한의 코드를 작성한다
3. Refactor → 코드를 정리한다 (테스트는 계속 통과해야 함)

이걸 반복합니다. 구체적인 예시를 볼게요.

** 요구사항 **: 비밀번호 검증 — 8자 이상, 대문자 포함, 숫자 포함

JAVA
// 1. Red — 실패하는 테스트 먼저
@Test
void passwordMustBeAtLeast8Characters() {
    PasswordValidator validator = new PasswordValidator();
    assertFalse(validator.isValid("Short1A")); // 7자 → 실패해야 함
    assertTrue(validator.isValid("ValidPw1")); // 8자 → 성공해야 함
}

이 시점에서는 PasswordValidator 클래스 자체가 없으니까 컴파일 에러가 나거나 테스트가 실패합니다.

JAVA
// 2. Green — 테스트를 통과하는 최소 코드
public class PasswordValidator {
    public boolean isValid(String password) {
        if (password.length() < 8) return false;
        if (!password.chars().anyMatch(Character::isUpperCase)) return false;
        if (!password.chars().anyMatch(Character::isDigit)) return false;
        return true;
    }
}

테스트가 통과합니다. 그런데 조건이 하드코딩되어 있고 가독성이 떨어져요.

JAVA
// 3. Refactor — 구조 개선
public class PasswordValidator {

    private static final int MIN_LENGTH = 8;

    public boolean isValid(String password) {
        return hasMinLength(password)
            && hasUpperCase(password)
            && hasDigit(password);
    }

    private boolean hasMinLength(String pw) {
        return pw.length() >= MIN_LENGTH;
    }

    private boolean hasUpperCase(String pw) {
        return pw.chars().anyMatch(Character::isUpperCase);
    }

    private boolean hasDigit(String pw) {
        return pw.chars().anyMatch(Character::isDigit);
    }
}

리팩터링 후에도 테스트는 여전히 통과합니다. 이게 TDD의 핵심이에요 — 테스트가 안전망 역할을 하면서 코드 품질을 끌어올리는 것.

TDD의 장점과 현실

** 장점 **:

  • 설계를 먼저 고민하게 됩니다 (테스트하기 쉬운 구조 = 좋은 구조)
  • 과도한 구현을 방지합니다 (테스트 통과에 필요한 만큼만 짜니까)
  • 회귀 테스트가 자연스럽게 쌓입니다

** 현실적인 어려움 **:

  • 초기 개발 속도가 느려집니다
  • 요구사항이 자주 바뀌면 테스트도 같이 바꿔야 합니다
  • 팀 전체가 TDD를 해야 효과가 큽니다

TDD에 대해 어떻게 생각하느냐는 질문이 나오면, 핵심만 정리하면 "장단점을 알고 있고, 상황에 따라 적용 여부를 판단한다"는 스탠스가 좋습니다. 무조건 TDD를 해야 한다거나 필요 없다고 하면 균형 잡힌 시각이 아니에요.


테스트 커버리지

라인 커버리지 vs 브랜치 커버리지

JAVA
public String getGrade(int score) {
    if (score >= 90) return "A";
    if (score >= 80) return "B";
    return "C";
}

** 라인 커버리지 **: 실행된 라인 수 / 전체 라인 수. getGrade(95)만 테스트해도 return "A" 라인을 실행하니까 일부 라인은 커버됩니다.

** 브랜치 커버리지 **: 모든 분기(if/else)를 커버했는지. 위 코드에서는 score >= 90이 true인 경우, false인 경우, score >= 80이 true인 경우, false인 경우 — 총 4개 브랜치를 다 커버해야 100%입니다.

브랜치 커버리지가 라인 커버리지보다 더 의미 있는 지표예요. 라인은 다 실행했어도 특정 조건 분기를 놓칠 수 있으니까요.

높은 커버리지 = 좋은 테스트?

** 아닙니다.** 커버리지가 높다고 테스트가 좋은 건 아니에요.

JAVA
@Test
void uselessHighCoverage() {
    // 라인은 다 실행하지만 아무것도 검증 안 함
    userService.createUser("hong", "hong@test.com");
    // assertion 없음!
}

이런 테스트는 커버리지를 높여주지만 실제로는 아무것도 검증하지 않습니다. 커버리지는 "테스트되지 않은 부분을 찾는 도구" 로 써야지, 그 자체가 목표가 되면 안 돼요.

보통 70~80% 이상이면 충분하다는 의견이 많고, getter/setter나 설정 클래스 같은 건 굳이 테스트하지 않습니다. 핵심 비즈니스 로직의 커버리지를 높이는 게 중요해요.


프론트엔드 테스트

백엔드만 테스트하면 되는 게 아닙니다. 프론트엔드도 테스트가 필요한데, 접근 방식이 좀 달라요.

Jest

JavaScript/TypeScript 테스트 프레임워크입니다. React, Vue, Node.js 어디서든 쓸 수 있어요.

JAVASCRIPT
// 순수 함수 테스트
describe('calculateDiscount', () => {
  test('VIP 회원이면 20% 할인', () => {
    const result = calculateDiscount(10000, 'VIP');
    expect(result).toBe(8000);
  });

  test('일반 회원이면 할인 없음', () => {
    const result = calculateDiscount(10000, 'NORMAL');
    expect(result).toBe(10000);
  });
});

React Testing Library

컴포넌트를 사용자 관점 에서 테스트하는 라이브러리입니다. 내부 구현(state, props)보다는 화면에 뭐가 보이고, 클릭하면 어떻게 되는지를 테스트해요.

JAVASCRIPT
import { render, screen, fireEvent } from '@testing-library/react';
import Counter from './Counter';

test('버튼 클릭 시 카운트가 증가한다', () => {
  render(<Counter />);

  const button = screen.getByRole('button', { name: '증가' });
  fireEvent.click(button);

  expect(screen.getByText('카운트: 1')).toBeInTheDocument();
});

"구현이 아닌 동작을 테스트하라"가 React Testing Library의 철학입니다. useState로 상태를 관리하든 useReducer를 쓰든, 사용자 입장에서 동작이 같으면 테스트도 통과해야 해요. 내부 구현에 의존하는 테스트는 리팩터링할 때마다 깨지니까요.


주의할 점

"테스트하기 좋은 코드란?"

외부 의존성이 적고, 입력과 출력이 명확한 코드입니다. 구체적으로:

  • **순수 함수에 가까운 코드 **: 같은 입력에 항상 같은 출력을 내고, 부수 효과(side effect)가 없는 코드
  • ** 의존성이 주입되는 코드 **: new로 직접 생성하지 않고 생성자로 받으면 테스트에서 Mock을 넣기 쉽습니다
  • ** 하나의 책임만 가진 코드 **: 메서드가 10가지 일을 하면 테스트도 10배 복잡해져요
JAVA
// 테스트하기 어려운 코드
public class OrderService {
    public void placeOrder(OrderRequest req) {
        // 현재 시간에 의존
        if (LocalDateTime.now().getHour() < 9) {
            throw new TooEarlyException();
        }
        // DB에 직접 접근
        Connection conn = DriverManager.getConnection(...);
        // ...
    }
}

// 테스트하기 좋은 코드
public class OrderService {
    private final Clock clock;
    private final OrderRepository repository;

    public OrderService(Clock clock, OrderRepository repository) {
        this.clock = clock;
        this.repository = repository;
    }

    public void placeOrder(OrderRequest req) {
        if (LocalDateTime.now(clock).getHour() < 9) {
            throw new TooEarlyException();
        }
        repository.save(new Order(req));
    }
}

두 번째 코드는 ClockOrderRepository를 외부에서 주입받으니까, 테스트에서 원하는 시간을 넣거나 Mock Repository를 넣을 수 있습니다.

"Mockito의 @Mock과 @MockBean의 차이는?"

  • @Mock: Mockito가 만드는 순수 가짜 객체. 스프링 컨텍스트와 무관합니다.
  • @MockBean: 스프링 컨텍스트에 등록된 빈을 Mock으로 교체합니다. @SpringBootTest와 함께 씁니다.
JAVA
// 단위 테스트 — @Mock
@ExtendWith(MockitoExtension.class)
class UnitTest {
    @Mock OrderRepository repo;         // 스프링 없이 가짜 객체
    @InjectMocks OrderService service;  // 가짜 객체 주입
}

// 통합 테스트 — @MockBean
@SpringBootTest
class IntegrationTest {
    @MockBean PaymentGateway gateway;   // 스프링 빈을 Mock으로 교체
    @Autowired OrderService service;    // 나머지는 진짜 빈
}

"테스트 픽스처(Fixture)란?"

테스트 실행에 필요한 사전 조건, 데이터, 객체 등을 통칭하는 용어입니다. @BeforeEach에서 만드는 테스트 데이터, 테스트용 설정 파일 같은 것들이 전부 픽스처예요.

"단위 테스트와 통합 테스트 중 뭘 더 많이 짜야 하나요?"

테스트 피라미드에 따르면 단위 테스트를 더 많이 짜는 게 원칙입니다. 단위 테스트는 빠르고 유지보수가 쉽기 때문이에요. 하지만 빈 연결, 트랜잭션 경계, 쿼리 정확성 같은 건 통합 테스트가 아니면 잡을 수 없습니다. 둘 다 필요하고, 비율의 문제예요.


파생 개념 — 여기서 더 파면 좋은 것들

  • CI/CD: 테스트가 있어야 CI 파이프라인에서 자동 검증이 가능합니다. 테스트 없는 CI/CD는 껍데기예요.
  • Spring: @SpringBootTest, @DataJpaTest, @WebMvcTest 같은 테스트 슬라이스 어노테이션은 스프링의 IoC 컨테이너를 기반으로 동작합니다.
  • ** 리팩터링 **: 테스트 없이 리팩터링하면 기존 동작이 깨졌는지 확인할 방법이 없어요. 리팩터링의 전제 조건이 테스트입니다.

정리

개념한 줄 정리
** 단위 테스트**메서드/클래스 단위로 격리해서 빠르게 검증
** 통합 테스트**여러 컴포넌트의 조합이 실제로 동작하는지 확인
E2E 테스트사용자 시나리오 전체를 브라우저 레벨에서 검증
TDD테스트 먼저 → 구현 → 리팩터링 사이클
Mockito외부 의존성을 가짜 객체로 교체해서 격리 달성
** 커버리지**테스트 누락 부분을 찾는 도구이지 목표가 아님

테스트는 "짜야 하느냐 마느냐"의 문제가 아니라 "어떻게 잘 짜느냐"의 문제입니다. 잘 짠 테스트는 버그를 잡아주고, 리팩터링을 가능하게 하고, 코드의 의도를 문서화해요. 테스트의 종류와 도구를 아는 것도 중요하지만, "왜 이 테스트를 이렇게 짰는지"를 설명할 수 있어야 합니다.

댓글 로딩 중...