테스트 코드 작성

김태은·2022년 5월 1일
0

JUnit

  • JUnit은 자바용 단위 테스트 작성을 위한 산업 표준 프레임워크이다.

테스트 구현

  1. Spring Boot Starter 라이브러리를 dependency로 추가
testImplementation 'org.springframework.boot:spring-boot-starter-test'
  1. 테스트 클래스 생성 - DMakerServiceTest (단축키 : Ctrl + Shift + T)

1. Service 테스트 작성


@ExtendWith(MockitoExtension.class)
class DMakerServiceTest {
    @Mock
    private DeveloperRepository developerRepository;
    @Mock
    private RetiredDeveloperRepository retiredDeveloperRepository;

    @InjectMocks
    private DMakerService dMakerService;

  
    @Test
    public void test(){
        

}
  • @ExtendWith(MockitoExtension.class): 테스트 클래스가 Mockito를 사용함을 의미한다.
  • @Mock: 실제 구현된 객체 대신에 Mock 객체를 사용하게 될 클래스를 의미한다. 테스트 런타임 시 해당 객체 대신 Mock 객체가 주입되어 Unit Test가 처리된다.
  • @InjectMocks: Mock 객체가 주입된 클래스를 사용하게 될 클래스를 의미한다. 테스트 런타임 시 클래스 내부에 선언된 멤버 변수들 중에서 @Mock으로 등록된 클래스의 변수에 실제 객체 대신 Mock 객체가 주입되어 Unit Test가 처리된다.

이제 Mock을 이용하여 Unit Test를 해보자. 일반적으로 Given – When – Then 패턴을 이용하여 Mock Test를 구성한다.

  • Given: 테스트를 위한 준비 과정. 변수를 선언하고, Mock 객체에 대한 정의도 함께 작성한다.
  • When: 테스트를 실행하는 과정. 테스트하고자 하는 내용을 작성.
  • Then: 테스트를 검증하는 과정. 예상한 값과 결괏값이 일치하는 지 확인한다.

출처 : https://tech.lattechiffon.com/2021/07/03/junit5%EC%99%80-mockito%EB%A5%BC-%EC%9D%B4%EC%9A%A9%ED%95%9C-mock-test-java/

  1. 테스트에서 쓰일 객체를 생성한다.
	 private final Developer defaultDeveloper = Developer.builder()
            .developerLevel(JUNIOR)
                   .developerSkillType(FRONT_END)
                       .experienceYears(3)
                       .statusCode(EMPLOYED)
                       .name("홍길동")
                       .age(24)
                       .memberId("Hong123")
                       .build();

	private final CreateDeveloper.Request defaultCreateRequest = CreateDeveloper.Request.builder()
            .developerLevel(DeveloperLevel.JUNIOR)
            .developerSkillType(DeveloperSkillType.FRONT_END)
            .experienceYears(3)
            .name("홍길동")
            .age(24)
            .memberId("Hong123")
            .build();
  1. 테스트할 함수를 확인해 exception이 언제 발생할 수 있는지 확인한다.
  @Transactional
    public CreateDeveloper.Response createDeveloper(CreateDeveloper.@Valid Request request){

        validateCreateDeveloperRequest(request);
        Developer developer = Developer.builder()
                .developerLevel(request.getDeveloperLevel())
                .developerSkillType(request.getDeveloperSkillType())
                .experienceYears(request.getExperienceYears())
                .name(request.getName())
                .age(request.getAge())
                .memberId(request.getMemberId())
                .statusCode(StatusCode.EMPLOYED)
                .build();
        developerRepository.save(developer);

        return CreateDeveloper.Response.fromEntity(developer);
    }

    private void validateCreateDeveloperRequest(CreateDeveloper.Request request) {
        //business validation

        validateDeveloperLevel(
                request.getDeveloperLevel(),
                request.getExperienceYears()
        );

        developerRepository.findByMemberId(request.getMemberId())
                .ifPresent((developer ->{
                    throw new DMakerException(DUPLICATED_MEMBER_ID);
                } ));

    }
  • 개발자를 생성하는 과정에서 비즈니스 밸리데이션을 할 때, findByMemberId에서 중복되는 id가 존재하면 exception이 발생할 수 있다.
  • developerRepository.save(developer)이 잘 동작하는지 확인해야 한다.
  1. given, when, then 패턴을 활용해 테스트를 작성한다.

