かんがるーさんの日記

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

Spring Boot で書籍の貸出状況確認・貸出申請する Web アプリケーションを作る ( その25 )( 貸出希望書籍 CSV ファイルアップロード画面の作成4 )

概要

Spring Boot で書籍の貸出状況確認・貸出申請する Web アプリケーションを作る ( その24 )( 貸出希望書籍 CSV ファイルアップロード画面の作成3 ) の続きです。

  • 今回の手順で確認できるのは以下の内容です。
    • 貸出希望書籍 CSV ファイルアップロード画面の作成
      • テストを作成します。

参照したサイト・書籍

  1. How to diff files/folders in Gradle?
    http://stackoverflow.com/questions/30401436/how-to-diff-files-folders-in-gradle

  2. comparedirs.groovy
    https://gist.github.com/igormukhin/71d780c4274336eeb297

  3. Java開発の強力な相棒として今すぐ使えるGroovy - SlideShare
    http://www.slideshare.net/nobeans/javagroovy

  4. Using Spring MVC Test to unit test multipart POST request
    http://stackoverflow.com/questions/21800726/using-spring-mvc-test-to-unit-test-multipart-post-request

    • MultipartFile クラスを使用するテストをするための方法を参照しました。MockMultipartFile でモックを作成してテストします。
  5. Java Code Examples for org.springframework.validation.BeanPropertyBindingResult
    http://www.programcreek.com/java-api-examples/index.php?api=org.springframework.validation.BeanPropertyBindingResult

    • Errors インターフェースの実体オブジェクトを MapBindingResult クラスで生成する方法を参考にしました。

目次

  1. バックアップ、リストア対象のテーブルを追加し、テストデータを用意する
  2. テスト未作成のクラス一覧を出力する Gradle タスクを作成する
  3. テスト作成対象のクラスを決める
  4. MessagesPropertiesHelper クラスのテストの作成
  5. BooklistCsvFileService クラスのテストの作成
  6. 次回は。。。

手順

バックアップ、リストア対象のテーブルを追加し、テストデータを用意する

  1. lending_app, lending_book テーブルをバックアップ、リストアの対象にします。src/test/java/ksbysample/common/test の下の TestDataResource.javaリンク先の内容 に変更します。

  2. テストデータを用意します。lending_app, lending_book の初期データは空にします。src/test/resources/testdata/base の下に lending_app.csv, lending_book.csv を作成します。作成後、リンク先の内容 に変更します。

テスト未作成のクラス一覧を出力する Gradle タスクを作成する

  1. テスト未作成のクラス一覧を出力してくれる機能が欲しくなったので、Gradle タスクで実装することにします。build.gradle を リンク先のその1、その2の内容 に変更します。

    作成する printClassWhatNotMakeTest タスクは以下の仕様です。

    • src/main/java と src/test/java の下の .java ファイルを比較し、テストクラス ( ~Test.java ) が作成されていない .java ファイル一覧をコンソールに出力します。
    • タスク内の変数 excludePaths に出力対象外にする Path を設定できます。
    • タスク内の変数 excludeFileNamePatterns に出力対象外にするファイル名を設定できます。
  2. Gradle projects View の左上にある「Refresh all Gradle projects」ボタンをクリックして更新します。

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

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

    f:id:ksby:20151004224744p:plain

    f:id:ksby:20151004224917p:plain

  2. 出力されたクラスの内、以下のクラスのテストを作成することにします。

    • helper/message/MessagesPropertiesHelper.java
    • service/file/BooklistCsvFileService.java
    • service/queue/InquiringStatusOfBookQueueService.java
    • web/booklist/BooklistController.java
    • web/booklist/BooklistService.java

MessagesPropertiesHelper クラスのテストの作成

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

    f:id:ksby:20151006003116p:plain

    src/test/java/ksbysample/webapp/lending/helper/message の下に MessagesPropertiesHelperTest.java が作成されます。

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

  3. テストを実行します。MessagesPropertiesHelperTest クラスのクラス名にカーソルを移動し、コンテキストメニューを表示後「Run 'MessagesPropertiesHelperTest' with Coverage」を選択します。

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

    f:id:ksby:20151006005253p:plain

BooklistCsvFileService クラスのテストの作成

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

    f:id:ksby:20151006011016p:plain

    src/test/java/ksbysample/webapp/lending/service/file の下に BooklistCsvFileServiceTest.java が作成されます。

  2. テストのためにコンストラクタが必要なので追加します。src/main/java/ksbysample/webapp/lending/service/file の下の BooklistCSVRecord.javaリンク先の内容 に変更します。

  3. src/test/java/ksbysample/webapp/lending/service/file の下の BooklistCsvFileServiceTest.javaリンク先の内容 に変更します。

  4. テストを実行します。BooklistCsvFileServiceTest クラスのクラス名にカーソルを移動し、コンテキストメニューを表示後「Run 'BooklistCsvFileServiceTest' with Coverage」を選択します。

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

    f:id:ksby:20151008003158p:plain

