かんがるーさんの日記

最近自分が興味をもったものを調べた時の手順等を書いています。今は Spring Boot をいじっています。

Spring Boot で書籍の貸出状況確認・貸出申請する Web アプリケーションを作る ( その46 )( 貸出承認画面の作成6 )

概要

Spring Boot で書籍の貸出状況確認・貸出申請する Web アプリケーションを作る ( その45 )( 貸出承認画面の作成5 ) の続きです。

  • 今回の手順で確認できるのは以下の内容です。
    • 貸出承認画面の作成
      • テストの作成 ( 2回に分けて書きます )

参照したサイト・書籍

  1. Getting started with Hibernate Validator
    http://hibernate.org/validator/documentation/getting-started/

    • カスタム Bean Validation のテストで、テストメソッド内で Bean Validation を実行する方法を調査した時に参照しました。
  2. com.google.common.base - Class Strings
    http://docs.guava-libraries.googlecode.com/git/javadoc/com/google/common/base/Strings.html

    • Guava の Strings クラスのドキュメントです。
    • "X" を繰り返して生成された 128 文字の文字列を生成する方法を調査した時に参照しました。

目次

  1. テスト作成対象のクラスを決める
  2. printClassWhatNotMakeTest タスクのチェック対象外のパッケージを設定する
  3. ValuesEnumValidator クラスのテストの作成
  4. ValuesHelper クラスの変更
  5. Mail003Helper クラスのテストの作成
  6. LendingapprovalFormValidator クラスのテストの作成

手順

テスト作成対象のクラスを決める

  1. Gradle projects View から printClassWhatNotMakeTest タスクを実行します。

    f:id:ksby:20160119011831p:plain

  2. 出力されたクラスに対して以下の対応を行います。

    • 以下のアノテーションは printClassWhatNotMakeTest タスクのチェック対象外にします。
      • src/main/java/ksbysample/webapp/lending/values/validation/ValuesEnum.java
    • 今回は Spock ではテストを作成しません。
    • 以下のクラスは JUnit でテストを作成します。
      • src/main/java/ksbysample/webapp/lending/values/validation/ValuesEnumValidator.java
      • src/main/java/ksbysample/webapp/lending/helper/mail/Mail003Helper.java
      • src/main/java/ksbysample/webapp/lending/web/lendingapproval/LendingapprovalFormValidator.java
      • src/main/java/ksbysample/webapp/lending/web/lendingapproval/LendingapprovalController.java

printClassWhatNotMakeTest タスクのチェック対象外のパッケージを設定する

  1. build.gradle を リンク先の内容 に変更します。

ValuesEnumValidator クラスのテストの作成

  1. src/main/java/ksbysample/webapp/lending/values/validation の下の ValuesEnumValidator.java で「Create Test」ダイアログを表示し、テストクラスを作成します。

    f:id:ksby:20160120070329p:plain

  2. src/test/java/ksbysample/webapp/lending/values/validation の下に ValuesEnumValidatorTest.java が作成されますので、リンク先の内容 に変更します。

  3. テストを実行します。ValuesEnumValidatorTest クラスのクラス名の左側に表示されているアイコンをクリックしてコンテキストメニューを表示後「Run 'ValuesEnumValidatorTest' with Coverage」を選択します。

    テストが成功することが確認できます。

    f:id:ksby:20160120085421p:plain

※Spock で作りたかったのですが、Spock のテストクラス内で lombok のアノテーションを付加した Values 列挙型を定義できなかったので、JUnit4 で作成しました。Groovy だと lombok のアノテーションが効かないようです。

ValuesHelper クラスの変更

Mail003Helper クラスのテストを作成する前に ValuesHelper クラスを変更します。Mail003Helper クラスを実装した時に以下のように実装したのですが、さすがに <?> はないかな。。。と思ったので、書かなくてすむように変更します。

    @Autowired
    private ValuesHelper<?> vh;

またインナークラスの Mail003BookData のコンストラクタ内で以下のように実装したのですが、Java のソース内ならば第1引数は "LendingBookApprovalResultValues" ではなく LendingBookApprovalResultValues.class と書けた方がよいと思ったので、書けるようにメソッドを追加します。

                this.approvalResultStr = vh.getText("LendingBookApprovalResultValues", lendingBook.getApprovalResult());
  1. src/main/java/ksbysample/webapp/lending/values の下の ValuesHelper.javaリンク先の内容 に変更します。

  2. src/main/java/ksbysample/webapp/lending/helper/mail の下の Mail003Helper.javaリンク先の内容 に変更します。

