백앤드(스프링)

스프링(TDD) 테스트 코드 작성

유승혁 2022. 11. 7. 13:15

 TDD를 만족하기 위해서 단위 테스트 코드 작성하는 법을 공부했었다. 이 글에서 잘 정리하고자 한당. 기본적인 방식은 given-when-then을 따르고자 한다. 이러한 패턴의 장단점도 있다고 하지만, 아직 그러한 깊이는 부족한 것 같다. 나중에 많이 하다 보면 보이겠지?

 

0. Mock 객체 작성

@MockBean
private RecordService recordService;

 단위 테스트를 만들기 위해 의존관계 주입 받는 외부 객체는 Mock객체로 만들어 낸다. @Mock과 @MockBean 어노테이션으로 Mock객체를 생성할 수 있다. Spring Boot Container가 테스트 시에 필요하고, Bean이 Container에 존재한다면 @MockBean을 사용하고 아닌 경우에는 @Mock을 사용한다.

 

1. Controller 테스트 코드

@Slf4j
@MockBean(JpaMetamodelMappingContext.class)
@WebMvcTest(RecordController.class)
@Import({ElementValidator.class, CollectionValidator.class, GlobalExceptionHandler.class, EmptySectionException.class})
public class RecordControllerTestV2 {

    @Autowired
    private  MockMvc mockMvc;

    @Autowired
    private ObjectMapper objectMapper;
   
    @MockBean
    private RecordService recordService;

    private static MockHttpSession session;
   
    @AfterAll
    static void logout() {
        session.clearAttributes();
    }

    @Test
    @DisplayName("달력 정보 가져오기")
    void getCalenderInfo() throws Exception {
        List<GetCalenderRes> response = getCalenderResList();
        Integer year = 2022;
        Integer month = 9;

        given(recordService.myCalenderPage(loginId, year, month)).willReturn(
                response);

        mockMvc.perform(get("/record/calender")
                        .contentType(MediaType.APPLICATION_JSON)
                        .characterEncoding("utf-8")
                        .session(session)
                        .param("year", year.toString())
                        .param("month", month.toString())
                )
                .andExpect(status().isOk())
                .andExpect(jsonPath("$").isArray())
                .andExpect(jsonPath("$.[0].day").value(1l))
                .andDo(print());

        verify(recordService).myCalenderPage(loginId, year, month);
    }
}

  Controller 테스트 코드 작성이 가장 까다롭다. 하나씩 봅시다.

 

1.1 클래스 외부

@MockBean(JpaMetamodelMappingContext.class)

 위 어노테이션이 없으면 이러한 에러 메시지가 나타난다.

No qualifying bean of type 'umc.healthper.service.MemberService' available: expected at least 1 bean which qualifies as autowire candidate. Dependency annotations: {}

 이 이유는 테스트를 돌릴때는 기본적으로 XApplication이 돌면서 작동한다. 따라서 @EnableJpaAuditing을 Application위에 올리면 Jpa관련된 빈들이 올라오기를 요구한다. 이때, mockMvc를 사용한 테스트는 mvc와 관련한 빈들만 찾아서 올리므로 JPA관련 빈을 찾지 못해서 JPA metamodel must not be empty에러가 발생한다.

 

@WebMvcTest(RecordController.class)

 테스트 목적인 Controller 클래스는 위 어노테이션으로 등록해준다.

 

@Import({ElementValidator.class, CollectionValidator.class, 
	GlobalExceptionHandler.class, EmptySectionException.class})

 mock 테스트를 하면 위 같이 Import는 없는게 좋을 수 도 있지만, 내가 정의한 Exception이 잘 되는지, validator가 잘 동작 하는지와 같은 것을 보는 것도 Controller에서 하는 것이라 생각해서 가져왔다.

 

1.2 클래스 내부

@Autowired
private  MockMvc mockMvc;

 MockMvc의 경우 컨트롤러의 역할(url mapping, http method 등등)을 한다.

private static MockHttpSession session;

 웹 통신에서 세션을 빼놓을 수 없다. 로그인 구현을 세션 방식으로 했어서 위 객체가 필요했다.

 

