testImplementation 'org.springframework.boot:spring-boot-starter-test'
@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를 구성한다.
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();
@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);
} ));
}
developerRepository.save(developer)
이 잘 동작하는지 확인해야 한다.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와 일치하는지 확인한다.
테스트 성공
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 {
}
@WebMvcTest
1) 특징
- MVC를 위한 테스트, 컨트롤러가 예상대로 작동되는지 테스트하기 위해 사용됨
- Web Layer만 로드하며,
@WebMvcTest
어노테이션 사용 시 아래의 항목들만 스캔하도록 제한하여 보다 빠르게 가벼운 테스트 가능
(ex, @Controller, @ControllerAdvice, @JsonComponent, @Convert, @GenericConverter, Filter, WebMvcConfigurer, HandlerMethodArgumentResolver)2) 장점
- WebApplication과 관련된 Bean들만 등록하기 때문에 @SpringBootTest보다 빠름
- 통합테스트를 진행하기 어려운 테스트를 개별적으로 진행 가능
3) 단점
Mock을 기반으로 테스트하기 때문에, 실제 환경에서는 예상 밖의 동작오류가 발생할 수 있음
//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
사용
@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()))
);
}
}
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 = []