Mail003Helper クラスのテストの作成

  1. src/main/java/ksbysample/webapp/lending/helper/mail の下の Mail003Helper.java で「Create Test」ダイアログを表示し、テストクラスを作成します。

    f:id:ksby:20160120093436p:plain

  2. src/test/java/ksbysample/webapp/lending/helper/mail の下に Mail003HelperTest.java が作成されますので、リンク先の内容 に変更します。

  3. src/test/resources/ksbysample/webapp/lending/helper/mail/assertdata/003 の下に message.txt を作成します。作成後、リンク先の内容 に変更します。

  4. テストを実行します。Mail003HelperTest クラスのクラス名の左側に表示されているアイコンをクリックしてコンテキストメニューを表示後「Run 'Mail003HelperTest' with Coverage」を選択します。

    テストが成功することが確認できます。

    f:id:ksby:20160120135938p:plain

LendingapprovalFormValidator クラスのテストの作成

  1. テストデータを作成します。src/test/resources/ksbysample/webapp/lending/web の下に lendingapproval ディレクトリを作成します。

  2. src/test/resources/ksbysample/webapp/lending/web/lendingapproval の下に LendingapprovalForm_001.yaml, LendingapprovalForm_002.yaml, LendingapprovalForm_003.yaml, LendingapprovalForm_004.yaml, LendingapprovalForm_005.yaml を作成し、リンク先の内容 に変更します。

  3. テストデータの YAML ファイルを読み込む時に問題があることが判明したため、メソッド名を変更します。src/main/java/ksbysample/webapp/lending/web/lendingapproval の下の LendingapprovalForm.javaリンク先の内容 に変更します。

  4. src/main/java/ksbysample/webapp/lending/web/lendingapproval の下の LendingapprovalFormValidator.java で「Create Test」ダイアログを表示し、テストクラスを作成します。

    f:id:ksby:20160120153714p:plain

  5. src/test/java/ksbysample/webapp/lending/web/lendingapproval の下に LendingapprovalFormValidatorTest.java が作成されますので、リンク先の内容 に変更します。

  6. テストを実行します。LendingapprovalFormValidatorTest クラスのクラス名の左側に表示されているアイコンをクリックしてコンテキストメニューを表示後「Run 'LendingapprovalFormValidatorTe...' with Coverage」を選択します。

    テストが成功することが確認できます。

    f:id:ksby:20160120171822p:plain

ソースコード

build.gradle

task printClassWhatNotMakeTest << {
    def srcDir = new File("src/main/java");
    def excludePaths = [
            "src/main/java/ksbysample/webapp/lending/Application.java"
            , "src/main/java/ksbysample/webapp/lending/config"
            , "src/main/java/ksbysample/webapp/lending/cookie"
            , "src/main/java/ksbysample/webapp/lending/dao"
            , "src/main/java/ksbysample/webapp/lending/entity"
            , "src/main/java/ksbysample/webapp/lending/exception"
            , "src/main/java/ksbysample/webapp/lending/helper/page/PagenationHelper.java"
            , "src/main/java/ksbysample/webapp/lending/security/LendingUser.java"
            , "src/main/java/ksbysample/webapp/lending/security/RoleAwareAuthenticationSuccessHandler.java"
            , "src/main/java/ksbysample/webapp/lending/service/calilapi/response"
            , "src/main/java/ksbysample/webapp/lending/service/file/BooklistCSVRecord.java"
            , "src/main/java/ksbysample/webapp/lending/service/openweathermapapi"
            , "src/main/java/ksbysample/webapp/lending/service/queue/InquiringStatusOfBookQueueMessage.java"
            , "src/main/java/ksbysample/webapp/lending/util/doma"
            , "src/main/java/ksbysample/webapp/lending/util/velocity/VelocityUtils.java"
            , "src/main/java/ksbysample/webapp/lending/values/validation/ValuesEnum.java"
            , "src/main/java/ksbysample/webapp/lending/web/.+/.+Service.java"
            , "src/main/java/ksbysample/webapp/lending/webapi/common/CommonWebApiResponse.java"
            , "src/main/java/ksbysample/webapp/lending/webapi/weather"
    ];
    def excludeFileNamePatterns = [
            ".*EventListener.java"
            , ".*Dto.java"
            , ".*Form.java"
            , ".*Values.java"
    ];

    compareSrcAndTestDir(srcDir, excludePaths, excludeFileNamePatterns);
}
  • excludePaths に , "src/main/java/ksbysample/webapp/lending/values/validation/ValuesEnum.java" を追加します。