1.3 메서드 내부

given(recordService.myCalenderPage(loginId, year, month)).willReturn(
        response);

 given-when-then에서 given에 해당한다. 위 코드는 Mock 객체인 recordService에 대해 myCalenderPage메서드에 위 세가지 인자가 들어올 경우 response를 리턴하도록 했다.

 

mockMvc.perform(get("/record/calender")
                .contentType(MediaType.APPLICATION_JSON)
                .characterEncoding("utf-8")
                .session(session)
                .param("year", year.toString())
                .param("month", month.toString())
        )
        .andExpect(status().isOk())
        .andExpect(jsonPath("$").isArray())
        .andExpect(jsonPath("$.[0].day").value(1l))
        .andDo(print());

 이번엔 when 부분이다. mockMvc.perform 메서드를 활용한다. get(url), post(url)로 http 메서드와 url명시한다. 이후 contetyType과 같은 헤더 정보들을 등록하고 param 메서드로 requestParameter를 등록한다. session으로 session 값을 넣을 수도 있다.

 위 코드에는 없지만 만약 body 부분에 내용을 등록하고 싶다면 content() 메서드를 통해 등록한다. 메서드 파라미터는 String 타입으로 넘겨 받는데, ObjectMapper를 통해 DTO를 변환하여 만들기도 한다. 아래 코드 처럼.

PostRecordReq recordReq = postReq();

ResultActions perform = mockMvc.perform(post("/record")
        .contentType(MediaType.APPLICATION_JSON)
        .characterEncoding("utf-8").content(req)
        .session(session));

 

 마지막으로 then 부분인 검증은 andExpect와 verify 메서드를 사용했다. andExpect는 $같은 표현식으로 json 타입을 검증하고 verify는 mock 객체의 메서드가 올바르게 사용 되었는지를 확인한다.

 

2. Service 테스트 코드

@Slf4j
@ExtendWith(MockitoExtension.class)
class RecordServiceTest{

    @InjectMocks
    RecordService service;

    @Mock
    RecordRepository repository;

    @Mock
    MemberService memberService;
    static Member testUser;
    static LocalDate testDate;
    @BeforeAll
    static void before(){
        testDate = LocalDate.now();
        testUser = Member.createMember(2022l, "test");

    }

    /**
     * 없는 운동 조회시
     */
    @Test
    @DisplayName("없는 기록 id 조회")
    void incorrectGet(){
        Long wrongRecordId = 500l;
        RecordNotFoundByIdException exception = assertThrows(RecordNotFoundByIdException.class, () -> {
            service.findById(wrongRecordId, testUser.getId());
        });
    }
    
     @Test
    @DisplayName("id 정상 조회")
    void getRecord(){
        Long recordId = 1l;
        String comment = "good-day";
        String sections = "0100101001";
        RecordJPA response = new RecordJPA(Member.createMember(2022l, "test"), comment, sections, testDate);
        Mockito.when(repository.findById(recordId)).thenReturn(response);

        RecordJPA findRecord = service.findById(recordId, testUser.getId());


        assertThat(findRecord.getComment()).isEqualTo(comment);
        assertThat(findRecord.getSections()).isEqualTo(sections);
        assertThat(findRecord.getCreatedDay()).isEqualTo(testDate);
    }
}

Service 테스트 코드 부터는 간단하다. 앞에서 많이 다루어서. 이번에는 mock 객체를 주입받는 RecordService를 만들었다. 

이번에는 given 단계에서 Mockito.when을 사용해 보았다.

 

3. Repository 테스트 코드

 repositoory 테스트 코드는 위 service 테스트 코드와 유사하게 진행하면 되어서 생략하겠다. 하지만 중요한 건 repository인 것 만큼 db와의 연결이 중요하다.

  테스트를 하는 만큼 기존 db 보다는 in-memory로 만들고 싶다면 아래와 같은 두 방식 중 하나를 클래스 레벨에 선언하면 된다.

@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE)

@DataJpaTest