단위 테스트(unit test)

조현희·2026년 2월 26일

테스트 코드 세션(레포지토리, 컨트롤러 유닛 테스트)

테스트 코드가 없으면

  • 테스트 코드가 없으면 버그를 찾기 힘듬
  • 테스트 코드를 작성하면 클린 코드를 작성하는 데 도움을 줌
  • 배포 단계(ci: integration지속적 빌드, cd: deployment 지속적 배포)에서 안정성 보장
  • 새로 개발할 때 테스트 코드가 있다면 기존 기능 동작이 확인됨
  • 초반에는 테스트코드때문에 작업 소요시간이 길어지지만, 점점 시간이 지날수록 점점 테스트코드가 있어야 시간이 짧아짐
  • 예외처리, 테스트를 잘하자!

testcontainer

  • 실제 인프라를 도커로 띄워서 사용하는 테스트도구
  • h2와 mysql 등 db가 다른 경우 적용 가능. 얘로 각종 db 띄울 수 있다
dependencies {
    testImplementation 'org.springframework.boot:spring-boot-testcontainers'
    testImplementation 'org.testcontainers:junit-jupiter'
    testImplementation 'org.testcontainers:mysql'
}

@Testcontainers
@SpringBootTest
class MyRepositoryTest {
	//랜덤포트로 띄운다!
    @Container
    static MySQLContainer<?> mysql =
        new MySQLContainer<>("mysql:8.0")
            .withDatabaseName("testdb")
            .withUsername("test")
            .withPassword("test");;
            
        @DynamicPropertySource
    static void configure(DynamicPropertyRegistry registry) {
        registry.add("spring.datasource.url", mysql::getJdbcUrl);
        registry.add("spring.datasource.username", mysql::getUsername);
        registry.add("spring.datasource.password", mysql::getPassword);        

}
@Container
static GenericContainer<?> redis =
        new GenericContainer<>("redis:7")
                .withExposedPorts(6379);
                
registry.add(
    "spring.data.redis.port",
    () -> redis.getMappedPort(6379)
);
  • h2는 user 라는 테이블명을 허용하지 않는 등, mysql과 차이가 있음

given-when-then

  • 어떤 상태일때. 어떤 행동을 실행하면. 예상한 결과가 맞는지 확인

annotation

  • @DataJpaTest: 레포지토리 테스트. jpa와 관련된 애들만 가지고옴. @AutoWired
  • @ExtendWith: 서비스(@Component) 테스트에 사용. @ExtendWith(MockitoExtension.class), @Mock, @InjectMocks(테스트대상)
  • @WebMvcTest: 컨트롤러 테스트. 필터(시큐리티)까지 띄운다. @AutoWired MockMVc, @MockBean(MockitoBean), @WithMockUser(username = "tester", roles = "USER")
  • @SpringBootTest: 스프링 전체 테스트.
  • @MockitoBean: 스프링 3.4 이후(현재)
  • @MockBean: 3.4 이전(실무)

Mocking

  • 테스트에 필요한 의존 객체를 가짜로 만듬

무거운 엔티티

  • 서비스에 있어야 할 거 같은 로직을 엔티티에 몰아넣기
  • 테스트 용이함

코드

  • 리플렉션은 내가 명확히 파악하고 쓰는 게 아니라면 쓰면 안됨. 실무에선 안씀. 동작도 약간 느림.
package com.example.demo.repository;

import com.example.demo.entity.User;
import com.example.demo.entity.UserRole;
import org.junit.jupiter.api.DisplayName;
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;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;

@DataJpaTest
class UserRepositoryTest {

    @Autowired
    private UserRepository userRepository;

    @Test
    @DisplayName("이메일로_사용자를_조회할_수_있다")
    void 이메일로_사용자를_조회할_수_있다() {
        // given
        String email = "asd@asd.com";
        User user = new User(email, "password", UserRole.USER);
        userRepository.save(user);

        // when
        User foundUser = userRepository.findByEmail(email).orElse(null);

        // then
        assertNotNull(foundUser);
        assertEquals(email, foundUser.getEmail());
        assertEquals(UserRole.USER, foundUser.getUserRole());

        // 강의 방식
        assertThat(foundUser).isNotNull();
        // 잘 모르겠으면 assertThat 하고 찾아보면 됨
    }
}
package com.example.demo.service;

import com.example.demo.dto.UserResponse;
import com.example.demo.entity.User;
import com.example.demo.entity.UserRole;
import com.example.demo.exception.InvalidRequestException;
import com.example.demo.repository.UserRepository;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.test.util.ReflectionTestUtils;

import java.util.Optional;

import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.mockito.ArgumentMatchers.anyLong;
import static org.mockito.BDDMockito.given;
import static org.mockito.Mockito.*;

@ExtendWith(MockitoExtension.class)
class UserServiceTest {

