JUnit

JUnit5でBean Validationのテストを行ってみた

Spring Bootを利用したWebアプリケーションで、Formオブジェクトのチェック処理を行う際はBean Validationを利用するが、Bean Validationを用いたチェック処理のテストも、JUnitを利用して行える。

今回は、JUnit5でBean Validationを用いたチェック処理を行ってみたので、そのサンプルプログラムを共有する。

前提条件

下記記事の実装が完了していること。

JUnit5でコントローラのテストを行うMockMvcや、APIのテストを行うMockRestServiceServerを利用してみた以前このブログで、MockMvcを利用してコントローラクラスのテストを行うプログラムを紹介したが、コントローラクラス内でAPI呼び出しを...

作成したサンプルプログラムの内容

作成したサンプルプログラムの構成は以下の通り。
サンプルプログラムの構成
なお、上記の赤枠は、このブログで掲載するソースコードである。

Formクラスの内容は以下の通りで、各種アノテーションを利用してチェック処理を実装している。

package com.example.demo;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

import java.util.LinkedHashMap;
import java.util.Map;

import javax.validation.constraints.AssertTrue;
import javax.validation.constraints.NotEmpty;
import javax.validation.constraints.Pattern;

import org.springframework.util.StringUtils;

import com.example.demo.check.CheckDate;

@Data
@NoArgsConstructor
@AllArgsConstructor
@CheckDate(dtYear = "birthY", dtMonth = "birthM", dtDay = "birthD"
, message = "{validation.date-invalidate}")
public class DemoForm {

    /** ID */
    @Pattern(regexp = "^[1-9][0-9]*$", message = "{validation.id-positive-int}")
    private String id;

    /** 名前 */
    @NotEmpty
    private String name;

    /** 生年月日_年 */
    private String birthY;

    /** 生年月日_月 */
    private String birthM;

    /** 生年月日_日 */
    private String birthD;

    /** 性別 */
    private String sex;

    /** メモ */
    private String memo;

    /** 生年月日_月のMapオブジェクト */
    public Map<String,String> getMonthItems(){
        Map<String, String> monthMap = new LinkedHashMap<String, String>();
        for(int i = 1; i <= 12; i++){
            monthMap.put(String.valueOf(i), String.valueOf(i));
        }
        return monthMap;
    }

    /** 生年月日_日のMapオブジェクト */
    public Map<String,String> getDayItems(){
        Map<String, String> dayMap = new LinkedHashMap<String, String>();
        for(int i = 1; i <= 31; i++){
            dayMap.put(String.valueOf(i), String.valueOf(i));
        }
        return dayMap;
    }

    /** 性別のMapオブジェクト */
    public Map<String,String> getSexItems(){
        Map<String, String> sexMap = new LinkedHashMap<String, String>();
        sexMap.put("1", "男");
        sexMap.put("2", "女");
        return sexMap;
    }
    
    /**
     * 生年月日の年・月・日が入力されているかをチェックする
     * @return チェック結果
     */
    @AssertTrue(message = "{validation.date-empty}")
    public boolean isBirthDayRequired(){
        if(!StringUtils.hasText(birthY)
                && !StringUtils.hasText(birthM)
                && !StringUtils.hasText(birthD)){
            return false;
        }
        return true;
    }

    /**
     * 性別が不正な値でないかチェックする
     * @return チェック結果
     */
    @AssertTrue(message = "{validation.sex-invalidate}")
    public boolean isSexInvalid(){
        return !StringUtils.hasText(sex) 
            || getSexItems().keySet().contains(sex);
    }

}

また、生年月日のチェック処理を行うアノテーションの定義は、以下の通り。

package com.example.demo.check;

import javax.validation.Constraint;
import javax.validation.Payload;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Documented;
import java.lang.annotation.Repeatable;

//RetentionPolicyはclassファイルに記録され実行時に参照できるモード(Runtime)とする
//JavaDoc指定対象(@Documented)とする
//バリデーションの実装クラスはCheckDateValidatorクラスとする
//本アノテーションによるチェック処理を、1クラスで複数回実施できるように設定
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Constraint(validatedBy={CheckDateValidator.class})
@Repeatable(CheckDateAnnotation.class)
public @interface CheckDate {
    //表示するエラーメッセージ(アノテーション属性で指定)
    String message();
    //特定のバリデーショングループがカスタマイズできるような設定
    Class<?>[] groups() default {};
    //チェック対象のオブジェクトになんらかのメタ情報を与えるためだけの宣言
    Class<? extends Payload>[] payload() default {};