ValuesEnumValidatorTest.java

package ksbysample.webapp.lending.values.validation;

import ksbysample.webapp.lending.Application;
import ksbysample.webapp.lending.values.Values;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.Getter;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.boot.test.SpringApplicationConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import org.springframework.test.context.web.WebAppConfiguration;

import javax.validation.ConstraintViolation;
import javax.validation.Validation;
import javax.validation.Validator;
import javax.validation.ValidatorFactory;
import java.util.Set;

import static org.assertj.core.api.Assertions.assertThat;

@RunWith(SpringJUnit4ClassRunner.class)
@SpringApplicationConfiguration(classes = Application.class)
@WebAppConfiguration
public class ValuesEnumValidatorTest {

    // テスト用 Value 列挙型
    @Getter
    @AllArgsConstructor
    private enum TestValues implements Values {
        FIRST("1", "1番目")
        , SECOND("2", "2番目")
        , THIRD("3", "3番目");

        private final String value;
        private final String text;
    }

    // テスト用 POJO クラス
    @Data
    private class NotAllowEmptyTestClass {
        @ValuesEnum(enumClass = TestValues.class)
        private String testStr;
    }

    // テスト用 POJO クラス
    @Data
    private class AllowEmptyTestClass {
        @ValuesEnum(enumClass = TestValues.class, allowEmpty = true)
        private String testStr;
    }
    
    private Validator validator;

    @Before
    public void setup() {
        ValidatorFactory factory = Validation.buildDefaultValidatorFactory();
        validator = factory.getValidator();
    }
    
    @Test
    public void ValuesEnumBeanValidationTest_AllowEmpty_False() {
        NotAllowEmptyTestClass notAllowEmptyTestClass = new NotAllowEmptyTestClass();

        // null の場合にはエラーにはエラーは発生しない ( チェックが実行されない )
        notAllowEmptyTestClass.setTestStr(null);
        Set<ConstraintViolation<NotAllowEmptyTestClass>> constraintViolations = validator.validate(notAllowEmptyTestClass);
        assertThat(constraintViolations).hasSize(0);

        // Value に定義されている値の場合にはエラーは発生しない
        notAllowEmptyTestClass.setTestStr("1");
        constraintViolations = validator.validate(notAllowEmptyTestClass);
        assertThat(constraintViolations).hasSize(0);

        // Value に定義されていない値の場合にはエラーが発生する
        notAllowEmptyTestClass.setTestStr("4");
        constraintViolations = validator.validate(notAllowEmptyTestClass);
        assertThat(constraintViolations).hasSize(1);

        // Value に定義されている値が含まれていてもエラーが発生する
        notAllowEmptyTestClass.setTestStr("2test");
        constraintViolations = validator.validate(notAllowEmptyTestClass);
        assertThat(constraintViolations).hasSize(1);

        // Text に定義されている値の場合にはエラーが発生する
        notAllowEmptyTestClass.setTestStr("3番目");
        constraintViolations = validator.validate(notAllowEmptyTestClass);
        assertThat(constraintViolations).hasSize(1);

        // 空文字列の場合にはエラーが発生する
        notAllowEmptyTestClass.setTestStr("");
        constraintViolations = validator.validate(notAllowEmptyTestClass);
        assertThat(constraintViolations).hasSize(1);
    }

