はじめに
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);
}
}
@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
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の設定を除外すれば、認証を除いて通常どおりに動かせます。
@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 {};
}
@Controller
@RequestMapping("/users")
public class UserController {
@PutMapping
void updateStatus(@RequestParam("id") long id, @RequestParam("status") UserStatus status) {
...
}
}
ここでは単純に、パラメタの型が一致しないリクエストはbad requestになることをテストします。
@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をつかって、自分がつくったクラスを直接パラメタとして提供するやり方を紹介します。
public class UpdateUserRequest {
private final long id;
private final UserStatus status;
...
}
public boolean update(UpdateUserRequest updateRequest) {
...
}
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);
}
}
@ParameterizedTest
@ArgumentSource(UpdateUserRequestsProvider.class)
void shouldReturnTrue(UpdateUserRequest updateRequest) {
boolean actual = userService.update(updateRequest);
assertTrue(actual);
}
このやり方の場合、同じパターンのパラメター化テストをするときに、再利用しやすいです。
再利用しなかったとしても、パラメタ生成をテストコードと分離できます。パラメタ生成が複雑な場合、分離した方が、可読性・保守性を維持しやすいです。
パラメタ+期待値
たとえば、消費税の計算で、端数切り捨てされるかどうかの、境界値テストをしたい場合です。
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を表現できます。
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;
}
}
@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も充実しています。大抵のことは、保守性・可読性を維持したテストコードを実装できそうです。
参考