    //チェック対象の日付_年(アノテーション属性で指定)
    String dtYear();
    //チェック対象の日付_月(アノテーション属性で指定)
    String dtMonth();
    //チェック対象の日付_日(アノテーション属性で指定)
    String dtDay();
}
package com.example.demo.check;

import java.lang.annotation.Documented;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;

//CheckDateアノテーションチェック処理を
//1クラスで複数回実施できるように設定
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface CheckDateAnnotation {
    CheckDate[] value();
}
package com.example.demo.check;

import org.springframework.beans.BeanWrapper;
import org.springframework.beans.BeanWrapperImpl;
import org.springframework.util.StringUtils;
import javax.validation.ConstraintValidator;
import javax.validation.ConstraintValidatorContext;
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import java.time.format.ResolverStyle;

public class CheckDateValidator implements ConstraintValidator<CheckDate, Object> {

    /** 日付のフォーマット */
    private final static String dateFormat = "uuuuMMdd";

    /** アノテーションで指定した年・月・日・メッセージの項目名 */
    private String dtYear;
    private String dtMonth;
    private String dtDay;
    private String message;

    @Override
    public void initialize(CheckDate annotation) {
        //アノテーションで指定した年・月・日・メッセージの項目名を取得
        this.dtYear = annotation.dtYear();
        this.dtMonth = annotation.dtMonth();
        this.dtDay = annotation.dtDay();
        this.message = annotation.message();
    }

    @Override
    public boolean isValid(Object value, ConstraintValidatorContext context) {
        //BeanWrapperオブジェクトを生成
        BeanWrapper beanWrapper = new BeanWrapperImpl(value);
        //アノテーションで指定した年・月・日の項目値を取得
        String year = (String)beanWrapper.getPropertyValue(dtYear);
        String month = (String)beanWrapper.getPropertyValue(dtMonth);
        String day = (String)beanWrapper.getPropertyValue(dtDay);

        //年・月・日がすべて空白値の場合はtrueを返す
        if(!StringUtils.hasText(year) && !StringUtils.hasText(month)
                && !StringUtils.hasText(day)){
            return true;
        }
        //年・月・日が存在する日付でなければ、エラーメッセージ・エラー項目を設定し
        //falseを返す。そうでなければtrueを返す
        String dateStr = year + addZero(month) + addZero(day);
        if(!isCorrectDate(dateStr, dateFormat)){
            context.disableDefaultConstraintViolation();
            context.buildConstraintViolationWithTemplate(message)
                    .addPropertyNode(dtYear)
                    .addConstraintViolation();
            return false;
        }
        return true;
    }

    /**
     * DateTimeFormatterを利用して日付チェックを行う
     * @param dateStr チェック対象文字列
     * @param dateFormat 日付フォーマット
     * @return 日付チェック結果
     */
    private static boolean isCorrectDate(String dateStr, String dateFormat){
        if(!StringUtils.hasText(dateStr) || !StringUtils.hasText(dateFormat)){
            return false;
        }
        //日付と時刻を厳密に解決するスタイルで、DateTimeFormatterオブジェクトを作成
        DateTimeFormatter df = DateTimeFormatter.ofPattern(dateFormat)
                .withResolverStyle(ResolverStyle.STRICT);
        try{
            //チェック対象文字列をLocalDate型の日付に変換できれば、チェックOKとする
            LocalDate.parse(dateStr, df);
            return true;
        }catch(Exception e){
            return false;
        }
    }

    /**
     * 数値文字列が1桁の場合、頭に0を付けて返す
     * @param intNum 数値文字列
     * @return 変換後数値文字列
     */
    private static String addZero(String intNum){
        if(!StringUtils.hasText(intNum)){
            return intNum;
        }
        if(intNum.length() == 1){
            return "0" + intNum;
        }
        return intNum;
    }
}

さらに、エラーメッセージを定義するプロパティファイルの内容は、以下の通り。

#メッセージ
javax.validation.constraints.NotEmpty.message={0}を入力してください。
validation.id-positive-int={0}は1以上の数値で入力してください。
validation.date-empty={0}を入力してください。
validation.date-invalidate=生年月日が存在しない日付になっています。
validation.empty-msg=
validation.sex-invalidate=性別に不正な値が入っています。

#フィールド名
id=ID
name=名前
sex=性別
birthDayRequired=生年月日



Formクラスのテストクラスの内容は以下の通りで、Validatorクラスを用いてテストを行っている。

package com.example.demo;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;

import org.springframework.validation.Validator;

import org.junit.jupiter.api.BeforeEach;
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;

@SpringBootTest
public class DemoFormTest {

    /** Validatorオブジェクト */
    @Autowired
    private Validator validator;

    /** 検証結果を設定するBindingResult */
    private BindingResult bindingResult;

