콘텐츠로 이동

Testing

Testing

왜 쓰는가?

코드 변경이 기존 기능을 망가뜨렸는지 매번 수동으로 확인하는 건 불가능하다. 테스트는 변경에 대한 안전망이고, 문서 역할도 한다.

테스트 종류

종류 범위 속도 신뢰도
단위 테스트 클래스/메서드 단위 매우 빠름 낮음
통합 테스트 여러 컴포넌트 + DB 느림 높음
E2E 테스트 전체 시스템 매우 느림 가장 높음

실무에서는 단위 테스트를 많이, 통합 테스트를 적절히 작성한다.

JUnit5 기본

class MemberServiceTest {

    @Test
    @DisplayName("이메일로 회원 조회 성공")
    void findByEmail_success() {
        // given
        String email = "test@example.com";

        // when
        Member result = memberService.findByEmail(email);

        // then
        assertThat(result.getEmail()).isEqualTo(email);
    }

    @Test
    @DisplayName("존재하지 않는 회원 조회 시 예외 발생")
    void findByEmail_notFound() {
        assertThatThrownBy(() -> memberService.findByEmail("no@example.com"))
            .isInstanceOf(MemberNotFoundException.class);
    }
}

Mockito — 단위 테스트

외부 의존성(Repository, 외부 API)을 가짜 객체로 대체해 순수하게 로직만 테스트한다.

@ExtendWith(MockitoExtension.class)
class MemberServiceTest {

    @InjectMocks
    private MemberService memberService;

    @Mock
    private MemberRepository memberRepository;

    @Mock
    private EmailService emailService;

    @Test
    @DisplayName("회원 저장 시 이메일 발송")
    void save_sendEmail() {
        // given
        MemberCreateRequest request = new MemberCreateRequest("홍길동", "test@test.com");
        Member savedMember = new Member(1L, "홍길동", "test@test.com");

        given(memberRepository.existsByEmail("test@test.com")).willReturn(false);
        given(memberRepository.save(any(Member.class))).willReturn(savedMember);

        // when
        memberService.save(request);

        // then
        verify(emailService, times(1)).sendWelcome("test@test.com");
    }
}

@SpringBootTest — 통합 테스트

실제 Spring 컨텍스트를 띄워 DB까지 포함한 전체 흐름을 테스트한다.

@SpringBootTest
@Transactional  // 테스트 후 롤백
class MemberServiceIntegrationTest {

    @Autowired
    private MemberService memberService;

    @Autowired
    private MemberRepository memberRepository;

    @Test
    void save_and_find() {
        MemberCreateRequest request = new MemberCreateRequest("홍길동", "test@test.com");
        memberService.save(request);

        Optional<Member> found = memberRepository.findByEmail("test@test.com");
        assertThat(found).isPresent();
        assertThat(found.get().getName()).isEqualTo("홍길동");
    }
}

MockMvc — 컨트롤러 테스트

@WebMvcTest(MemberController.class)  // 컨트롤러 레이어만 로드
class MemberControllerTest {

    @Autowired
    private MockMvc mockMvc;

    @MockBean
    private MemberService memberService;

    @Test
    @DisplayName("POST /api/members - 회원 생성 성공")
    void create_success() throws Exception {
        MemberResponse response = new MemberResponse(1L, "홍길동", "test@test.com");
        given(memberService.save(any())).willReturn(response);

        mockMvc.perform(post("/api/members")
                .contentType(MediaType.APPLICATION_JSON)
                .content("""
                    {"name": "홍길동", "email": "test@test.com"}
                """))
            .andExpect(status().isOk())
            .andExpect(jsonPath("$.name").value("홍길동"))
            .andDo(print());
    }
}

테스트 픽스처 — @BeforeEach

@BeforeEach
void setUp() {
    memberRepository.deleteAll();
    testMember = memberRepository.save(new Member("홍길동", "test@test.com"));
}

단점 / 주의할 점

상황 문제 해결
@SpringBootTest 남용 전체 컨텍스트 로딩으로 느림 단위 테스트 위주, 통합 테스트 최소화
@Transactional 테스트와 실제 동작 차이 프록시 동작 차이 발생 가능 중요 시나리오는 별도 검증
Mock 과다 실제 동작과 괴리 핵심 의존성은 실제 객체 사용 고려
테스트 간 의존성 한 테스트가 다른 테스트에 영향 테스트마다 DB 초기화