    @Test
    public void ValuesEnumBeanValidationTest_AllowEmpty_True() {
        AllowEmptyTestClass allowEmptyTestClass = new AllowEmptyTestClass();

        // null の場合にはエラーにはエラーは発生しない ( チェックが実行されない )
        allowEmptyTestClass.setTestStr(null);
        Set<ConstraintViolation<AllowEmptyTestClass>> constraintViolations = validator.validate(allowEmptyTestClass);
        assertThat(constraintViolations).hasSize(0);

        // Value に定義されている値の場合にはエラーは発生しない
        allowEmptyTestClass.setTestStr("1");
        constraintViolations = validator.validate(allowEmptyTestClass);
        assertThat(constraintViolations).hasSize(0);

        // Value に定義されていない値の場合にはエラーが発生する
        allowEmptyTestClass.setTestStr("4");
        constraintViolations = validator.validate(allowEmptyTestClass);
        assertThat(constraintViolations).hasSize(1);

        // Value に定義されている値が含まれていてもエラーが発生する
        allowEmptyTestClass.setTestStr("2test");
        constraintViolations = validator.validate(allowEmptyTestClass);
        assertThat(constraintViolations).hasSize(1);

        // Text に定義されている値の場合にはエラーが発生する
        allowEmptyTestClass.setTestStr("3番目");
        constraintViolations = validator.validate(allowEmptyTestClass);
        assertThat(constraintViolations).hasSize(1);

        // 空文字列の場合にはエラーは発生しない
        allowEmptyTestClass.setTestStr("");
        constraintViolations = validator.validate(allowEmptyTestClass);
        assertThat(constraintViolations).hasSize(0);
    }
    
}
  • クラス内にテスト用の Values 列挙型、POJO クラスを作成し、それらを使用してテストします。

ValuesHelper.java

package ksbysample.webapp.lending.values;

import com.google.common.reflect.ClassPath;
import org.springframework.stereotype.Component;

import java.io.IOException;
import java.util.Map;
import java.util.stream.Collectors;

@Component("vh")
public class ValuesHelper {

    private final Map<String, String> valuesObjList;

    private ValuesHelper() throws IOException {
        ClassLoader loader = Thread.currentThread().getContextClassLoader();
        valuesObjList = ClassPath.from(loader).getTopLevelClasses(this.getClass().getPackage().getName())
                .stream()
                .filter(classInfo -> !classInfo.getName().equals(this.getClass().getName()))
                .collect(Collectors.toMap(ClassPath.ClassInfo::getSimpleName, ClassPath.ClassInfo::getName));
    }

    @SuppressWarnings("unchecked")
    public <T extends Enum<T> & Values> String getValue(String classSimpleName, String valueName)
            throws ClassNotFoundException {
        Class<T> enumType = (Class<T>) Class.forName(this.valuesObjList.get(classSimpleName));
        T val = Enum.valueOf(enumType, valueName);
        return val.getValue();
    }

    public <T extends Enum<T> & Values> String getValue(Class<T> enumType, String valueName) {
        T val = Enum.valueOf(enumType, valueName);
        return val.getValue();
    }

    @SuppressWarnings("unchecked")
    public <T extends Enum<T> & Values> String getText(String classSimpleName, String value)
            throws ClassNotFoundException {
        Class<T> enumType = (Class<T>) Class.forName(this.valuesObjList.get(classSimpleName));
        String result = "";
        for (T val : enumType.getEnumConstants()) {
            if (val.getValue().equals(value)) {
                result = val.getText();
                break;
            }
        }
        return result;
    }

    public <T extends Enum<T> & Values> String getText(Class<T> enumType, String value) {
        String result = "";
        for (T val : enumType.getEnumConstants()) {
            if (val.getValue().equals(value)) {
                result = val.getText();
                break;
            }
        }
        return result;
    }

    @SuppressWarnings("unchecked")
    public <T extends Enum<T> & Values> T[] values(String classSimpleName)
            throws ClassNotFoundException {
        Class<T> enumType = (Class<T>) Class.forName(this.valuesObjList.get(classSimpleName));
        return enumType.getEnumConstants();
    }

}
  • クラス名の ValuesHelper の後に記述していた <T extends Enum<T> & Values> を各メソッドのアクセス修飾子の後に書くように修正します。
  • 以下の2つのメソッドを追加します。
    • String getValue(Class<T> enumType, String valueName)
    • String getText(Class<T> enumType, String value)

Mail003Helper.java

package ksbysample.webapp.lending.helper.mail;

