<개요>

  • 테스트의 크기는 되도록이면 작게
  • 각 테스트 단위는 독립적으로 주입할 수 있도록
  • 필요한 것만 주입받고 테스트 해야하며
  • 최대한 빠르게 테스트 실행이 가능해야 함

<내용>

  • Layer
    • Controller
      • Web을 통한 호출시 PathVariable, RequestBody, Header, 인증등 을 담당하는 역할
    • Service
      • Biz Logic 구현
      • Biz Transaction 이 필요할 경우
    • Repository
      • Storage 와 Application 의 다리역할
      • 객체변환, 타입검증
    • POJO
      • 독립된 구현
      • 주로 Input / Output 을 통합 로직 수행
  • 공통사항
    • Given / When / Then 으로 작성하면 편함 (준비-실행-검증)
  • 각 Layer 별 테스트 코드는 다른 레이어가 정상동작한다는 가정으로 테스트하는 것이 원칙
    • Controller Layer
      • WebMvc에 관련된 Context만 로딩 (WebMvcTest)
      • 사용할 Bean들만 TestConfiguration 으로 정의하여 Context의 가동범위를 최소한 으로 한다.  
@RunWith(SpringRunner.class)
@WebMvcTest(BizController.class
)
@Import(SecurityConfig.class)
@ContextConfiguration(classes = SpringSecurityWebAuthTestConfig.class)
public class BizControllerTest {

    @Autowired
    private MockMvc mvc;
    @MockBean
    private BizService bizService;
   
    @Test
    @WithUserDetails("admin")
    public void getAllData() throws Exception{
        List<DataDto> datas = new ArrayList<>();

        DataDto dataDto = new DataDto();
        dataDto.setDataId(1);
        String dataName = "ttt";
        dataDto.setDataName(dataName);
        dataDto.setUserId(1);
        datas.add(dataDto);

        Page<DataDto> pages=new PageImpl<>(datas, Pageable.unpaged(), 1);

        //given
        given(bizService.findAllDatas(PageRequest.of(0,1))).willReturn(pages);
        //when , then
        this.mvc.perform(get("/datas/all?page=0&size=1"))
                .andExpect(jsonPath("content").isNotEmpty())
                .andExpect(jsonPath("content[0].dataId").value(1))
                .andExpect(status().isOk());
    }
    • 테스트 메소드 작성
      • MockBean : Mockup 대상
      • given : 테스트 범위내에서 정상동작할 경우의 응답, 혹은 주어진 조건
      • when : mvc.perform : 수행
      • then : andExpect : 기대값
  • Service Layer
    • JUnit으로만 테스트 (Spring Mvc 필요없음)
    • 테스트 대상인 Service 만 Inject, 나머지는 Mock
@RunWith(MockitoJUnitRunner.class)
public class BizServiceTest {
    @Mock
    private BizRepository dataRepository;
    @Mock
    private ModelMapper modelMapper;
    @InjectMocks
    private BizService bizService;

    @Test
    public void createService() throws Exception {
        DataDto dataDto = new DataDto();
        dataDto.setDataId(1);
        dataDto.setUserId(1);
        dataDto.setDataName("text");
        
        DataEntity dataEntity = new DataEntity();
        dataEntity.setDataId(1);
        dataEntity.setUserId(1);
        dataEntity.setDataName("text");
				
        //given
        given(modelMapper.map(dataDto, DataEntity.class)).willReturn(dataEntity);
        given(modelMapper.map(dataEntity, DataDto.class)).willReturn(dataDto);
        //when
        DataDto result = dataService.createData(dataDto);
        //then
        Assert.assertEquals(dataDto, result);

    }
      • 테스트 메소드 작성
        • given : 테스트 범위내에서 정상동작할 경우의 응답, 혹은 주어진 조건
        • when : 테스트 대상
        • then : assertEquals ( expected, actual)
  • Repository Layer
    • DataJpaTest 관련된 Context만 로딩
    • 사용할 Bean들만 정의하여 가동범위 최소한으로
    @RunWith(SpringRunner.class)
    @DataJpaTest
    @AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE)
    @Import({EnableEncryptablePropertiesConfiguration.class, JasyptConfig.class, SpringSecurityWebAuthTestConfig.class, TestJpaAuditingConfig.class})
    public class NewsRepositoryTest {
    
        @Autowired
        private NewsRepository newsRepository;
    
        @Test
        public void findAllByEnabled(){
            //given
            Pageable pageable = PageRequest.of(0,10);
    	//when
            Page<NewsEntity> newsEntityPage = newsRepository.findAllByEnabled(true, pageable);
    	//then
            Assert.assertEquals(1, newsEntityPage.getTotalElements());
        }
    • 테스트 메소드 작성
      • given : 테스트 범위내에서 정상동작할 경우의 응답, 혹은 주어진 조건
      • when : 테스트 대상
      • then : assertEquals ( expected, actual )
    • Local Test의 경우 H2 나 기타 메모리DB로 기동될때마다 테스트 데이터를 넣어놓으면 독립적인 테스트가 가능하기 때문에 편리하다.
  • 독립 Module
    • Context 기동없이 가능
    public class EmailValidationTest {
    