    @Mock // 테스트대상이 아님
    private UserRepository userRepository;

    @InjectMocks //테스트 대상임
    private UserService userService;

    @Test
    void User를_ID로_조회할_수_있다() {
        // given
        String email = "asd@asd.com";
        long userId = 1L;
        User user = new User(email, "password", UserRole.USER);
        ReflectionTestUtils.setField(user, "id", userId);

        given(userRepository.findById(anyLong())).willReturn(Optional.of(user));

        // when
        UserResponse userResponse = userService.getUser(userId);

        // then
        assertThat(userResponse).isNotNull();
        assertThat(userResponse.getId()).isEqualTo(userId);
        assertThat(userResponse.getEmail()).isEqualTo(email);
    }

    @Test
    void 존재하지_않는_User를_조회_시_InvalidRequestException을_던진다() {
        // Given
        long userId = 1L;
        given(userRepository.findById(anyLong())).willReturn(Optional.empty());

        // When & Then
        assertThrows(InvalidRequestException.class,
                () -> userService.getUser(userId),
                "User not found"
        );
    }

    @Test
    void User를_삭제할_수_있다() {
        // given
        long userId = 1L;
 // anyLong: 어떤 Long 타입 값이 와도 상관없다       given(userRepository.existsById(anyLong())).willReturn(true);
        doNothing().when(userRepository).deleteById(anyLong());

        // when
        userService.deleteUser(userId);

        // then
        verify(userRepository, times(1)).deleteById(userId);
    }

    @Test
    void 존재하지_않는_User를_삭제_시_InvalidRequestException를_던진다() {
        // given
        long userId = 1L;
        given(userRepository.existsById(userId)).willReturn(false);

        // when & then
        assertThrows(InvalidRequestException.class, () -> userService.deleteUser(userId));
        verify(userRepository, never()).deleteById(userId);
    }
}
@WebMvcTest(UserController.class)
class UserControllerTest {

    @Autowired
    private MockMvc mockMvc;

    @MockitoBean // 깡통
    private UserService userService;

    @Test
    void User_목록_조회_빈리스트() throws Exception {
        // given
        given(userService.getUsers()).willReturn(List.of());

        // when & then
        mockMvc.perform(get("/users"))
                .andExpect(status().isOk())
                .andExpect(jsonPath("$").isEmpty());
    }

    @Test
    void User_목록_조회() throws Exception {
        // given
        long userId1 = 1L;
        long userId2 = 2L;
        String email1 = "user1@a.com";
        String email2 = "user2@a.com";
        List<UserResponse> userList = List.of(
                new UserResponse(userId1, email1),
                new UserResponse(userId2, email2)
        );
        given(userService.getUsers()).willReturn(userList);

        // when & then
        mockMvc.perform(get("/users"))
                .andExpect(status().isOk())
                .andExpect(jsonPath("$.length()").value(2))
                .andExpect(jsonPath("$[0].id").value(userId1))
                .andExpect(jsonPath("$[0].email").value(email1))
                .andExpect(jsonPath("$[1].id").value(userId2))
                .andExpect(jsonPath("$[1].email").value(email2));
    }

    @Test
    void User_단건_조회() throws Exception {
        // given
        long userId = 1L;
        String email = "a@a.com";

        given(userService.getUser(userId)).willReturn(new UserResponse(userId, email));

        // when & then
        mockMvc.perform(get("/users/{userId}", userId))
                .andExpect(status().isOk())
                .andExpect(jsonPath("$.id").value(userId))
                .andExpect(jsonPath("$.email").value(email));
    }
  • 시큐리티 추가시 이렇게 수정해야 함

API 문서화(Rest Docs)

  • 컨트롤러 테스트 내용이 약간 달라지지만, 컨트롤러 테스트 내용이 곧 문서가 됨
profile
도전

0개의 댓글