はじめに
こんにちは!ゼロからJavaであそぶシリーズ第5弾です。
今回は、前回作ったバリデーションのテストクラスを製作します。
ネタバレすると、最初に失敗例があるタイプのQiita投稿です。さくっとパラメータ化テストの実例が見たい方は【◎良い例】まで飛んでください(笑)
前回までの記事はこちら↓
前回までのあらすじ
現状のバリデーション要件は以下の通りです。フォームクラスにアノテーションを付与して以下の文字種チェック、文字数チェックを行っています。
- 使用可能文字種:半角英数のみ
- 文字数制限:4文字以上
使用環境とバージョン
- macOS Catalina
- jdk14.0.1
- JUnit 5
- Maven 3.6.3_1
- STS 4.6.1
- Spring Boot 2.3.1
- spring-boot-starter-thymeleaf-2.3.1
【△良くない例①】テストする変数の設定箇所以外がコピペで冗長
フォームクラスでバリデートしているのでフォームクラスのテストクラスを作成します。
今回テストしたいケースは以下の4つです。
- 正常系(エラーなし)
- 異常系(文字数不正)
- 異常系(文字種不正)
- 異常系(文字数不正&文字種不正)
しかし、正常系でも半角英数を許可しているので、より厳密なテストを行うのならば、数字だけ、英字だけ、英数、というデータパターンが欲しいところ。
文字種不正の異常系も、ひらがな、カタカナ、漢字、全角英数、記号などなどのデータパターンやっておきたいですよね…1
以下のコードは、正常系3パターン、文字数不正2パターン、文字種不正2パターンを取り急ぎ作成したものです。
これでもJUnitテストはちゃんと通るのですが、テスト実施や結果検証は同じコードの繰り返し(ボイラープレートコード)であるため、とても冗長ですね…
しかもまだまだ異常系の漢字やひらがな、文字種混合のパターン、文字数と文字種が共に不正となる異常系などは実装されていない状態です。このまま完成させようとしたら、さらにコード量は爆増します。
package com.example.form;
import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assertions.assertNull;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.validation.BindException;
import org.springframework.validation.BindingResult;
import org.springframework.validation.Validator;
@SpringBootTest
public class EchoFormTest {
@Autowired
Validator validator;
private EchoForm testEchoForm = new EchoForm();
private BindingResult bindingResult = new BindException(testEchoForm, "echoForm");
/**
* 正常系
*/
@Test
public void test_echo_正常系_英字4字以上() {
// テスト準備
testEchoForm.setName("aaaa");
// テスト実施
validator.validate(testEchoForm, bindingResult);
// 結果検証
assertNull(bindingResult.getFieldError());
}
@Test
public void test_echo_正常系_数字4字以上() {
// テスト準備
testEchoForm.setName("1111");
// テスト実施
validator.validate(testEchoForm, bindingResult);
// 結果検証
assertNull(bindingResult.getFieldError());
}
@Test
public void test_echo_正常系_英数4字以上() {
// テスト準備
testEchoForm.setName("aa11");
// テスト実施
validator.validate(testEchoForm, bindingResult);
// 結果検証
assertNull(bindingResult.getFieldError());
}
/**
* 異常系_文字数不足
*/
@Test
public void test_echo_異常系_半角英3文字() {
// テスト準備
testEchoForm.setName("aaa");
// テスト実施
validator.validate(testEchoForm, bindingResult);
// 結果検証
assertThat(bindingResult.getFieldError().toString()).contains("4文字以上でなくてはいけません。");
}
@Test
public void test_echo_異常系_半角数3文字() {
// テスト準備
testEchoForm.setName("111");
// テスト実施
validator.validate(testEchoForm, bindingResult);
// 結果検証
assertThat(bindingResult.getFieldError().toString()).contains("4文字以上でなくてはいけません。");
}
/**
* 異常系_文字種不正
*/
@Test
public void test_echo_異常系_ひらがな() {
// テスト準備
testEchoForm.setName("ああああ");
// テスト実施
validator.validate(testEchoForm, bindingResult);
// 結果検証
assertThat(bindingResult.getFieldError().toString()).contains("半角英数のみ有効です。");
}
@Test
public void test_echo_異常系_カタカナ() {
// テスト準備
testEchoForm.setName("アアアア");
// テスト実施
validator.validate(testEchoForm, bindingResult);
// 結果検証
assertThat(bindingResult.getFieldError().toString()).contains("半角英数のみ有効です。");
}
}
【×良くない例②】データパターンをfor文ループする
「テスト実施と結果検証は同じコードで、テストデータだけ変えたい…
そうだ!テストデータを配列に入れて、for文でデータごとに実施と結果検証を行おう!」
Javaの入門書を読むと、冗長な記述はfor文で解決しがちなところがあるので、そう思ってしまうのも無理はないです。仕方ないのです。ないと言ってください(←経験者)。
一見すると問題なさそうに見えるコード…
【△良くない例①】よりも短く記述できるfor文…
そして実際にJUnitで動かしても全部通るこのコードの、何が問題か分かりますか?↓
/**
* 正常系
*/
@Test
public void test_echo_正常系() {
String[] data = {"aaaa", "aa11", "1111"};
for(String testCase: data) {
// テスト準備
testEchoForm.setName(testCase);
// テスト実施
validator.validate(testEchoForm, bindingResult);
// 結果検証
assertNull(bindingResult.getFieldError());
}
}
実はこの例、全部のテストデータが正常に終了する場合はいいのですが、途中のデータでテスト失敗した場合に、後続のテストデータが実施されることなくテスト失敗になってしまいます。すべてのデータのテスト実施ができないのです。
例えば、以下のように3データ中2つ目と3つ目でエラーが発生するようにしてみると…
/**
* 正常系
*/
@Test
public void test_echo_正常系() {
String[] dataPattern = {"aaaa", "err", "bad"}; // 3文字以下はエラーになるためerrはこのテストを通らない
for(String testData: dataPattern) {
// テスト準備
testEchoForm.setName(testData);
// テスト実施
validator.validate(testEchoForm, bindingResult);
// 結果検証
assertNull(bindingResult.getFieldError());
}
}
2つ目のデータでテスト失敗してしまうため、ログにも2つ目のエラーのことしか出ていません。3つ目以降のデータのテストがそもそも行われないため、後続のデータが正常か異常かが判別できないのです。
org.opentest4j.AssertionFailedError: expected: <null> but was: <Field error in object 'echoForm' on field 'name': rejected value [err]; codes [Size.echoForm.name,Size.name,Size.java.lang.String,Size]; arguments [org.springframework.context.support.DefaultMessageSourceResolvable: codes [echoForm.name,name]; arguments []; default message [name],2147483647,4]; default message [4文字以上でなくてはいけません。]>
at org.junit.jupiter.api.AssertionUtils.fail(AssertionUtils.java:55)
at org.junit.jupiter.api.AssertNull.failNotNull(AssertNull.java:48)
at org.junit.jupiter.api.AssertNull.assertNull(AssertNull.java:37)
at org.junit.jupiter.api.AssertNull.assertNull(AssertNull.java:32)
at org.junit.jupiter.api.Assertions.assertNull(Assertions.java:258)
at com.example.form.EchoFormTest.test_echo_正常系(EchoFormTest.java:97)
後続に100や200のデータパターンがあったりしたら、、と思うと恐ろしいですね。どれだけ冗長でも、この【×良くない例②】よりは【△良くない例①】の方がテストコードとしての責務を果たすことができるので、まだマシだと言えます。
【◎良い例】パラメータ化テストで実装する
お待たせしました!良い例をご紹介します。パラメータ化テストでの実装です。
/**
* 正常系
*/
@ParameterizedTest
@ValueSource(strings = {"aaaa", "aa11", "1111"})
public void test_echo_正常系(String s) {
// テスト準備
testEchoForm.setName(s);
// テスト実施
validator.validate(testEchoForm, bindingResult);
// 結果検証
assertNull(bindingResult.getFieldError());
}
【△良くない例①】のような冗長なコードはありませんね。
さらにパラメータ化の何がいいかというと、【×良くない例②】で見てきた「後続のテストが実施されない」「どのテストデータが実施されたか分からない」という問題も解決するところです。
以下の実行結果を見ると、3テストデータすべてが正常終了していることが分かります。
また、データを増やし、1つ目と4つ目が正常、2つ目と3つ目のデータが不正の場合も確認してみましょう。
/**
* 正常系
*/
@ParameterizedTest
@ValueSource(strings = {"aaaa", "err", "bad", "1111"}) // errとbadが文字数不足
public void test_echo_正常系(String s) {
// テスト準備
testEchoForm.setName(s);
// テスト実施
validator.validate(testEchoForm, bindingResult);
// 結果検証
assertNull(bindingResult.getFieldError());
}
2つ目でエラーになった後も、3つ目4つ目のデータの検証がきちんとできており、スタックトレースもエラーごとに出るようになりました!JUnit5便利すぎる…
異常系の例も載せておきます。
/**
* 異常系_文字数不足
*/
@ParameterizedTest
@ValueSource(strings = {"aaa", "111", "aa1"})
public void test_echo_異常系_文字数不足(String s) {
// テスト準備
testEchoForm.setName(s);
// テスト実施
validator.validate(testEchoForm, bindingResult);
// 結果検証
assertThat(bindingResult.getFieldError().toString()).contains("4文字以上でなくてはいけません。");
こちらも問題なく検証できました!
下記Qiitaも併せてご参照ください(今回参考にさせていただきました!)。
JUnit 5 のパラメーター化テストは超便利
蛇足になりますが、JUnit 4の場合は以下サイトが参考になります。
https://javaworld.helpfulness.jp/post-81/
自分は@RunWith(Parameterized.class)
を利用して実装しました。コード量はJUnit5よりも多くはなるのですが、JUnit4でも十分パラメータ化テストは実装可能です。また@RunWith(Enclosed.class)
のインナークラスとして作成することで、別のテストランナーとも併用可能です。
おわりに
今回はJUnit 5でのパラメータ化テストについて見ていきました。
良くない例は、パラメータ化テストの存在自体を知らない頃、実際に自分がやってしまった書き方です…
for文でテスト回すの、実際簡単に思いつきやすいので、もしやってしまっている方がいたらぜひパラメータ化テストへの変更をご検討ください!
お読みいただき、ありがとうございました!
-
厳密に言うと今回のフォームの実装はほぼjavaxのバリデーション機能の使用のみなので(独自バリデーションなどを作成していない)、ライブラリ使用側でここまでがっつりテストする必要はないです。これで異常が出たらjavaxライブラリ自体に致命的バグがあるようなレベル。あくまでパラメータ化テストを紹介したいがための前座だと思ってください(笑) ↩