spring5 와 junit5

저번시간에 이어 오늘도 junit5 포스팅이다. 오늘은 junit5로 Spring을 어떻게 Test 하는지 알아보자. 이미 spring에서 만들어 놓아서 우리는 사용하기만 하면 된다. 일단 아래와 같이 maven 디펜더시를 받자.

<dependencies>
  <dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-context</artifactId>
    <version>5.0.0.RELEASE</version>
  </dependency>
  <dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-test</artifactId>
    <version>5.0.0.RELEASE</version>
    <scope>test</scope>
  </dependency>
  <dependency>
    <groupId>org.junit.jupiter</groupId>
    <artifactId>junit-jupiter-engine</artifactId>
    <version>5.0.1</version>
    <scope>test</scope>
  </dependency>
</dependencies>

간단하게 어떻게 하는지만 테스트를 할 예정이라 web과 관련된 설정은 하지 않았다. 그렇다고 웹이라고 해서 크게 다른 건 없다.
테스트를 위해 샘플 코드를 작성해보자.

public interface HelloService {

  String hello();
}

@Configuration
public class Application {

  @Bean
  HelloService helloService() {
    return () -> "hello world";
  }
}

우리는 HelloService 라는 인터페이스를 빈으로 등록 하였다. 샘플 코드이기에 크게 의미는 없다.
저번시간에 배운 ParameterResolver 인터페이스를 구현한 Spring 의 코드가 존재한다. 바로 SpringExtension 클래스이다. 뿐만아니라 여러 인터페이스를 구현했으니 참고해도 되겠다. 우리는 SpringExtension 클래스를 사용해서 테스트를 할 것이다. 일단 한번 작성해보자.

@ExtendWith(SpringExtension.class)
@ContextConfiguration(classes = Application.class)
class GeneralHelloServiceTests {

  private final HelloService helloService;

  @Autowired
  GeneralHelloServiceTests(HelloService helloService) {
    this.helloService = helloService;
  }

  @Test
  void parametersHello(@Autowired HelloService helloService) {
    assertEquals(helloService.hello(), "hello world");
  }
  @Test
  void constructorHello() {
    assertEquals(this.helloService.hello(), "hello world");
  }

}

junit5의 ExtendWith 어노테이션을 사용해서 우리는 확장할 수 있었다. 또한 기존의 ContextConfiguration 어노테이션으로 컨텍스트를 설정 할 수 있었다. SpringExtension를 사용하면 생성자를 통해 빈을 주입 받을 수 있고 파라미터를 통해서도 빈을 주입 받을 수 있다. 하지만 여기서 주의할 점은 spring 4.3 부터는 일반적인 코드에는 생성자 위에 @Autowired를 붙이지 않아도 자동으로 주입하지만 테스트에서는 그렇지 않다. 무조건 @Autowired를 작성해줘야한다. 물론 파라미터도 마찬가지다. 우리는 Spring에서 미리 만들어진의 junit5 지원으로 아주 심플하게 테스트를 할 수 있게 되었다.

이 보다 좀 더 간단하게 테스트 할 수 있는 방법이다. 간단하다고 하더라도 그냥 한줄 정도? Spring에서 지원해주는 @SpringJUnitConfig 어노테이션은 위의 2개의 애노테이션을 합친 애노테이션이다. 그래서 2줄을 1줄로 바꿀 수 있는 좋은(?) 애노테이션이다. 한번 해보자.

@SpringJUnitConfig(Application.class)
class AnnotationHelloServiceTests {

  private final HelloService helloService;

  @Autowired
  AnnotationHelloServiceTests(HelloService helloService) {
    this.helloService = helloService;
  }

  @Test
  void parametersHello(@Autowired HelloService helloService) {
    assertEquals(helloService.hello(), "hello world");
  }

  @Test
  void constructorHello(@Autowired HelloService helloService) {
    assertEquals(this.helloService.hello(), "hello world");
  }
}

