-
Notifications
You must be signed in to change notification settings - Fork 4
04 ‐ Spring Test 간단 가이드
우리가 스프링으로 개발을 할 때 각 컴포넌트들을 각각의 계층에 맞게 세분화하고 독립적이게 둠으로써 가지는 이점을 그대로 활용하기 위해 테스트 또한 서로의 의존성을 최대한 줄이고 독립적인 환경에서 테스트를 구동하는 것이 좋습니다. 그래서 그냥 API 통째로 테스트를 돌려버리는게 아니라(해당 테스트는 유닛 테스트가 아닌 e2e에서 진행해야 할 테스트 입니다), 컨트롤러 / 서비스 / 레포지토리 등 각 컴포넌트들을 계층 별로 독립적이게 테스트를 해야 합니다.
레포지토리 테스트에서는 Query Method
나 QueryDsl
을 테스트합니다.
Test Class에 @DataJpaTest
어노테이션을 붙여주시면 되고 해당 어노테이션을 붙이면 각 테스트가 Transaction으로 동작하며 각 테스트가 끌날때마다 자동으로 롤백되어 데이터베이스에 불필요한 데이터가 남지 않습니다.
@DisplayName("Team User Repository 테스트")
@DataJpaTest
@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE)
public class TeamUserRepositoryTest {
@Autowired
TeamUserRepository teamUserRepository;
@Autowired
UserRepository userRepository;
@Autowired
TeamRepository teamRepository;
각 테스트를 수행하기전에 반복적으로 수행해야할 로직이 있다면 @BeforeEach를 사용하시면 됩니다.
다음 코드의 경우 TeamUser
가 User
와 Team
을 참조하므로 그 둘의 객체를 생성하고 insert 해주고 있습니다.
@BeforeEach
void beforeEach() {
User user = User.builder()
.name("test")
.email("[email protected]")
.nickname("test")
.birthday(LocalDateTime.now())
.is_alarm(false)
.phone("test")
.address("test")
.certification(false)
.company("test")
.introduce("test")
.peerLevel(0L)
.representAchievement("test")
.build();
userRepository.save(user);
Team team = Team.builder()
.name("test")
.type(TeamType.STUDY)
.dueTo("10월")
.operationFormat(TeamOperationFormat.ONLINE)
.status(TeamStatus.RECRUITING)
.teamMemberStatus(TeamMemberStatus.RECRUITING)
.isLock(false)
.region1("test")
.region2("test")
.region3("test")
.build();
teamRepository.save(team);
}
그 후 수행하고 싶은 테스트를 @Test
어노테이션을 붙여 수행하면 됩니다. 다음 코드는 TeamUserRespository
의 findByUserIdAndTeamId
를 테스트하고 있습니다.
@Test
@DisplayName("TeamUser findByUserIdAndTeamId test")
void findByUserIdAndTeamIdTest() {
User user = userRepository.findAll().get(0);
Team team = teamRepository.findAll().get(0);
TeamUser teamUser = TeamUser.builder()
.user(user)
.userId(user.getId())
.team(team)
.teamId(team.getId())
.build();
teamUserRepository.save(teamUser);
TeamUser find = teamUserRepository.findByUserIdAndTeamId(user.getId(),
team.getId());
assertEquals(find.getTeamId(), teamUser.getTeamId());
}
서비스 테스트에서는 서비스에서 이루어지는 비즈니스 로직을 테스트합니다. 이 때 주의해야 할 점은 철저하게 관심사를 분리해서 다른 컴포넌트들에 의존적이지 않게 테스트를 수행해야 한다는 점입니다. 그를 위해 Mockito
라는 테스트 프레임워크를 사용합니다. Mockito
는 이름에서 유추해 볼 수 있듯이 특정 컴포넌트들을 Mock 객체로 사용함으로써 특정 컴포넌트들에 대한 의존성을 제거하고 오로지 내가 테스트 하려는 로직만 테스트를 할 수 있도록 해주는 역할을 합니다.
@ExtendWith(MockitoExtension.class)
@DisplayName("TeamService Test")
public class TeamServiceTest {
@Mock
private TeamRepository teamRepository;
@Mock
private TeamUserRepository teamUserRepository;
@InjectMocks
private TeamService teamService;
TeamService
를 테스트하려는 테스트 클래스입니다. 해당 테스트 클래스의 목적은 TeamService
를 테스트 하려는 것이므로 그 내부의 비즈니스 로직만 테스트 하면 되지, TeamService
가 사용하는 Repository
들이 잘 작동하냐 안하냐는 알바가 아닙니다. 그렇기 때문에 Repository
들에 @Mock
어노테이션을 붙여 Mock 객체임을 선언하고, 실제로는 이 Repository
들에 의존하고 있는 TeamService
에는 @InjectMocks
어노테이션을 붙여 이 Mock 객체들을 주입시켜 줍니다.
@Test
@DisplayName("getTeamList 함수 테스트")
void getTeamListTest() {
TeamUser teamUser = TeamUser.builder()
.user(user)
.team(team)
.userId(user.getId())
.teamId(team.getId())
.build();
List<TeamUser> teamUserList = new ArrayList<>();
teamUserList.add(teamUser);
when(teamUserRepository.findByUserId(anyLong())).thenReturn(teamUserList);
assertEquals(teamService.getTeamList(0L).get(0).getName(), team.getName());
}
이대로 테스트를 돌렸을때 마법같이 전부 잘 돌아가면 좋겠지만 애석하게도 저 Mock 객체들을 빈 깡통 객체들입니다. 이 빈 깡통들에게 어떤 함수가 호출되었을때 어떻게 동작해
라는 내용물을 채워넣어주어야지 비로소 제 역할을 할 수 있습니다. 위 코드에서 볼 수 있다시피 when()
을 통해 어떤 함수가 호출 되었을 때
를 정의하고 thenReturn()
을 통해 어떻게 동작해
를 정의합니다. 이렇게 TeamUserRepository
Mock 객체에 대한 행동을 정의를 해놓으면 그 Mock 객체를 주입받은 TeamService 내부에서 TeamUserRepository
의 findByUserId
가 호출 되었을 때 thenReturn()
에 정의 해 놓은 객체를 리턴하게 됩니다.
추가로 위와 같은 방법 말고도 verify()
를 이용한 테스트 방법도 존재합니다. 예를 들어, Repository의 delete와 같은 동작의 경우 리턴 값이 없기 때문에 thenReturn()
으로 테스트를 하기에는 부적합합니다. 그럴 경우 다음 코드처럼 verify()
의 times()
를 통해 Mock 객체의 어떤 함수가 몇번 호출되었는지를 통해서도 테스트를 수행할 수 있습니다. times()
외에도 verift()
에는 정말 다양한 동작이 있으니 검색해보시면 다양한 테스팅 방법들을 배우실 수 있으실 겁니다.
컨트롤러를 테스트할 떄도 마찬가지로 해당 컨트롤러가 의존하고 있는 모든 컴포넌트들을 Mock 객체로 사용함으로써 의존성을 제거해주시면 됩니다. 이 때 컨트롤러 테스트는 실제로 API가 콜이 되었을때를 상정하는 테스트이므로 실제 URL을 통해 API를 호출 시켜야 합니다. 그 역할을 해주는 녀석이 바로 MockMvc
입니다.
@SpringBootTest
@AutoConfigureMockMvc
public class TeamControllerTest {
MockMvc mockMvc;
@Mock
TeamService teamService;
@InjectMocks
TeamController teamController;
MockMvc
객체의 perform()
을 통해 실제 API를 호출 해 볼 수 있습니다. 이 때 andExpect()
를 통해 리스폰스의 status를 확인 할 수도 있습니다. 다음 코드는 API 리턴 값이 JSON 형태 이므로 이를 다시 List<Object>
화를 시킨 후 그 결과를 검증하는 테스트 코드입니다.
void getTeamListTest() throws Exception {
List<Team> teamList = new ArrayList<>();
teamList.add(team);
when(teamService.getTeamList(anyLong())).thenReturn(teamList);
MvcResult mvcResult = mockMvc.perform(get(TeamController.TEAM_URL + "/" + anyLong()))
.andExpect(status().isOk())
.andDo(print())
.andReturn();
String json = mvcResult.getResponse().getContentAsString();
List<TeamListResponse> list = objectMapper.readValue(json,
new TypeReference<List<TeamListResponse>>() {
});
assertEquals(list.get(0).getName(), team.getName());