    3.1) 중복되는 Id 존재하지 않을 때

	@Test
    void createDeveloperTest_success(){
        //given

		given(developerRepository.findByMemberId(anyString()))
                .willReturn(Optional.empty());


        ArgumentCaptor<Developer> captor =
                ArgumentCaptor.forClass(Developer.class);

        //when
        CreateDeveloper.Response developer = dMakerService.createDeveloper(defaultCreateRequest);

        //then
        //developerRepository.save(developer) 동작 캡처 -> 성공적으로 save되는지 확인
        verify(developerRepository, times(1))
                .save(captor.capture());
        Developer saveDeveloper = captor.getValue();


        assertEquals(JUNIOR, saveDeveloper.getDeveloperLevel());
        assertEquals(FRONT_END, saveDeveloper.getDeveloperSkillType());
        assertEquals(3, saveDeveloper.getExperienceYears());


    }
  • given : 테스트를 위한 준비 과정, developerRepository에 접근해 memberId로 중복된 ID가 존재하는지 확인할 때 무조건 null을 리턴한다.

  • when : 테스트를 실행하는 과정, createDeveloper 함수를 통해 개발자를 생성한다.

  • then : 테스트를 검증하는 과정, ArgumentCaptor을 사용해 developerRepository에 save하는 과정을 캡처해 assertEquals를 통해 기대한 결과값과 일치하는 지를 확인한다.

  • 테스트 성공

    3.2) 중복되는 Id 존재할 때

	@Test
    void createDeveloperTest_failed_with_duplicated() {

        //given
        given(developerRepository.findByMemberId(anyString()))
                .willReturn(Optional.of(defaultDeveloper));

        //when
        //then
        DMakerException dMakerException = assertThrows(DMakerException.class,
                () -> dMakerService.createDeveloper(defaultCreateRequest));


        assertEquals(DUPLICATED_MEMBER_ID, dMakerException.getDMakerErrorCode());


    }
  • findByMemberId를 했을 때, 어떠한 값을 리턴하게 되면 중복된 Id가 존재하므로 Exception이 발생한다.

  • assertThrows를 이용해 createDeveloper를 했을 때 발생하는 Exception을 받아서 assertEquals를 이용해 기대하는 ERROR_CODE와 일치하는지 확인한다.

  • 테스트 성공

  • 다른 에러 코드를 넣으면 테스트값과 일치하지 않아 테스트 실패

2. Controller 테스트 작성

  1. JpaConfig 클래스 생성 (오류 방지)
package com.fastcampus.programming.dmaker.config;

import org.springframework.context.annotation.Configuration;
import org.springframework.data.jpa.repository.config.EnableJpaAuditing;

@Configuration
@EnableJpaAuditing
public class JpaConfig {
}
  1. DMakerControllerTest 클래스 생성

@WebMvcTest

1) 특징

  • MVC를 위한 테스트, 컨트롤러가 예상대로 작동되는지 테스트하기 위해 사용됨
  • Web Layer만 로드하며, @WebMvcTest어노테이션 사용 시 아래의 항목들만 스캔하도록 제한하여 보다 빠르게 가벼운 테스트 가능
    (ex, @Controller, @ControllerAdvice, @JsonComponent, @Convert, @GenericConverter, Filter, WebMvcConfigurer, HandlerMethodArgumentResolver)

2) 장점

  • WebApplication과 관련된 Bean들만 등록하기 때문에 @SpringBootTest보다 빠름
  • 통합테스트를 진행하기 어려운 테스트를 개별적으로 진행 가능

3) 단점

  • Mock을 기반으로 테스트하기 때문에, 실제 환경에서는 예상 밖의 동작오류가 발생할 수 있음

    출처 : https://astrid-dm.tistory.com/536