import ksbysample.webapp.lending.entity.LendingBook;
import ksbysample.webapp.lending.util.velocity.VelocityUtils;
import ksbysample.webapp.lending.values.LendingBookApprovalResultValues;
import ksbysample.webapp.lending.values.ValuesHelper;
import lombok.Data;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.mail.javamail.JavaMailSender;
import org.springframework.mail.javamail.MimeMessageHelper;
import org.springframework.stereotype.Component;

import javax.mail.MessagingException;
import javax.mail.internet.MimeMessage;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;

@Component
public class Mail003Helper {

    private final String TEMPLATE_LOCATION_TEXTMAIL = "mail/mail003-body.vm";

    private final String FROM_ADDR = "lendingapp@sample.com";
    private final String SUBJECT = "貸出申請が承認・却下されました";

    @Autowired
    private VelocityUtils velocityUtils;

    @Autowired
    private JavaMailSender mailSender;

    @Autowired
    private ValuesHelper vh;

    public MimeMessage createMessage(String toAddr, Long lendingAppId, List<LendingBook> lendingBookList)
            throws MessagingException {
        List<Mail003BookData> mail003BookDataList = lendingBookList.stream()
                .map(Mail003BookData::new)
                .collect(Collectors.toList());

        MimeMessage mimeMessage = this.mailSender.createMimeMessage();
        MimeMessageHelper message = new MimeMessageHelper(mimeMessage, false, "UTF-8");
        message.setFrom(FROM_ADDR);
        message.setTo(toAddr);
        message.setSubject(SUBJECT);
        message.setText(generateTextUsingVelocity(lendingAppId, mail003BookDataList), false);
        return message.getMimeMessage();
    }

    private String generateTextUsingVelocity(Long lendingAppId, List<Mail003BookData> mail003BookDataList) {
        Map<String, Object> model = new HashMap<>();
        model.put("lendingAppId", lendingAppId);
        model.put("mail003BookDataList", mail003BookDataList);
        return velocityUtils.merge(this.TEMPLATE_LOCATION_TEXTMAIL, model);
    }

    @Data
    public class Mail003BookData {
        private String approvalResultStr;
        private String bookName;

        public Mail003BookData(LendingBook lendingBook) {
            this.approvalResultStr = vh.getText(LendingBookApprovalResultValues.class, lendingBook.getApprovalResult());
            this.bookName = lendingBook.getBookName();
        }
    }

}
  • private ValuesHelper<?> vh;private ValuesHelper vh; へ変更します。
  • インナークラスの Mail003BookData のコンストラクタ public Mail003BookData(LendingBook lendingBook) の以下の点を変更します。
    • vh.getText(LendingBookApprovalResultValues.class, ...);vh.getText(LendingBookApprovalResultValues.class, ...); へ変更します。
    • メインの処理の前後に書いている try { ... } catch (ClassNotFoundException e) { throw new RuntimeException(e); } を削除します。

Mail003HelperTest.java

package ksbysample.webapp.lending.helper.mail;

import com.google.common.base.Charsets;
import com.google.common.base.Strings;
import ksbysample.webapp.lending.Application;
import ksbysample.webapp.lending.entity.LendingBook;
import ksbysample.webapp.lending.values.ValuesHelper;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.SpringApplicationConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import org.springframework.test.context.web.WebAppConfiguration;

import javax.mail.Message;
import javax.mail.internet.MimeMessage;
import java.io.File;
import java.util.ArrayList;
import java.util.List;

import static ksbysample.webapp.lending.values.LendingBookApprovalResultValues.APPROVAL;
import static ksbysample.webapp.lending.values.LendingBookApprovalResultValues.REJECT;
import static org.assertj.core.api.Assertions.assertThat;

@RunWith(SpringJUnit4ClassRunner.class)
@SpringApplicationConfiguration(classes = Application.class)
@WebAppConfiguration
public class Mail003HelperTest {

    @Autowired
    private Mail003Helper mail003Helper;

    @Autowired
    private ValuesHelper vh;
    
