코딩하는 오징어

Spring boot starter test 와 Junit5를 이용한 테스트 본문

Framework/Spring

Spring boot starter test 와 Junit5를 이용한 테스트

코딩하는 오징어 2018. 8. 24. 23:51
반응형

Junit 5 & Spring Test을 이용한 TDD 환경 세팅

기본 세팅

@ExtendWith(SpringExtension.class)
@SpringBootTest(classes = KkApplication.class)
@ActiveProfiles("test")
public abstract class SpringTestSupport {
}
  • TestContext를 사용하려면 위의 SpringTestSupport를 상속받아 테스트 코드를 개발한다.
  • @ExtendWith는 Junit4의 RunWith(SpringRunner.class)와 비슷하다고 생각하면 된다.
  • @SpringBootTest는 @SpringBootApplication이 붙은 애너테이션을 찾아 context를 찾는다.
    • @SpringBootApplication을 사용하지 않고 다음과 같이 애너테이션을 풀어 개발했다면 위와 같이 classes필드를 통해 지정해야한다.

@Configuration
@EnableAutoConfiguration(exclude = { DataSourceAutoConfiguration.class, HibernateJpaAutoConfiguration.class })
@ComponentScan(
      basePackageClasses = KkApplication.class,
      excludeFilters = {
              @ComponentScan.Filter(type = FilterType.CUSTOM, classes = TypeExcludeFilter.class),
              @ComponentScan.Filter(type = FilterType.CUSTOM, classes = AutoConfigurationExcludeFilter.class)
      })
public class KkApplication {

  public static void main(String[] args) {
      SpringApplication.run(KkApplication.class, args);
  }

}

MockMvc 테스트 세팅

@AutoConfigureMockMvc
public abstract class SpringMockMvcTestSupport extends SpringTestSupport {

    @Autowired
    protected MockMvc mockMvc;

}
  • @AutoConfigureMockMvc를 통해 MockMvc를 Builder없이 주입 받을 수 있다. 스프링부트의 매력인 Auto Config의 장점이다. 이 애너테이션을 상속받아 controller 테스트들을 진행한다. 다음은 샘플 테스트 코드이다.

class HealthCheckerControllerTest extends SpringMockMvcTestSupport { @Test void healthCheckTest() throws Exception { this.mockMvc.perform(get("/v1/health-checker")) .andDo(print()) .andExpect(status().is(HttpStatus.OK.value())) .andExpect(content().contentType(MediaType.APPLICATION_JSON_UTF8_VALUE)) .andExpect(jsonPath("$.deployDate").exists()) .andExpect(jsonPath("$.deployVersion").exists()) .andExpect(jsonPath("$.distributor").exists()); } }

Mock 테스트 세팅

public class MockitoExtension implements TestInstancePostProcessor, ParameterResolver {

    @Override
    public void postProcessTestInstance(Object testInstance, ExtensionContext context) {
        MockitoAnnotations.initMocks(testInstance);
    }

    @Override
    public boolean supportsParameter(ParameterContext parameterContext, ExtensionContext extensionContext) {
        return parameterContext.getParameter().isAnnotationPresent(Mock.class);
    }

    @Override
    public Object resolveParameter(ParameterContext parameterContext, ExtensionContext extensionContext) {
        return getMock(parameterContext.getParameter(), extensionContext);
    }

    private Object getMock(Parameter parameter, ExtensionContext extensionContext) {
        Class<?> mockType = parameter.getType();
        Store mocks = extensionContext.getStore(Namespace.create(MockitoExtension.class, mockType));
        String mockName = getMockName(parameter);

        if (mockName != null) {
            return mocks.getOrComputeIfAbsent(mockName, key -> mock(mockType, mockName));
        } else {
            return mocks.getOrComputeIfAbsent(mockType.getCanonicalName(), key -> mock(mockType));
        }
    }

    private String getMockName(Parameter parameter) {
        String explicitMockName = parameter.getAnnotation(Mock.class).name().trim();
        if (!explicitMockName.isEmpty()) {
            return explicitMockName;
        } else if (parameter.isNamePresent()) {
            return parameter.getName();
        }
        return null;
    }
}
  • 위의 MockitoExtension 클래스를 @ExtendWith의 value 필드에 넣음으로써 Mock테스트에 필요한 곳에 애너테이션을 달면 된다. 다음은 샘플 코드이다.

import static org.mockito.Mockito.when;

@ExtendWith(MockitoExtension.class)
class HealthCheckerControllerTest extends SpringMockMvcTestSupport {

    @Mock
    private BookRepository bookRepository;

    @InjectMocks
    private BookService bookService;

    private givenBookRepository() {
      when(bookRepository.findByXXX(..)).thenReturn(...);
    }

    @Test
    void healthCheckTest() throws Exception {
        this.mockMvc.perform(get("/v1/health-checker"))
                .andDo(print())
                .andExpect(status().is(HttpStatus.OK.value()))
                .andExpect(content().contentType(MediaType.APPLICATION_JSON_UTF8_VALUE))
                .andExpect(jsonPath("$.deployDate").exists())
                .andExpect(jsonPath("$.deployVersion").exists())
                .andExpect(jsonPath("$.distributor").exists());
    }

}
  • 여기서 @Test메서드에 @Mock 파라미터로 바로 줄 수도 있다. @Test메서드에 파라미터를 줄 수 있는 것은 Junit4와 달라진 Junit5의 장점인 것 같다.

주의

  • org.junit.jupiter.api.Test를 import하여 @Test를 이용하여야 @ExtendWith(SpringExtension.class)를 통해 컨텍스트를 끌어 올 수 있다. null pointer exception이 발생한다면 org.junit.test가 import 되어있는지 확인해보자.
  • build.gradle에 다음과 같이 설정을 해야 @Test가 달린 메서드들을 테스트한다. gradle 버전이 4.6이상이어야 함을 참고하자.

  • build.gradle

    dependencies {
      ...
      testCompile('org.springframework.boot:spring-boot-starter-test')
    	testCompile("org.junit.platform:junit-platform-launcher:1.1.1")
    	testCompile("org.junit.platform:junit-platform-suite-api:1.1.1")
    	testCompile("org.junit.jupiter:junit-jupiter-engine:5.1.1")
    	testCompile("org.junit.vintage:junit-vintage-engine:5.1.1")
      ...
    }
    
    test {
      useJUnitPlatform() //Gradle 4.6이상에서 사용할 수 있는 설정
      systemProperty 'spring.profiles.active', 'test'
    }
    

    /gradle/wrapper/gradle-wrapper.properties

    ...
    distributionUrl=https\://services.gradle.org/distributions/gradle-4.8.1-all.zip 
    //필자는 Junit5로 테스트를 짰지만 gradle test task로 테스트들이 실행되지 않음을 발견하였다.
    //4.5.1 -> 4.8.1로 올려서 task를 실행시켜주었더니 필자가 겪은 에러가 해결되었다.


반응형
Comments