    /**
     * 各テストメソッドを実行する前に実行する処理
     */
    @BeforeEach
    public void setUp() {
        bindingResult = new BindException(new DemoForm(), "demoForm");
    }

    /**
     * Formオブジェクト(エラー無し)の検証を行うテストを実行する
     */
    @Test
    public void testHasNoError() {
        System.out.println("*** testHasNoErrorメソッド 開始 ***");

        // テスト対象となる検証対象のFormオブジェクト(エラー無し)を生成
        DemoForm demoForm 
            = new DemoForm("3", "テスト プリン3", "2013", "2", "1", "1", "テスト3");
        System.out.println("demoForm : " + demoForm);

        // 生成したFormオブジェクトを検証
        validator.validate(demoForm, bindingResult);

        // Formオブジェクトの検証結果を確認
        assertFalse(bindingResult.hasErrors());
        System.out.println("bindingResult.hasErrors()の値 : false");

        System.out.println("*** testHasNoErrorメソッド 終了 ***");
    }

    /**
     * Formオブジェクト(IDでエラー)の検証を行うテストを実行する
     */
    @Test
    public void testHasIdError() {
        System.out.println("*** testHasIdErrorメソッド 開始 ***");

        DemoForm demoForm 
            = new DemoForm("3a", "テスト プリン3", "2013", "2", "1", "1", "テスト3");
        System.out.println("demoForm : " + demoForm);

        validator.validate(demoForm, bindingResult);

        assertTrue(bindingResult.hasErrors());
        assertEquals("{0}は1以上の数値で入力してください。"
            , bindingResult.getFieldError().getDefaultMessage());
        assertEquals("id", bindingResult.getFieldError().getField());
        System.out.println("bindingResultの設定値 : " 
            + bindingResult.getFieldError().toString());

        System.out.println("*** testHasIdErrorメソッド 終了 ***");
    }

    /**
     * Formオブジェクト(名前でエラー)の検証を行うテストを実行する
     */
    @Test
    public void testHasNameError() {
        System.out.println("*** testHasNameErrorメソッド 開始 ***");

        DemoForm demoForm 
            = new DemoForm("3", "", "2013", "2", "1", "1", "テスト3");
        System.out.println("demoForm : " + demoForm);

        validator.validate(demoForm, bindingResult);

        assertTrue(bindingResult.hasErrors());
        assertEquals("{0}を入力してください。"
            , bindingResult.getFieldError().getDefaultMessage());
        assertEquals("name", bindingResult.getFieldError().getField());
        System.out.println("bindingResultの設定値 : " 
            + bindingResult.getFieldError().toString());

        System.out.println("*** testHasNameErrorメソッド 終了 ***");
    }

    /**
     * Formオブジェクト(生年月日でエラー)の検証を行うテストを実行する
     */
    @Test
    public void testHasBirthdayError() {
        System.out.println("*** testHasBirthdayErrorメソッド 開始 ***");

        DemoForm demoForm 
            = new DemoForm("3", "テスト プリン3", "2013", "2", "31", "1", "テスト3");
        System.out.println("demoForm : " + demoForm);

        validator.validate(demoForm, bindingResult);

        assertTrue(bindingResult.hasErrors());
        assertEquals("生年月日が存在しない日付になっています。"
            , bindingResult.getFieldError().getDefaultMessage());
        assertEquals("birthY", bindingResult.getFieldError().getField());
        System.out.println("bindingResultの設定値 : " 
            + bindingResult.getFieldError().toString());

        System.out.println("*** testHasBirthdayErrorメソッド 終了 ***");
    }

    /**
     * Formオブジェクト(生年月日未入力でエラー)の検証を行うテストを実行する
     */
    @Test
    public void testHasNoBirthdayError() {
        System.out.println("*** testHasNoBirthdayErrorメソッド 開始 ***");

        DemoForm demoForm 
            = new DemoForm("3", "テスト プリン3", "", "", "", "1", "テスト3");
        System.out.println("demoForm : " + demoForm);

        validator.validate(demoForm, bindingResult);

        assertTrue(bindingResult.hasErrors());
        assertEquals("{0}を入力してください。"
            , bindingResult.getFieldError().getDefaultMessage());
        assertEquals("birthDayRequired", bindingResult.getFieldError().getField());
        System.out.println("bindingResultの設定値 : " 
            + bindingResult.getFieldError().toString());

        System.out.println("*** testHasNoBirthdayErrorメソッド 終了 ***");
    }