    @Test
    public void testCreateMessage() throws Exception {
        List<LendingBook> lendingBookList = new ArrayList<>();
        // 1件目
        LendingBook lendingBook = new LendingBook();
        lendingBook.setBookName("x");
        lendingBook.setApprovalResult(APPROVAL.getValue());
        lendingBookList.add(lendingBook);
        // 2件目
        lendingBook = new LendingBook();
        lendingBook.setBookName(Strings.repeat("X", 128));
        lendingBook.setApprovalResult(REJECT.getValue());
        lendingBookList.add(lendingBook);
        
        MimeMessage message = mail003Helper.createMessage("test@sample.com", 1L, lendingBookList);
        assertThat(message.getRecipients(Message.RecipientType.TO))
                .extracting(Object::toString)
                .containsOnly("test@sample.com");
        assertThat(message.getContent())
                .isEqualTo(com.google.common.io.Files.toString(
                        new File("src/test/resources/ksbysample/webapp/lending/helper/mail/assertdata/003/message.txt")
                        , Charsets.UTF_8));
    }

}

helper/mail/assertdata/003/message.txt

貸出申請が承認・却下されました。
========================================================================
承認/却下 書籍
------------------------------------------------------------------------
 承認   x
 却下   XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
========================================================================

詳細は以下のURLから確認してください。

http://localhost:8080/confirmresult?lendingAppId=1

LendingapprovalForm_001.yaml, LendingapprovalForm_002.yaml, LendingapprovalForm_003.yaml, LendingapprovalForm_004.yaml, LendingapprovalForm_005.yaml

■LendingapprovalForm_001.yaml

!!ksbysample.webapp.lending.web.lendingapproval.LendingapprovalForm
# 「確定」ボタンが押された場合
# 3件全て「承認」も「却下」も選択していない
lendingApp: 
  lendingAppId: 105
  status: 3
  lendingUserId: 1
  approvalUserId: 
  version: 1
username: temporarySave
applyingBookFormList: 
  - lendingBookId: 522
    isbn: 978-4-7741-6366-6
    bookName: GitHub実践入門
    lendingAppReason: 開発で使用する為
    approvalResult: 
    approvalReason: 
    version: 1
  - lendingBookId: 524
    isbn: 978-4-7741-5377-3
    bookName: JUnit実践入門
    lendingAppReason: 勉強の為
    approvalResult: 
    approvalReason: 
    version: 1
  - lendingBookId: 525
    isbn: 978-4-7973-8014-9
    bookName: Java最強リファレンス
    lendingAppReason: 勉強会の調査の為
    approvalResult: 
    approvalReason: 
    version: 1

■LendingapprovalForm_002.yaml

!!ksbysample.webapp.lending.web.lendingapproval.LendingapprovalForm
# 「確定」ボタンが押された場合
# 3件全て「却下」が選択されているが却下理由が入力されていない
lendingApp: 
  lendingAppId: 105
  status: 3
  lendingUserId: 1
  approvalUserId: 
  version: 1
username: temporarySave
applyingBookFormList: 
  - lendingBookId: 522
    isbn: 978-4-7741-6366-6
    bookName: GitHub実践入門
    lendingAppReason: 開発で使用する為
    approvalResult: 2
    approvalReason: 
    version: 1
  - lendingBookId: 524
    isbn: 978-4-7741-5377-3
    bookName: JUnit実践入門
    lendingAppReason: 勉強の為
    approvalResult: 2
    approvalReason: 
    version: 1
  - lendingBookId: 525
    isbn: 978-4-7973-8014-9
    bookName: Java最強リファレンス
    lendingAppReason: 勉強会の調査の為
    approvalResult: 2
    approvalReason: 
    version: 1

■LendingapprovalForm_003.yaml

!!ksbysample.webapp.lending.web.lendingapproval.LendingapprovalForm
# 「確定」ボタンが押された場合
# 2件目は「承認」も「却下」も選択されていない、3件目は「却下」が選択されているが却下理由が入力されていない
lendingApp: 
  lendingAppId: 105
  status: 3
  lendingUserId: 1
  approvalUserId: 
  version: 1
username: temporarySave
applyingBookFormList: 
  - lendingBookId: 522
    isbn: 978-4-7741-6366-6
    bookName: GitHub実践入門
    lendingAppReason: 開発で使用する為
    approvalResult: 1
    approvalReason: 
    version: 1
  - lendingBookId: 524
    isbn: 978-4-7741-5377-3
    bookName: JUnit実践入門
    lendingAppReason: 勉強の為
    approvalResult: 
    approvalReason: 
    version: 1
  - lendingBookId: 525
    isbn: 978-4-7973-8014-9
    bookName: Java最強リファレンス
    lendingAppReason: 勉強会の調査の為
    approvalResult: 2
    approvalReason: 
    version: 1