次回は。。。

テストの作成が続きます。

ソースコード

TestDataResource.java

Component
public class TestDataResource extends ExternalResource {

    private final String TESTDATA_DIR = "src/test/resources/testdata/base";
    private final String BACKUP_FILE_NAME = "ksbylending_backup";
    private final List<String> BACKUP_TABLES = Arrays.asList(
            "user_info"
            , "user_role"
            , "library_forsearch"
            , "lending_app"
            , "lending_book"
    );
  • BACKUP_TABLES の配列に "lending_app", "lending_book" を追加します。

lending_app.csv, lending_book.csv

■lending_app.csv

lending_app_id,status,lending_user_id,approval_user_id

■lending_book.csv

lending_book_id,lending_app_id,isbn,book_name,lending_state,lending_app_flg,lending_app_reason,approval_result,approval_reason

build.gradle

■その1

task downloadCssFontsJs << {
    .....
}

task printClassWhatTestNotMake << {
    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/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/Librar"
            , "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/webapi/common/CommonWebApiResponse.java"
            , "src/main/java/ksbysample/webapp/lending/webapi/weather"
    ];
    def excludeFileNamePatterns = [
            ".*EventListener.java"
            , ".*Form.java"
            , ".*Values.java"
    ];

    compareSrcAndTestDir(srcDir, excludePaths, excludeFileNamePatterns);
}
  • downloadCssFontsJs タスクの下に printClassWhatTestNotMake タスクを追加します。

■その2

void downloadBootstrapFileInputMinJs(String workDirPath, String staticDirPath) {
    .....
}

def compareSrcAndTestDir(srcDir, excludePaths, excludeFileNamePatterns) {
    def existFlg;

    for (srcFile in srcDir.listFiles()) {
        String srcFilePath = (srcFile.toPath() as String).replaceAll("\\\\", "/");
        existFlg = false;

        for (exclude in excludePaths) {
            if (srcFilePath =~ /^${exclude as String}/) {
                existFlg = true;
                break;
            }
        }
        if (existFlg == true) continue;

        for (exclude in excludeFileNamePatterns) {
            if (srcFilePath =~ /${exclude as String}/) {
                existFlg = true
                break;
            }
        }
        if (existFlg == true) continue;
        
        if (srcFile.isDirectory()) {
            compareSrcAndTestDir(srcFile, excludePaths, excludeFileNamePatterns);
        } else {
            String testFilePath = srcFilePath.replaceFirst(/^src\/main\/java/, "src/test/java").replaceFirst(/\.java$/, "Test.java");
            def testFile = new File(testFilePath);
            if (!testFile.exists()) {
                println(srcFilePath);
            }
        }
    }
}
  • downloadBootstrapFileInputMinJs 関数の下に compareSrcAndTestDir関数を追加します。

MessagesPropertiesHelperTest.java

package ksbysample.webapp.lending.helper.message;

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 static org.assertj.core.api.Assertions.assertThat;

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

    @Autowired
    private MessagesPropertiesHelper messagesPropertiesHelper;
    
    @Test
    public void testGetMessage_NoArgs() throws Exception {
        String message
                = messagesPropertiesHelper.getMessage("AbstractUserDetailsAuthenticationProvider.locked"
                , null);
        assertThat(message).isEqualTo("入力された ID はロックされています");
    }

    @Test
    public void testGetMessage_Args() throws Exception {
        int line = 1;
        int length = 3;
        String message
                = messagesPropertiesHelper.getMessage("UploadBooklistForm.fileupload.lengtherr"
                , new Object[]{line, length});
        assertThat(message).isEqualTo("1行目のレコードの項目数が 2個ではありません ( 3個 )。");
    }
}

BooklistCSVRecord.java

package ksbysample.webapp.lending.service.file;

import com.univocity.parsers.annotations.Parsed;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

@Data
@NoArgsConstructor
@AllArgsConstructor
public class BooklistCSVRecord {

    @Parsed
    private String isbn;

    @Parsed(field = "書名")
    private String bookName;

}

BooklistCsvFileServiceTest.java

package ksbysample.webapp.lending.service.file;

import com.univocity.parsers.csv.CsvWriter;
import com.univocity.parsers.csv.CsvWriterSettings;
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.mock.web.MockMultipartFile;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import org.springframework.test.context.web.WebAppConfiguration;
import org.springframework.validation.Errors;
import org.springframework.validation.MapBindingResult;
import org.springframework.validation.ObjectError;