//import 추가
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.hamcrest.CoreMatchers.is;

@WebMvcTest(DMakerController.class)
class DMakerControllerTest {

   @Autowired
   private MockMvc mockMvc;

   @MockBean
   private DMakerService dMakerService;

   protected final MediaType contentType = new MediaType(
           MediaType.APPLICATION_JSON.getType(),
           MediaType.APPLICATION_JSON.getSubtype(),
           StandardCharsets.UTF_8);

   @Test
   void getAllDevelopers() throws Exception{

   }

}
  • 컨트롤러를 테스트 하기 위해 @WebMvcTest 사용
  • MediaType을 지정해주어 JSON타입으로 요청을 받고, 응답을 해주도록 하고, 문자 인코딩 방식은 UTF_8로 지정한다.
  1. 테스트 코드 작성

@WebMvcTest(DMakerController.class)
class DMakerControllerTest {

    @Autowired
    private MockMvc mockMvc;

    @MockBean
    private DMakerService dMakerService;

    protected final MediaType contentType = new MediaType(
            MediaType.APPLICATION_JSON.getType(),
            MediaType.APPLICATION_JSON.getSubtype(),
            StandardCharsets.UTF_8);

    @Test
    void getAllDevelopers() throws Exception{

        DeveloperDto juniorDeveloperDto = DeveloperDto.builder()
                .developerLevel(JUNIOR)
                .developerSkillType(FRONT_END)
                .memberId("member1")
                .build();

        DeveloperDto seniorDeveloperDto = DeveloperDto.builder()
                .developerLevel(SENIOR)
                .developerSkillType(BACK_END)
                .memberId("member2")
                .build();
        given(dMakerService.getAllEmployedDevelopers())
                .willReturn(Arrays.asList(juniorDeveloperDto,seniorDeveloperDto));

        mockMvc.perform(get("/developers").contentType(contentType))
                .andExpect(status().isOk())
                .andDo(print())
                .andExpect(
                        jsonPath("$.[0].developerSkillType",
                                is(FRONT_END.name()))
                ).andExpect(
                        jsonPath("$.[0].developerLevel",
                                is(JUNIOR.name()))
                ).andExpect(
                        jsonPath("$.[1].developerSkillType",
                                is(BACK_END.name()))
                ).andExpect(
                        jsonPath("$.[1].developerLevel",
                                is(SENIOR.name()))
                );
    }

   
}
  • developerDto 객체를 만들고, Service의 getAllEmployedDevelopers()가 실행될 때 리턴값으로 생성한 Dto 객체를 리턴해주도록 한다.
  • mockMvc가 제공하는 메소드를 이용해 get 방식으로 요청했을 때, 기대하는 Json 데이터와 상태코드 값을 andExpect를 이용해 입력한다.
  • andDo(print()) 를 이용하면 콘솔창에 요청값과 응답받은 데이터를 print해준다.
MockHttpServletRequest:
      HTTP Method = GET
      Request URI = /developers
       Parameters = {}
          Headers = [Content-Type:"application/json;charset=UTF-8"]
             Body = null
    Session Attrs = {}

Handler:
             Type = com.fastcampus.programming.dmaker.controller.DMakerController
           Method = com.fastcampus.programming.dmaker.controller.DMakerController#getAllDevelopers()

Async:
    Async started = false
     Async result = null

Resolved Exception:
             Type = null

ModelAndView:
        View name = null
             View = null
            Model = null

FlashMap:
       Attributes = null

MockHttpServletResponse:
           Status = 200
    Error message = null
          Headers = [Content-Type:"application/json"]
     Content type = application/json
             Body = [{"developerLevel":"JUNIOR","developerSkillType":"FRONT_END","memberId":"member1"},
             {"developerLevel":"SENIOR","developerSkillType":"BACK_END","memberId":"member2"}]
    Forwarded URL = null
   Redirected URL = null
          Cookies = []

  • 테스트 성공

모든 테스트가 잘 동작하는지 확인

  • java -> Run 'All Tests' 클릭

  • 모든 테스트 성공

0개의 댓글