■LendingapprovalForm_004.yaml

!!ksbysample.webapp.lending.web.lendingapproval.LendingapprovalForm
# 「確定」ボタンが押された場合
# 3件全て「承認」が選択されている
lendingApp: 
  lendingAppId: 105
  status: 3
  lendingUserId: 1
  approvalUserId: 
  version: 1
username: temporarySave
applyingBookFormList: 
  - lendingBookId: 522
    isbn: 978-4-7741-6366-6
    bookName: GitHub実践入門
    lendingAppReason: 開発で使用する為
    approvalResult: 1
    approvalReason: 
    version: 1
  - lendingBookId: 524
    isbn: 978-4-7741-5377-3
    bookName: JUnit実践入門
    lendingAppReason: 勉強の為
    approvalResult: 1
    approvalReason: 
    version: 1
  - lendingBookId: 525
    isbn: 978-4-7973-8014-9
    bookName: Java最強リファレンス
    lendingAppReason: 勉強会の調査の為
    approvalResult: 1
    approvalReason: 
    version: 1

■LendingapprovalForm_005.yaml

!!ksbysample.webapp.lending.web.lendingapproval.LendingapprovalForm
# 「確定」ボタンが押された場合
# 3件全て「却下」が選択されて、却下理由も入力されている
lendingApp: 
  lendingAppId: 105
  status: 3
  lendingUserId: 1
  approvalUserId: 
  version: 1
username: temporarySave
applyingBookFormList: 
  - lendingBookId: 522
    isbn: 978-4-7741-6366-6
    bookName: GitHub実践入門
    lendingAppReason: 開発で使用する為
    approvalResult: 2
    approvalReason: 購入済です
    version: 1
  - lendingBookId: 524
    isbn: 978-4-7741-5377-3
    bookName: JUnit実践入門
    lendingAppReason: 勉強の為
    approvalResult: 2
    approvalReason: 自分で購入して勉強しましょう
    version: 1
  - lendingBookId: 525
    isbn: 978-4-7973-8014-9
    bookName: Java最強リファレンス
    lendingAppReason: 勉強会の調査の為
    approvalResult: 2
    approvalReason: XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
    version: 1

LendingapprovalForm.java

package ksbysample.webapp.lending.web.lendingapproval;

import ksbysample.webapp.lending.entity.LendingApp;
import ksbysample.webapp.lending.entity.LendingBook;
import lombok.Data;
import lombok.NoArgsConstructor;

import javax.validation.Valid;
import java.util.List;
import java.util.stream.Collectors;

@Data
@NoArgsConstructor
public class LendingapprovalForm {

    private LendingApp lendingApp;

    private String username;

    @Valid
    private List<ApplyingBookForm> applyingBookFormList;

    public void setApplyingBookFormListFromLendingBookList(List<LendingBook> lendingBookList) {
        this.applyingBookFormList = null;
        if (lendingBookList != null) {
            this.applyingBookFormList = lendingBookList.stream()
                    .map(ApplyingBookForm::new)
                    .collect(Collectors.toList());
        }
    }
    
}

LendingapprovalFormValidatorTest.java

package ksbysample.webapp.lending.web.lendingapproval;

import ksbysample.webapp.lending.Application;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.SpringApplicationConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import org.springframework.test.context.web.WebAppConfiguration;
import org.springframework.validation.Errors;
import org.springframework.validation.FieldError;
import org.springframework.validation.MapBindingResult;
import org.springframework.validation.ObjectError;
import org.yaml.snakeyaml.Yaml;

import java.util.HashMap;

import static org.assertj.core.api.Assertions.assertThat;

@RunWith(SpringJUnit4ClassRunner.class)
@SpringApplicationConfiguration(classes = Application.class)
@WebAppConfiguration
public class LendingapprovalFormValidatorTest {