위와 같이 작성해도 기존과 동일한 코드로 동작한다. @SpringJUnitConfig 애노테이션 말고도 web을 지원해주는 @SpringJUnitWebConfig 애노테이션도 존재한다. 이것은 한번씩 해보도록 하자. 그렇다고 크게 어려운건 없고 @WebAppConfiguration 애노테이션 한개가 추가 됐을 뿐이다.

눈치빠른 사람들은 눈치챘겠지만 junit5 또한 메타 애노테이션을 지원한다. Spring과 비슷하게 가고 있는 듯하다. 테스트를 위해 좀 더 코드를 작성해보자.

@Configuration
public class Application {

  @Bean
  HelloService helloService() {
    return () -> "hello world";
  }

  @Profile("default")
  static class TestOrder {

    @Bean
    OrderService orderService() {
      return name -> "hello " + name;
    }
  }
  @Profile("dev")
  static class DevOrder {

    @Bean
    OrderService devOrderService() {
      return name -> "hello dev " + name;
    }
  }
}

아까 위의 코드에서 클래스 2개가 추가 되었다. 하나의 클래스는 Profile이 default 일때 또 다른 한개의 클래스는 dev일 때 동작한다고 가정해보자. 그리고나서 아래와 같은 메타 어노테이션을 만들어보자.

@Documented
@Inherited
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@SpringJUnitConfig(Application.class)
@ActiveProfiles("dev")
public @interface DevTestConfig {
}

여기에는 @SpringJUnitConfig@ActiveProfiles 애노테이션 두개가 메타 어노테이션으로 정의 되어있다. 이 어노테이션은 dev의 Profile만 적용되는 애노테이션이다. 한번 그렇게 되나 테스트를 해보자.

@DevTestConfig
class DevHelloServiceTest {
  private final OrderService orderService;

  @Autowired
  DevHelloServiceTest(OrderService orderService) {
    this.orderService = orderService;
  }

  @Test
  void hello() {
    assertEquals(orderService.ordered("wonwoo"), "hello dev wonwoo");
  }
}

위에서 보다 시피 dev쪽의 bean은 "hello dev " + name 문자열로 dev라는 문자열이 포함되어 있다. 만약 dev 프로파일이 적용이 되었다면 위의 테스트 코드는 통과해야만 한다. 만약 그렇지 않고 기본 프로파일이 적용된다면 이 테스트는 실패로 돌아간다.
한번 테스트를 돌려보면 이상없이 테스트 코드가 잘 동작하는 것을 볼 수 있다.

우리는 이렇게 junut5로 spring를 테스트하는 코드를 작성해봤다. 그렇게 많이 어려운 내용은 아닌 듯 싶다. 아직 당분간은 회사에서 사용할 일은 없기에 개인적으로만 사용해야겠다.

위의 해당 소스는 여기에 있으니 한번씩 돌려보도록 하자.

junit5 ParameterResolver

오늘은 junit5의 기본적인 사용법만 살펴보자. 예전에 릴리즈 되기 전에 여기에 대충 사용법만 포스팅한적이 있었다. 아주 junit5 의 기본적인 내용만 살펴봤으니 좀 더 많은 내용은 문서를 통해서 확인하면 더 좋을 듯 싶다. 많이 바뀐 내용은 없는 듯 하니 추가할 내용은 없을 것 같다. 기본적으로 class가 public이 아니여도 되고, test 메서드도 public이 아니고 package private 이여도 된다는 것은 동일하다. 아주아주 기본적인 사용법은 예전에 살펴본 내용이므로 생략하자. 그렇게 어려운 내용은 아니니 한번씩 해보면 좋을 것 같다.

ExtendWith

Junit5 에 추가된 어노테이션중에 하나이다. 어노테이션명 그대로 ExtendWith은 뭔가를 확장 시킬 수 있는 그런 어노테이션이다. 실제로 코드는 아래와 같다.

