테스트

단위/통합 테스트

테스트는 "얼마나 넓은 범위를 한 번에 검증하느냐"에 따라 나뉜다. 작은 부품 하나만 빠르게 보는 단위 테스트, 여러 부품을 실제로 엮어서 보는 통합 테스트다. 둘은 경쟁 관계가 아니라 역할이 다르며, 좋은 프로젝트는 둘을 함께 쓴다.


1. 단위 테스트 vs 통합 테스트

단위(Unit) 테스트

통합(Integration) 테스트

대상

클래스/메소드 하나

여러 계층(컨트롤러~DB)을 엮어서

스프링 컨테이너

안 띄움 (순수 자바)

띄움(@SpringBootTest 등)

속도

매우 빠름(ms)

상대적으로 느림

외부 의존(DB 등)

가짜로 대체

실제(또는 테스트용) 사용

테스트 피라미드

빠르고 가벼운 단위 테스트를 많이, 느리지만 실제에 가까운 통합 테스트를 적게 두는 비율이 권장된다. 단위 테스트로 로직을 촘촘히 검증하고, 통합 테스트로 "조립했을 때도 동작하는지"를 확인하는 식이다.


2. 가짜(Fake)와 목(Mock)

단위 테스트는 컨테이너를 띄우지 않으므로, 대상이 의존하는 객체(예: DB를 쓰는 Repository)를 진짜 대신 가짜로 넣어 준다. 그래야 DB 없이도, 빠르게, 원하는 상황을 만들어 검증할 수 있다.

  • Fake(가짜) — 동작은 하지만 단순화된 직접 만든 구현체. 예: DB 대신 Map에 저장하는 Repository.

  • Mock(목) — 라이브러리(Mockito)가 만들어 주는 흉내 객체. "이 메소드를 부르면 이 값을 주라"고 지정한다.

// Fake : DB 대신 메모리에 저장하는 가짜 구현
class FakeMemberRepository implements MemberRepository {
    private final Map<Long, Member> store = new HashMap<>();
    public Member save(Member m) { store.put(m.getId(), m); return m; }
    public Member findById(Long id) { return store.get(id); }
}

3. 순수 단위 테스트 — 생성자 주입이 빛나는 지점

Spring MVC — 의존성 주입에서 본 문제를 다시 보자. 필드 주입은 의존성을 넣을 통로(생성자·setter)가 없어, 스프링 없이 테스트하면 의존성이 null로 남아 막힌다.

반면 생성자 주입이면 가짜를 그냥 생성자로 넘기면 끝이다. 컨테이너가 필요 없다.

class MemberServiceTest {

    @Test
    void 회원을_저장한다() {
        // given : 가짜 Repository를 생성자로 주입 (스프링 컨테이너 X)
        MemberService service = new MemberService(new FakeMemberRepository());

        // when
        Member saved = service.join(new Member(1L, "kim"));

        // then
        assertThat(saved.getName()).isEqualTo("kim");
    }
}

왜 빠르고 쉬운가

스프링을 띄우지 않으니 테스트가 수 밀리초 만에 끝난다. 가짜를 마음대로 끼워 넣어 "저장 실패", "중복 회원" 같은 상황도 자유롭게 연출할 수 있다. 이게 생성자 주입을 권장하는 가장 큰 실무적 이유다.

Mockito로 목 객체 쓰기

앞에서는 FakeMemberRepository를 직접 만들었다. 인터페이스의 모든 메소드를 구현해야 해서 번거롭다. Mockito는 이 가짜 객체를 자동으로 만들어 주는 라이브러리다. spring-boot-starter-test에 기본 포함되어 있어 따로 추가할 필요가 없다.

Mockito의 핵심은 두 가지다.

  • 스터빙(stubbing) — "이 메소드를 이렇게 부르면 이 값을 돌려줘"라고 응답을 미리 지정하는 것. when(...).thenReturn(...) 으로 쓴다.

  • 검증(verify) — 테스트 대상이 그 목의 메소드를 실제로 호출했는지 확인하는 것. verify(...) 로 쓴다.

import static org.mockito.Mockito.*;

@Test
void 아이디로_조회() {
    // ① 목 생성 : 구현 없이 껍데기만 있는 가짜 객체
    MemberRepository repo = mock(MemberRepository.class);

    // ② 스터빙 : "findById(1L)을 부르면 이 회원을 돌려줘"라고 응답을 미리 정의
    when(repo.findById(1L)).thenReturn(Optional.of(new Member(1L, "kim")));

    MemberService service = new MemberService(repo);   // 생성자 주입

    // ③ 실행 & 검증
    assertThat(service.find(1L).getName()).isEqualTo("kim");

    // ④ 호출 검증 : service가 repo.findById(1L)을 정확히 1번 불렀는가
    verify(repo, times(1)).findById(1L);
}

스터빙하지 않은 메소드는?

목 객체는 스터빙하지 않은 메소드를 부르면 '기본값'을 돌려준다. 객체 반환 타입은 null, 숫자는 0, 불린은 false, 컬렉션은 빈 컬렉션이다. 그래서 테스트에 필요한 동작만 골라서 스터빙하면 된다.

자주 쓰는 문법

의미

mock(Xxx.class)