    // テストデータ
    private LendingapprovalForm lendingapprovalForm_001
            = (LendingapprovalForm) new Yaml().load(getClass().getResourceAsStream("LendingapprovalForm_001.yaml"));
    private LendingapprovalForm lendingapprovalForm_002
            = (LendingapprovalForm) new Yaml().load(getClass().getResourceAsStream("LendingapprovalForm_002.yaml"));
    private LendingapprovalForm lendingapprovalForm_003
            = (LendingapprovalForm) new Yaml().load(getClass().getResourceAsStream("LendingapprovalForm_003.yaml"));
    private LendingapprovalForm lendingapprovalForm_004
            = (LendingapprovalForm) new Yaml().load(getClass().getResourceAsStream("LendingapprovalForm_004.yaml"));
    private LendingapprovalForm lendingapprovalForm_005
            = (LendingapprovalForm) new Yaml().load(getClass().getResourceAsStream("LendingapprovalForm_005.yaml"));

    @Autowired
    private LendingapprovalFormValidator lendingapprovalFormValidator;

    @Test
    public void testValidate_全ての書籍で承認も却下も選択されていない場合はエラーになる() throws Exception {
        Errors errors = new MapBindingResult(new HashMap<String, String>(), "");
        lendingapprovalFormValidator.validate(lendingapprovalForm_001, errors);
        assertThat(errors.hasGlobalErrors()).isTrue();
        assertThat(errors.getGlobalErrorCount()).isEqualTo(1);
        assertThat(errors.getGlobalErrors())
                .extracting(ObjectError::getCode)
                .containsOnly("LendingapprovalForm.applyingBookFormList.approvalResult.notAllCheckedErr");
        assertThat(errors.hasFieldErrors()).isFalse();
    }

    @Test
    public void testValidate_全ての書籍で却下が選択されいるが却下理由が入力されていない場合はエラーになる() throws Exception {
        Errors errors = new MapBindingResult(new HashMap<String, String>(), "");
        lendingapprovalFormValidator.validate(lendingapprovalForm_002, errors);
        assertThat(errors.hasGlobalErrors()).isFalse();
        assertThat(errors.hasFieldErrors()).isTrue();
        assertThat(errors.getFieldErrorCount()).isEqualTo(3);
        assertThat(errors.getFieldErrors())
                .extracting(FieldError::getField)
                .containsOnly("applyingBookFormList[0].approvalReason"
                        , "applyingBookFormList[1].approvalReason"
                        , "applyingBookFormList[2].approvalReason");
    }

    @Test
    public void testValidate_一部の書籍は承認却下未選択で一部の書籍は却下理由未入力の場合はエラーになる() throws Exception {
        Errors errors = new MapBindingResult(new HashMap<String, String>(), "");
        lendingapprovalFormValidator.validate(lendingapprovalForm_003, errors);
        assertThat(errors.hasGlobalErrors()).isTrue();
        assertThat(errors.getGlobalErrorCount()).isEqualTo(1);
        assertThat(errors.getGlobalErrors())
                .extracting(ObjectError::getCode)
                .containsOnly("LendingapprovalForm.applyingBookFormList.approvalResult.notAllCheckedErr");
        assertThat(errors.hasFieldErrors()).isTrue();
        assertThat(errors.getFieldErrorCount()).isEqualTo(1);
        assertThat(errors.getFieldErrors())
                .extracting(FieldError::getField)
                .containsOnly("applyingBookFormList[2].approvalReason");
    }

    @Test
    public void testValidate_全ての書籍で承認が選択されている場合はエラーにならない() throws Exception {
        Errors errors = new MapBindingResult(new HashMap<String, String>(), "");
        lendingapprovalFormValidator.validate(lendingapprovalForm_004, errors);
        assertThat(errors.hasGlobalErrors()).isFalse();
        assertThat(errors.hasFieldErrors()).isFalse();
    }

    @Test
    public void testValidate_全ての書籍で却下が選択され却下理由も入力されている場合はエラーにならない() throws Exception {
        Errors errors = new MapBindingResult(new HashMap<String, String>(), "");
        lendingapprovalFormValidator.validate(lendingapprovalForm_005, errors);
        assertThat(errors.hasGlobalErrors()).isFalse();
        assertThat(errors.hasFieldErrors()).isFalse();
    }
    
}

履歴

2016/01/20
初版発行。