import java.io.BufferedWriter;
import java.io.InputStream;
import java.nio.charset.Charset;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.HashMap;
import java.util.List;

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

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

    @Autowired
    private BooklistCsvFileService booklistCsvFileService;

    @Test
    public void testValidateUploadFile_NoErrorCsvFile() throws Exception {
        MockMultipartFile multipartFile = createNoErrorCsvFile();
        Errors errors = new MapBindingResult(new HashMap<String, String>(), "");
        booklistCsvFileService.validateUploadFile(multipartFile, errors);
        assertThat(errors.hasErrors()).isFalse();
    }

    @Test
    public void testValidateUploadFile_ErrorCsvFile() throws Exception {
        MockMultipartFile multipartFile = createErrorCsvFile();
        Errors errors = new MapBindingResult(new HashMap<String, String>(), "");
        booklistCsvFileService.validateUploadFile(multipartFile, errors);
        assertThat(errors.hasErrors()).isTrue();
        assertThat(errors.getErrorCount()).isEqualTo(6);
        assertThat(errors.getAllErrors())
                .contains(new ObjectError("", new String[]{"UploadBooklistForm.fileupload.lengtherr"}, new Object[]{2, 3}, null))
                .contains(new ObjectError("", new String[]{"UploadBooklistForm.fileupload.isbn.patternerr"}, new Object[]{3, "978-4-7741-5x77-3"}, null))
                .contains(new ObjectError("", new String[]{"UploadBooklistForm.fileupload.isbn.lengtherr"}, new Object[]{4, "978-4-79173-8014-9"}, null))
                .contains(new ObjectError("", new String[]{"UploadBooklistForm.fileupload.isbn.numlengtherr"}, new Object[]{4, "97847917380149"}, null))
                .contains(new ObjectError("", new String[]{"UploadBooklistForm.fileupload.isbn.numlengtherr"}, new Object[]{5, "97847197347784"}, null))
                .contains(new ObjectError("", new String[]{"UploadBooklistForm.fileupload.bookname.lengtherr"}, new Object[]{6, "123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789"}, null));
    }

    @Test
    public void testConvertFileToList() throws Exception {
        MockMultipartFile multipartFile = createNoErrorCsvFile();
        List<BooklistCSVRecord> booklistCSVRecordList = booklistCsvFileService.convertFileToList(multipartFile);
        assertThat(booklistCSVRecordList).hasSize(5);
        assertThat(booklistCSVRecordList).contains(new BooklistCSVRecord("978-4-7741-5377-3", "JUnit実践入門"));
    }

    private MockMultipartFile createNoErrorCsvFile() throws Exception {
        Path path = Files.createTempFile("テスト", "csv");
        try (BufferedWriter bw = Files.newBufferedWriter(path, Charset.forName("Windows-31J"))) {
            CsvWriterSettings settings = new CsvWriterSettings();
            settings.setQuoteAllFields(true);
            CsvWriter writer = new CsvWriter(bw, settings);
            writer.writeHeaders("ISBN", "書名");
            writer.writeRow("978-4-7741-6366-6", "GitHub実践入門");
            writer.writeRow("978-4-7741-5377-3", "JUnit実践入門");
            writer.writeRow("978-4-7973-8014-9", "Java最強リファレンス");
            writer.writeRow("978-4-7973-4778-4", "アジャイルソフトウェア開発の奥義");
            writer.writeRow("978-4-87311-704-1", "Javaによる関数型プログラミング");
            writer.close();
        }

        MockMultipartFile multipartFile;
        try (InputStream is = Files.newInputStream(path)) {
            multipartFile = new MockMultipartFile("テスト.csv", is);
        }

        return multipartFile;
    }

    private MockMultipartFile createErrorCsvFile() throws Exception {
        Path path = Files.createTempFile("テスト2", "csv");
        try (BufferedWriter bw = Files.newBufferedWriter(path, Charset.forName("Windows-31J"))) {
            CsvWriterSettings settings = new CsvWriterSettings();
            settings.setQuoteAllFields(true);
            CsvWriter writer = new CsvWriter(bw, settings);
            writer.writeHeaders("ISBN", "書名");
            writer.writeRow("978-4-7741-6366-6", "GitHub実践入門", "項目が3つ");
            writer.writeRow("978-4-7741-5x77-3", "JUnit実践入門");
            writer.writeRow("978-4-79173-8014-9", "Java最強リファレンス");
            writer.writeRow("978-4-719734778-4", "アジャイルソフトウェア開発の奥義");
            writer.writeRow("978-4-87311-704-1", "123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789");
            writer.close();
        }

        MockMultipartFile multipartFile;
        try (InputStream is = Files.newInputStream(path)) {
            multipartFile = new MockMultipartFile("テスト2.csv", is);
        }

        return multipartFile;
    }
}
  • テストのポイントを以下に記載します。
    • テスト用の MultipartFile のインスタンスは org.springframework.mock.web.MockMultipartFile を利用して生成します。
    • CSV ファイルは Files.createTempFile で Path オブジェクトを取得した後、Files.newBufferedWriter と uniVocity-parsers を利用して作成します。作成後、Files.newInputStream で MultipartFile のインスタンスに読み込みます。
    • Errors クラスのインスタンスは MapBindingResult クラスで生成します。第2引数は空文字列で構いません。文字列を指定した場合には、その後の new ObjectError の第1引数に同じ文字列を指定します。

履歴

2015/10/08
初版発行。