@Target({ ElementType.TYPE, ElementType.METHOD })
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@Repeatable(Extensions.class)
@API(status = STABLE, since = "5.0")
public @interface ExtendWith {

    Class<? extends Extension>[] value();

}

별 다른 내용은 없고 value에는 Extension를 상속한 클래스만 가능하다. 실제로 Extension 인터페이스는 딱히 구현할 것도 없는 마커 인터페이스 이다. Extension를 사용하는 인터페이스도 많으니 한번씩 살펴보도록 하고 오늘할 내용은 ParameterResolver라는 인터페이스를 사용하는법을 알아보도록 하자. ParameterResolver 인터페이스는 파라미터를 컨트롤 할 수 있는 그런 인터페이스이다. 실제로 junit4에서는 무조건 기본생성자가 있어야 하며 test 메서드는 파라미터가 없어야 했었다. 하지만 junit5 부터는 기본생성자뿐만 아니라 test 메서드에도 파라미터가 있어도 된다. 하지만 그 타입에 맞게 구현은 해줘야 한다. 그게 바로 ParameterResolver 인터페이스이다.

public interface ParameterResolver extends Extension {

    boolean supportsParameter(ParameterContext parameterContext, ExtensionContext extensionContext)
            throws ParameterResolutionException;

    Object resolveParameter(ParameterContext parameterContext, ExtensionContext extensionContext)
            throws ParameterResolutionException;
}

ParameterResolver 인터페이스는 다음과 같은 형태이다. supportsParameter 지원가능한 파라미터 타입을 검사하고 resolveParameter메서드는 실제 지원가능한 타입을 조작하는 그런 메서드라고 생각하면 된다. 뭔가 spring과 비슷하다. 약간 비슷할 수 밖에 없는게 junit5 를 개발한 개발자가 spring 팀에도 속해있기 때문이다. 그래서 비슷한 구석이 있을 수 있다.

한번 아주간단하게 구현을 해보자.

class UserInfoParameterResolver implements ParameterResolver {

  @Override
  public boolean supportsParameter(ParameterContext parameterContext, ExtensionContext extensionContext) {
    return (parameterContext.getParameter().getType() == UserInfo.class);
  }

  @Override
  public UserInfo resolveParameter(ParameterContext parameterContext, ExtensionContext extensionContext) {
    return new UserInfo("wonwoo", "wonwoo@test.com");
  }
}

ParameterResolver를 구현한 구현체이다. 간단하게 어떤 동작을 하는지 보는 것이기 때문에 의미 있는 코드는 아니니 참고하기 바란다.

class UserInfo {
  private final String name;
  private final String email;

  UserInfo(String name, String email) {
    this.name = name;
    this.email = email;
  }

  public String getEmail() {
    return email;
  }

  public String getName() {
    return name;
  }
}

다음은 UserInfo를 담는 Object이다. 간단하므로 자세한 내용은 생략한다. 어떻게 사용하는지 한번 살펴보자.

@ExtendWith(UserInfoParameterResolver.class)
class NestedUserInfoTest {

  private final UserInfo userInfo;

  NestedUserInfoTest(UserInfo userInfo) {
    this.userInfo = userInfo;
  }

  @Test
  void user_info_test() {
    assertEquals(userInfo.getName(), "wonwoo");
    assertEquals(userInfo.getEmail(), "wonwoo@test.com");
  }
}

위와 같이 기본 생성자가 없어도 테스트는 통과 한다. 아래와 같이 test 메서드에 파라미터로 UserInfo 타입을 받아도 동일한 결과를 얻을 수 있다.

@ExtendWith(UserInfoParameterResolver.class)
class UserInfoTest {

  @Test
  void user_info_test(UserInfo userInfo) {
    assertEquals(userInfo.getName(), "wonwoo");
    assertEquals(userInfo.getEmail(), "wonwoo@test.com");
  }
}