    /**
     * Formオブジェクト(性別でエラー)の検証を行うテストを実行する
     */
    @Test
    public void testHasSexError() {
        System.out.println("*** testHasSexErrorメソッド 開始 ***");

        DemoForm demoForm 
            = new DemoForm("3", "テスト プリン3", "2013", "2", "1", "3", "テスト3");
        System.out.println("demoForm : " + demoForm);

        validator.validate(demoForm, bindingResult);

        assertTrue(bindingResult.hasErrors());
        assertEquals("性別に不正な値が入っています。"
            , bindingResult.getFieldError().getDefaultMessage());
        assertEquals("sexInvalid", bindingResult.getFieldError().getField());
        System.out.println("bindingResultの設定値 : " 
            + bindingResult.getFieldError().toString());

        System.out.println("*** testHasSexErrorメソッド 終了 ***");
    }

    /**
     * Formオブジェクト(複数箇所でエラー)の検証を行うテストを実行する
     */
    @Test
    public void testMultiError() {
        System.out.println("*** testMultiErrorメソッド 開始 ***");

        DemoForm demoForm 
            = new DemoForm("3a", "", "2013", "2", "31", "3", "テスト3");
        System.out.println("demoForm : " + demoForm);

        validator.validate(demoForm, bindingResult);

        assertTrue(bindingResult.hasErrors());
        assertEquals(4, bindingResult.getFieldErrors().size());
        assertEquals("{0}は1以上の数値で入力してください。"
            , bindingResult.getFieldErrors("id").get(0).getDefaultMessage());
        assertEquals("{0}を入力してください。"
            , bindingResult.getFieldErrors("name").get(0).getDefaultMessage());
        assertEquals("生年月日が存在しない日付になっています。"
            , bindingResult.getFieldErrors("birthY").get(0).getDefaultMessage());
        assertEquals("性別に不正な値が入っています。"
            , bindingResult.getFieldErrors("sexInvalid").get(0).getDefaultMessage());

        System.out.println("エラー数: 4");
        System.out.println("bindingResultの設定値 : " + bindingResult.getAllErrors());

        System.out.println("*** testMultiErrorメソッド 終了 ***");
    }

}

その他のソースコード内容は、以下のサイトを参照のこと。
https://github.com/purin-it/java/tree/master/junit5-bean-validation/demoRestApiCallWeb



「FlexClip」はテンプレートとして利用できる動画・画像・音楽などが充実した動画編集ツールだったテンプレートとして利用できるテキスト・動画・画像・音楽など(いずれも著作権フリー)が充実している動画編集ツールの一つに、「FlexCli...

テスト対象プログラムの実行結果

テスト対象プログラムの実行結果は、以下の通り。

1) 前提条件となるRest APIサービスのSpring Bootアプリケーションを起動後、今回作成したプロジェクトのSpring Bootアプリケーションを起動し、「http:// localhost:8084/」とアクセスすると、以下の画面が表示される。
テスト対象プログラムの実行結果_1

2) 上記画面で「データ追加」ボタンを押下すると、以下の画面に遷移する。
テスト対象プログラムの実行結果_2

3) 何も入力せず「登録」ボタンを押下すると、以下のように、エラーメッセージが表示されることが確認できる。
テスト対象プログラムの実行結果_3



Code VillageはJavaScriptを中心としたサポート体制が充実したプログラミングスクールだったJavaScriptや、JavaScriptのフレームワーク「React」「Vue」を中心にオンラインで学習できるプログラミングスクール...

テストプログラムの実行結果

テストプログラムの実行結果は、以下の通り。

1) testHasNoErrorメソッドを実行した結果は以下の通りで、Formクラスでエラーが無いことが確認できる。
テストプログラムの実行結果_1

2) testHasIdErrorメソッドを実行した結果は以下の通りで、IDの数値チェックエラーが発生していることが確認できる。
テストプログラムの実行結果_2

3) testHasNameErrorメソッドを実行した結果は以下の通りで、名前の必須チェックでエラーが発生していることが確認できる。
テストプログラムの実行結果_3

4) testHasBirthdayErrorメソッドを実行した結果は以下の通りで、生年月日の不正チェックでエラーが発生していることが確認できる。
テストプログラムの実行結果_4

5) testHasNoBirthdayErrorメソッドを実行した結果は以下の通りで、生年月日の必須チェックでエラーが発生していることが確認できる。
テストプログラムの実行結果_5

6) testHasSexErrorメソッドを実行した結果は以下の通りで、性別の不正チェックでエラーが発生していることが確認できる。
テストプログラムの実行結果_6

7) testMultiErrorメソッドを実行した結果は以下の通りで、複数のエラーが発生していることが確認できる。
テストプログラムの実行結果_7

要点まとめ

  • Bean Validationを利用したFormオブジェクトのチェック処理も、JUnitを利用してテストを行うことができる。