목 객체 생성

when(mock.f()).thenReturn(v)

f() 호출 시 v 반환하도록 스터빙

when(mock.f()).thenThrow(e)

f() 호출 시 예외를 던지도록 스터빙

verify(mock).f()

f()가 호출됐는지 검증

verify(mock, times(2)).f()

f()가 정확히 2번 호출됐는지 검증

verify(mock, never()).f()

f()가 호출되지 않았는지 검증

어노테이션으로 더 간결하게

mock(...)을 직접 부르는 대신, 어노테이션으로 목 생성과 주입을 자동화할 수 있다.

@ExtendWith(MockitoExtension.class)   // JUnit5에 Mockito 연동
class MemberServiceTest {

    @Mock                              // 목 객체 자동 생성
    MemberRepository repo;

    @InjectMocks                       // 위 목들을 생성자로 주입한 대상 생성
    MemberService service;

    @Test
    void 아이디로_조회() {
        when(repo.findById(1L)).thenReturn(Optional.of(new Member(1L, "kim")));
        assertThat(service.find(1L).getName()).isEqualTo("kim");
    }
}

어노테이션

역할

@ExtendWith(MockitoExtension.class)

JUnit 5에서 Mockito 어노테이션을 활성화

@Mock

목 객체를 자동 생성

@InjectMocks

@Mock으로 만든 목들을 생성자 주입한 테스트 대상 생성

@Mock vs @MockitoBean

@Mock은 스프링 컨테이너 없이 순수 단위 테스트에서 쓰는 Mockito 기능이다. 반면 5절의 @MockitoBean은 컨테이너가 떠 있는 상태(@SpringBootTest·@WebMvcTest)에서 특정 빈을 목으로 교체할 때 쓴다. "컨테이너가 있느냐 없느냐"로 구분하면 된다.


4. 통합 테스트 — @SpringBootTest

실제로 컨테이너를 띄워 여러 빈을 엮어 검증하고 싶을 때는 @SpringBootTest를 쓴다. 스프링이 빈을 만들고 주입해 주므로, 테스트에는 @Autowired로 받아서 쓴다.

@SpringBootTest          // 스프링 컨테이너를 띄운다
class MemberServiceIntegrationTest {

    @Autowired           // 컨테이너가 만든 진짜 빈을 주입
    MemberService memberService;

    @Test
    void 회원가입_통합() {
        Member saved = memberService.join(new Member(1L, "kim"));
        assertThat(memberService.find(1L)).isEqualTo(saved);
    }
}

느린 만큼 적게

컨테이너 기동 비용이 있어 단위 테스트보다 느리다. 모든 걸 통합 테스트로 하면 빌드가 굼떠진다. "조립 검증"이 필요한 핵심 경로에만 쓰고, 세부 로직은 단위 테스트로 커버한다.


5. 컨트롤러 테스트 — @WebMvcTest + MockMvc { #5-controller-test }

컨트롤러의 요청 매핑·응답만 가볍게 검증하려면 웹 계층만 띄우는 @WebMvcTest와, 실제 서버 없이 HTTP 요청을 흉내 내는 MockMvc를 쓴다.

@WebMvcTest(MemberController.class)   // 웹 계층만 로딩 (가벼움)
class MemberControllerTest {

    @Autowired MockMvc mockMvc;       // 가짜 HTTP 요청 도구

    @MockitoBean MemberService memberService;  // 의존 서비스는 목으로 대체

    @Test
    void 회원목록_페이지() throws Exception {
        mockMvc.perform(get("/members"))     // GET /members 요청 흉내
               .andExpect(status().isOk())   // 200 OK 인가
               .andExpect(view().name("members/list"));
    }
}

어노테이션/도구

역할

@WebMvcTest

컨트롤러 등 웹 계층만 로딩(서비스·리포지토리는 제외)

MockMvc

톰캣을 띄우지 않고 HTTP 요청/응답을 흉내

@MockitoBean

컨테이너의 특정 빈을 목으로 교체

@MockBean → @MockitoBean (Spring Boot 3.4+)

예전 자료에서 쓰던 @MockBean(org.springframework.boot.test.mock.mockito)은 Spring Boot 3.4부터 deprecated되었다. 이 강의(Boot 3.5) 기준으로는 스프링 프레임워크가 제공하는 @MockitoBean(org.springframework.test.context.bean.override.mockito.MockitoBean)을 사용한다. 기능과 사용법은 동일하다.


정리

  • 단위 테스트는 컨테이너 없이 클래스 하나를 빠르게, 통합 테스트는 컨테이너를 띄워 조립 상태를 검증한다.

  • 단위 테스트에서는 의존성을 가짜(Fake)나 목(Mock) 으로 대체한다.

  • 생성자 주입이면 가짜를 생성자로 넘겨 컨테이너 없이 테스트할 수 있다 — 이것이 생성자 주입을 권장하는 핵심 이유다.

  • 통합은 @SpringBootTest, 컨트롤러는 @WebMvcTest + MockMvc + @MockitoBean으로 가볍게 검증한다.

  • 빠른 단위 테스트를 많이, 느린 통합 테스트를 적게 두는 테스트 피라미드를 지향한다.

마지막 수정 2026년 10월 22일