또한 spring과 비슷하다고 느낀점이 메타 어노테이션도 지원한다. 아래와 같이 특정한 어노테이션을 만든 후에 ExtendWith를 사용해도 된다.

@ExtendWith(UserInfoParameterResolver.class)
@Documented
@Inherited
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface UserInfoExtension {
}

위와 같이 메타 어노테이션을 만들었다면 다음과 같이도 테스트를 할 수 있다.

@UserInfoExtension
class UserInfoTest {

  @Test
  void user_info_test(UserInfo userInfo) {
    assertEquals(userInfo.getName(), "wonwoo");
    assertEquals(userInfo.getEmail(), "wonwoo@test.com");
  }
}

아주 심플하게 테스트를 할 수 있어서 좋은 것 같다. 오늘은 이정도면 기본적인 개념도 좀 더 알 듯 싶고 다음시간에는 좀 더 활용할 수 있도록 Spring 혹은 mockito를 이용해서 테스트를 할 수 있도록 해보자.

물론 전체 소스도 여기에 있다. 좀 더 많은 소스가 있으니 차근차근 살펴봐도 될 듯 싶다.

Spring boot 2.0 ApplicationContextRunner

오늘은 아직 릴리즈 되지는 않았지만 그래도 큰 변화를 없을 것 같아 이렇게 포스팅을 한다. 그 전에 올해 하반기에 자바진영에 많은 변화가 있을 것으로 예상된다. 그중에 이미 릴리즈 된 Junit5 가 첫 번째 변화이고 두 번째로 자바9(9월 21일), spring5 (9월 28일) spring boot 2.0(11월 20일) 등이 릴리즈 될 예정이다. 하지만 예정은 예정일뿐.. 언제 바뀔지는 모른다. spring5 와 spring boot 2 는 모두 자바 8 이상에서만 작동한다. 그러니 이제는 자바8을 꼭 쓰도록 하자. 그건 그렇고 java EE가 이제는 이클립스재단으로 간다는.. 좋은 소식(?)

아무튼 오늘은 Spring boot 2 에 추가된 ApplicationContextRunner에 대해서 살펴보도록 하자. ApplicationContextRunner의 용도는 테스트로 사용하는 클래스인데 특정한 설정 정보를 불러와서 테스트를 하기에 적합한 클래스이다. 또 한 좀 더 모던한 스타일을 제공하므로 java8의 문법(람다 표현식) 을 사용할 수 도 있고 AssertJ 스타일의 assertions을 제공해 주고 있다. 그나저나 Spring boot가 이제는 AssertJ를 밀고 있는 듯 하다.

ApplicationContextRunner 말고도 WebApplicationContextRunner, ReactiveWebApplicationContextRunner 등이 존재하나 거의 비슷하지 않을까 싶다. 예제를 살펴보면서 해보도록 하자.

public class ApplicationContextRunnerTest {

  private final ApplicationContextRunner contextRunner = new ApplicationContextRunner();

  @Test
  public void fooTest() {
    contextRunner.withUserConfiguration(FooConfiguration.class)
        .run(context -> {
          assertThat(context).hasBean("foo");
          assertThat(context).getBean("foo").isEqualTo("foo");
        });
  }

  @Configuration
  protected static class FooConfiguration {

    @Bean
    public String foo() {
      return "foo";
    }
  }
}

클래스 레벨에 ApplicationContextRunner을 필드로 선언하면 된다. 기본적으로는 AnnotationConfigApplicationContext를 컨테이너로 사용한다. 만약 다른 컨테이너를 사용하고 싶다면 아래와 같이 변경하면 된다.

private final ApplicationContextRunner contextRunner = new ApplicationContextRunner(GenericWebApplicationContext::new);