        @Test
        public void validation(){
            Pattern codePattern = PatternValidator.ValidationType.EMAIL.getMyPattern();
    
            Matcher matcher = codePattern.matcher("terst@gmail.com");
            Assert.assertTrue(matcher.matches());
    
            matcher = codePattern.matcher("test-1@naver.com");
            Assert.assertTrue(matcher.matches());
    • Bean주입없이 그냥 Java new로 POJO 테스트
public class JasyptTest {

	@Test
	public void encryptDecrypt() throws Exception {
		PooledPBEStringEncryptor encryptor = new PooledPBEStringEncryptor();
		SimpleStringPBEConfig config = new SimpleStringPBEConfig();
		config.setPassword("");
		config.setAlgorithm("PBEWithMD5AndDES");
		config.setKeyObtentionIterations("1000");
		config.setPoolSize("1");
		config.setSaltGeneratorClassName("org.jasypt.salt.RandomSaltGenerator");
		config.setStringOutputType("base64");
		encryptor.setConfig(config);

		String raw = "abcdefg";
		String encrypted = encryptor.encrypt(raw);

		String decrypted = encryptor.decrypt(encrypted);
		Assert.assertEquals(raw , decrypted);
	}

 

Git Remote Repository에 Push하기전에 Local Test Case를 모두 통과하는지 반드시 확인해야 한다.

@Test
    public void updateDeviceTelemetryInterval() throws Exception {

        int deviceId = 1;
        String serviceName = "AAAService";

        //given service info
        ServiceDto serviceDto = new ServiceDto();
        serviceDto.setServiceId(1);
        AzureIotHubEntity azureIotHubEntity = new AzureIotHubEntity();
        azureIotHubEntity.setAzureIotHubConnectionString("");
        serviceDto.setAzureIotHub(azureIotHubEntity);
        //given device info
        DeviceDto deviceDto = new DeviceDto();
        deviceDto.setDeviceId(1);
        //given interval info
        Gson gson = new Gson();
        DeviceDto device = new DeviceDto();
        device.setTelemetryInterval(1);
        String str = gson.toJson(device);

        given(serviceService.findServiceByName(serviceName)).willReturn(serviceDto);
        given(deviceService.findDeviceById(deviceId)).willReturn(deviceDto);

        //when then
        MockHttpServletRequestBuilder createUserRequest = post("/ServiceName/setTelemetryInterval/1")
                .contentType(MediaType.APPLICATION_JSON_UTF8_VALUE)
                .content(str);

        this.mvc.perform(createUserRequest)
                .andExpect(status().is2xxSuccessful());
    }

이와 같은 테스트코드는 정상적 상황을 테스트하는 상황입니다.

 

만약 해당하는 서비스가 없는 상황에서는 다음과 같은 Exception이 발생하도록 내부적으로 정의했습니다.

@ResponseStatus(value= HttpStatus.NOT_FOUND, reason="No such Service")
public class ServiceNotFoundException extends Exception {

    public ServiceNotFoundException(String message){
        super(message);
    }

    public ServiceNotFoundException(String message, Throwable cause) {
        super(message,cause);
    }
}

이와 같은 Exception이 의도한 것처럼 발생하는지 우리는 일반적으로 TestCode에서 아래와 같은 annotation을 통해서 처리합니다.

@Test(expected = ServiceNotFoundException.class)

그런데 여기서 주의해야 할점이 있습니다!!! 

 

우리가 일반적으로 Spring mvc를 사용할때 위와 같은 형태로 Exception을 정의할 경우, 내부적으로 Exception을 catch하여 밖으로 나가지 않도록 하고 HTTP STATUS코드를 변경하게 됩니다.

따라서 위와 같이 @Test(expected) 를 추가하더라도 Exception은 잡히지 않습니다.

어떻게 하면 내가 의도한 Exception을 잡아낼 수 있을지 Spring 내부소스를 살펴보았습니다.

final class TestDispatcherServlet extends DispatcherServlet {

@Override
	protected ModelAndView processHandlerException(HttpServletRequest request, HttpServletResponse response,
			@Nullable Object handler, Exception ex) throws Exception {

		ModelAndView mav = super.processHandlerException(request, response, handler, ex);

		// We got this far, exception was processed..
		DefaultMvcResult mvcResult = getMvcResult(request);
		mvcResult.setResolvedException(ex);
		mvcResult.setModelAndView(mav);

		return mav;
	}
 

위와 같이 result에 resolvedException에 발생한 Exception정보를 저장하고 있습니다.

그렇다면 아래와 같이 테스트 코드를 작성하면 될것이라고 추측됩니다.

@Test
    public void updateDeviceTelemetryIntervalNonExistService() throws Exception {

        //given
        ServiceDto serviceDto = new ServiceDto();

        Gson gson = new Gson();
        DeviceDto device = new DeviceDto();
        device.setTelemetryInterval(1);
        String str = gson.toJson(device);

        given(serviceService.findServiceByName("TestService")).willReturn(serviceDto);

        //when then
        MockHttpServletRequestBuilder createUserRequest = post("/AAAService/setTelemetryInterval/1")
                .contentType(MediaType.APPLICATION_JSON_UTF8_VALUE)
                .content(str);

        this.mvc.perform(createUserRequest)
                .andExpect((result)->assertTrue(result.getResolvedException().getClass().isAssignableFrom(ServiceNotFoundException.class)))
                .andExpect(status().is4xxClientError());
    }

테스트 결과 404 NotFound  와 exception의 유형을 모두 확인할 수  있습니다.

+ Recent posts