LoginSignup
3
2

More than 1 year has passed since last update.

JUnit5のParameterizedTestで網羅的なテストをする

Posted at

はじめに

JUnit4では、テストクラス単位でパラメータ化テストできましたが、JUnit5から、テストメソッド単位でできるようになりました。パラメタの提供方法も柔軟になり、パラメタのとりうるパターンを網羅するテストコードが書きやすくなっています。(JUnit4とJUnit5との比較はこちらの記事が参考になります。)
この記事では、JUnit5での異常値、境界値などの網羅性テストを書くためのサンプルコードを紹介していきます。

パラメタがひとつのとき

EnumSource

ステータスをEnumで実装している場合、EnumSourceをつかえば、Enumの全要素をテストすることができます。

public enum UserStatus {
   ACTIVE,
   PENDING,
   INACTIVE;
}


public class UserService {

   private final UserRepository userRepository;

   public void updateStatus(UserStatus status) {
          userRepository.update(status);
   }

}
UserServiceTest.java
@ParameterizedTest
@EnumSource
void shouldCallUpdate(UserStatus status) {
     userService.update(status);

     verify(userRepository, times(1)).update(status);
}

ValueSource

以下に挙げるような、整数や文字列など、Javaの標準機能で提供されている型であれば、ValueSource

  • short
  • byte
  • int
  • long
  • float
  • double
  • char
  • boolean
  • java.lang.String
  • java.lang.Class
UserService.java
public void rename(String newName) {
     userRepository.updateName(newName);
}
@ParameterizedTest
@ValueSource(strings={"test", "テスト", "てすと", "試験"})
void shouldCallUpdateName(String newName) {
     userService.rename(newName);

     verify(userRepository, times(1)).updateName(newName);
}

パラメタが複数のとき

CsvSource

たとえば、パラメタが複数ある画面で、認証部分を除いて、パラメタのバリデーションについてのテストをしたいときです。

spring bootの @WebMvcTestからsecurityの設定を除外すれば、認証を除いて通常どおりに動かせます。

UnsecuredWebMvcTest.java
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@WebMvcTest(excludeAutoConfiguration = {SecurityAutoConfiguration.class, SecurityFilterAutoConfiguration.class})
public @interface UnsecuredWebMvcTest {
    @AliasFor(annotation = WebMvcTest.class, attribute = "controllers")
    Class<?>[] value() default {};

    @AliasFor(annotation = WebMvcTest.class, attribute = "controllers")
    Class<?>[] controllers() default {};
}
UserController.java
@Controller
@RequestMapping("/users")
public class UserController {
   
   @PutMapping
   void updateStatus(@RequestParam("id") long id, @RequestParam("status") UserStatus status) {
       ...
   }

}

ここでは単純に、パラメタの型が一致しないリクエストはbad requestになることをテストします。

UserControllerTest.java
@UnsecuredWebMvcTest
public class UserControllerTest {
  ... 

  @ParameterizedTest
  @CsvSource({
     // 片方は正しい
     ",ACTIVE",
     "xxx,ACTIVE",
     "1,"
     "1,yyy",
     // 両方まちがっている
     "xxx,yyy"
     ","  
  })
  void shouldReturnBadRequestIfInvalidParameter(String id, String status) {
    MockHttpServletRequestBuilder request = MockMvcRequestBuilders.put("/users")
                                                .param("id", id)
                                                .param("status", status);
    
    mockMvc.perform(request)
                    .andDo(print())
                    .andExpect(status().isBadRequest());
  }

}

ArgumentSource + ArgumentsProvider

複数パラメタをひとつのクラスをまとめている場合、CsvSource単独では難しいです。
ArgumentsAggregator と組みあわせて、CsvSourceで定義したパラメタから、自分がつくったクラスのインスタンスを生成するやり方もあります。

ここでは、ArgumentsProviderをつかって、自分がつくったクラスを直接パラメタとして提供するやり方を紹介します。

UpdateUserRequest.java
public class UpdateUserRequest {
   
   private final long id;

   private final UserStatus status;
   
   ...
}
UserService.java
public boolean update(UpdateUserRequest updateRequest) {
    ...
}
UpdateUserRequestsProvider.java
public class UpdateUserRequestsProvider implements ArgumentsProvider {

   @Override
    public Stream<? extends Arguments> provideArguments(ExtensionContext context) {
        return Arrays.stream(UserStatus.values())
                     .map(status -> UpdateUserRequest.of(1L, status))
                     .map(Arguments::of);
    }
}
UserServiceTest.java
@ParameterizedTest
@ArgumentSource(UpdateUserRequestsProvider.class)
void shouldReturnTrue(UpdateUserRequest updateRequest) {
     boolean actual = userService.update(updateRequest);

     assertTrue(actual);  
}

このやり方の場合、同じパターンのパラメター化テストをするときに、再利用しやすいです。
再利用しなかったとしても、パラメタ生成をテストコードと分離できます。パラメタ生成が複雑な場合、分離した方が、可読性・保守性を維持しやすいです。

パラメタ+期待値

たとえば、消費税の計算で、端数切り捨てされるかどうかの、境界値テストをしたい場合です。

Calculator.java
public int tax(int price) {
   return (int) Math.floor(price * 0.10); 
}

この場合、テストの呼び出し方は、どのパラメタも同じだけど、パラメタによって、期待値が変わります。
CsvSourceなどで、パラメタと期待値のペアを提供することで、テストコードが冗長になるのをおさえられます。

@ParameterizedTest
@CsvSource({
   "9,0",
   "10,1",
   "100,10"
})
void shouldReturnTax(int price, int expected) {
     int actual = calculator.tax(price);

     assertEquals(expected, actual);
}

パラメタの状態+パラメタ

パラメタの状態に応じて、assertionなどのテストの内容が一部かわる場合、「どういったパラメタか」もあわせて提供すると、テストが書きやすくなることがあります。
Assumptions と組み合わせると、テストメソッド内で、特定のケースだけのassertionを表現できます。

UserListsProvider.java
public class UserListsProvider implements ArgumentsProvider {

    @Override
    public Stream<? extends Arguments> provideArguments(ExtensionContext context) {
        return Stream.of(
             Arguments.of(Pattern.EMPTY, Collections.emptyList()),
             Arguments.of(Pattern.ONLY_ACTIVE, Collections.singletonList(User.active(...))),
             Arguments.of(Pattern.BOTH_INACTIVE_ACTIVE, Arrays.asList(User.active(...), User.inactive(...)))
        );
    }

    public enum Pattern {
         EMPTY,
         ONLY_ACTIVE,
         BOTH_ACTIVE_INACTIVE;
         
    }
}
UserServiceTest.java
@ParameterizedTest
@ArgumentSource(UserListsProvider.class)
void shouldReturnTrueIfOnlyActiveUsers(Pattern pattern, List<User> users) {
    assumingThat(Pattern.ONLY_ACTIVE == pattern || Pattern.EMPTY == pattern, () -> {
          int actual = userService.inactivate(users); 
          assertEquals(users.size(), actual);
    }):

    assumingThat(Pattern.BOTH_INACTIVE_ACTIVE == pattern, () -> {
          assertThrows(IllegalArgumentException.class , userService.inactivate(users));
    }):
    
}

まとめ

JUnit5のUser Guideも充実しています。大抵のことは、保守性・可読性を維持したテストコードを実装できそうです。

参考

3
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
2