어떠한 특정한 설정을 추가적으로 하고 싶다면 withUserConfiguration() 메서드를 사용해서 configuration 정보를 등록 시킬 수 있다. withUserConfiguration() 메서드 말고 withConfiguration() 메서드로 존재하는데 하는 역할을 동일해 보인다. 실제 Spring boot가 사용하는 패턴을 보면 withConfiguration() 메서드에는 테스트 클래스 전체의 기본설정을 하고 withUserConfiguration() 메서드는 해당하는 테스트에만 설정이 필요한 경우에만 사용하는 것으로 보인긴 하나 굳이 뭐 그럴 필요는 없어 보인다.

예를 들면 다음과 같다.

public class CacheApplicationTests {

  private final ApplicationContextRunner contextRunner = new ApplicationContextRunner()
      .withConfiguration(AutoConfigurations.of(CacheAutoConfiguration.class));

  @Test
  public void simpleCacheTest() {
    contextRunner.withUserConfiguration(DefaultConfiguration.class)
        .run(context -> {
          assertThat(context).hasBean("cacheManager");
          assertThat(context).getBean(CacheManager.class).isInstanceOf(ConcurrentMapCacheManager.class);
        });
  }

  @Configuration
  @EnableCaching
  static class DefaultConfiguration {

  }
}

이렇게 테스트 클래스의 기본 설정인 CacheAutoConfiguration을 기본적으로 등록하고 나머지 각각의 테스트에는 그에 맞게 설정 정보를 작성하면 된다. 뭐가 좀 다르긴 하나? 코드로 봐서는 다른 걸 못느끼겠지만..

ApplicationContextRunner에 프로퍼티도 등록하여 테스트 할 수 있는데 withPropertyValues() 메서드를 사용해서 pairs 정보를 String으로 넣으면 된다.

public class CacheApplicationTests {

  private final ApplicationContextRunner contextRunner = new ApplicationContextRunner()
      .withConfiguration(AutoConfigurations.of(CacheAutoConfiguration.class));

  @Test
  public void simpleCacheTest() {
    contextRunner.withUserConfiguration(DefaultConfiguration.class)
        .withPropertyValues("spring.cache.type=simple")
        .run(context -> {
          assertThat(context).hasBean("cacheManager");
          assertThat(context).getBean(CacheManager.class).isInstanceOf(ConcurrentMapCacheManager.class);
        });
  }

  @Test
  public void caffeineCacheTest() {
    contextRunner.withUserConfiguration(DefaultConfiguration.class)
        .withPropertyValues("spring.cache.type=caffeine")
        .run(context -> {
          assertThat(context).hasBean("cacheManager");
          assertThat(context).getBean(CacheManager.class).isInstanceOf(CaffeineCacheManager.class);
        });

  }

  @Configuration
  @EnableCaching
  static class DefaultConfiguration {

  }
}

위와 같이 withPropertyValues() 메서드를 사용해서 cache.type을 caffeine으로 등록하게 하였다. 이외에도 withParent(), withClassLoader(), withSystemProperties() 가 존재한다. 아직은 자주 사용될 만한 메서드는 아닌 것 같아서 생략한다. 주로 사용될 메서드는 withUserConfiguration(), withConfiguration(), withPropertyValues() 메서드이기 때문에 이 세가지만 봐도 아직은 괜찮을 것 같다. (물론 필자 생각)

Spring boot는 test에도 관심을 많이 가져준다. boot 1.4 부터 아주 다양하고 사용하기 쉽게 테스트를 도와주는 클래스들이 많으니 1.4부터 차근차근 보는 것을 추천한다. 조만간 나올 spring5 에서 reactive stream 을 지원하니 필자도 공부좀 해놔야 겠다. 당장은 운영에서는 사용할 일이 없더라도 사용할 일이 생긴다면 별 무리 없이 사용하기 위해 봐야겠다. 예전부터 관심은 있었지만 공부를 계속 미루다보니 나올때 공부를 시작하는거 아닐까 싶다.

오늘은 이렇게 Spring boot 2.0에 작은 변화를 살펴봤다. 아주 많은 변화가 있을 예정이지만 언제 다 살펴보지?