読者です 読者をやめる 読者になる 読者になる

かんがるーさんの日記

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

自作したテスト用クラス ( src/test/java/ksbysample/common/test ) の使い方

「Spring Boot で書籍の貸出状況確認・貸出申請する Web アプリケーションを作る」でテストを書くために、前に作成したテスト用クラスを使おうとして、使い方をきちんと覚えておらず思い出すのにちょっと苦労したので現時点での内容をまとめることにしました。

この記事を書いている時の対象ファイルは以下のレポジトリのものです。

目次

  1. JUnit ルール用クラス
    1. TestDataResource クラス
    2. TestDataLoaderResource クラス、TestDataLoader アノテーション
    3. MailServerResource クラス
  2. Assert 用クラス
    1. TableDataAssert クラス
  3. MockMvc によるテストのヘルパークラス
    1. TestHelper クラス
    2. CustomModelResultMatchers クラス
    3. ErrorsResultMatchers クラス

JUnit ルール用クラス

TestDataResource クラス

テスト前にテーブルのデータのバックアップ取得+初期テストデータ投入を行い、テスト終了後にバックアップデータからリストアするためのクラスです。JUnit のルールとして使用します。

package ksbysample.common.test;

import org.dbunit.database.DatabaseConnection;
import org.dbunit.database.IDatabaseConnection;
import org.dbunit.database.QueryDataSet;
import org.dbunit.dataset.DefaultDataSet;
import org.dbunit.dataset.DefaultTable;
import org.dbunit.dataset.IDataSet;
import org.dbunit.dataset.ReplacementDataSet;
import org.dbunit.dataset.csv.CsvDataSet;
import org.dbunit.dataset.xml.FlatXmlDataSet;
import org.dbunit.dataset.xml.FlatXmlDataSetBuilder;
import org.dbunit.operation.DatabaseOperation;
import org.junit.rules.ExternalResource;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

import javax.sql.DataSource;
import java.io.File;
import java.io.FileOutputStream;
import java.nio.file.Files;
import java.util.Arrays;
import java.util.List;

@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"
    );

    @Autowired
    private DataSource dataSource;

    private File backupFile;
    
    @Override
    protected void before() throws Exception {
        IDatabaseConnection conn = null;
        try {
            conn = new DatabaseConnection(dataSource.getConnection());

            // バックアップを取得する
            QueryDataSet partialDataSet = new QueryDataSet(conn);
            for (String backupTable : BACKUP_TABLES) {
                partialDataSet.addTable(backupTable);
            }
            ReplacementDataSet replacementDatasetBackup = new ReplacementDataSet(partialDataSet);
            replacementDatasetBackup.addReplacementObject("", "[null]");
            backupFile = File.createTempFile(BACKUP_FILE_NAME, "xml");
            try (FileOutputStream fos = new FileOutputStream(backupFile)) {
                FlatXmlDataSet.write(replacementDatasetBackup, fos);
            }

            // テストデータに入れ替える
            IDataSet dataSet = new CsvDataSet(new File(TESTDATA_DIR));
            ReplacementDataSet replacementDataset = new ReplacementDataSet(dataSet);
            replacementDataset.addReplacementObject("[null]", null);
            DatabaseOperation.CLEAN_INSERT.execute(conn, replacementDataset);
        } finally {
            if (conn != null) conn.close();
        }
    }

    @Override
    protected void after() {
        try {
            IDatabaseConnection conn = null;
            try {
                conn = new DatabaseConnection(dataSource.getConnection());

                // バックアップからリストアする
                if (backupFile != null) {
                    IDataSet dataSet = new FlatXmlDataSetBuilder().build(backupFile);
                    ReplacementDataSet replacementDatasetRestore = new ReplacementDataSet(dataSet);
                    replacementDatasetRestore.addReplacementObject("[null]", null);
                    DatabaseOperation.CLEAN_INSERT.execute(conn, replacementDatasetRestore);
                }
            } finally {
                if (backupFile != null) {
                    Files.delete(backupFile.toPath());
                    backupFile = null;
                }
                try {
                    if (conn != null) conn.close();
                } catch (Exception ignored) {
                }
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

}

テスト開始前に2点準備が必要です。

最初に TestDataResource クラス内の BACKUP_TABLES 定数にバックアップ/レストア対象のテーブル名を列挙します。

    private final List<String> BACKUP_TABLES = Arrays.asList(
            "user_info"
            , "user_role"
            , "library_forsearch"
            , "lending_app"
            , "lending_book"
    );

次に TESTDATA_DIR 定数に設定したディレクトリに初期テストデータのファイルを用意します。

    private final String TESTDATA_DIR = "src/test/resources/testdata/base";

初期テストデータのファイルは table-ordering.txt 1ファイルと [テーブル名].csv という名前の CSV ファイルを初期テストデータを投入したいテーブル分用意します。以下の画像は ksbysample-webapp-lending の初期テストデータのファイル一覧と一部のファイルの中身です。

f:id:ksby:20151017194853p:plain

table-ordering.txt に初期テストデータを投入するテーブルを列挙します。1点注意があり、列挙するテーブル間に外部キー制約で親子関係がある場合には、必ず子テーブルを下に書いてください。

ksbysample-webapp-lending では (親)user_info <-- lending_app <-- lending_book(子) という外部キー制約による親子関係がありますので、上から user_info, lending_app, lending_book の順になるよう記述しています。

user_info
user_role
library_forsearch
lending_app
lending_book

CSV ファイルは IntelliJ IDEA Ultimate Edition の Database View の Save To File の機能で出力するのが一番簡単です。以下に出力する手順を記載します。

まず出力する CSV ファイルにカラム名が出力されるよう設定します。Database View 上でコンテキストメニューを表示し、「Save To File」->「Configure Extractors...」を選択します。

f:id:ksby:20151017203016p:plain

「Data Extractors」ダイアログが表示されます。左側のリストから「Comma-separated Values (CSV)」を選択した後、「Include column names」をチェックして「OK」ボタンをクリックします。

f:id:ksby:20151017203248p:plain

次に CSV ファイルを出力します。Database View でデータを出力したいテーブルを選択してコンテキストメニューを表示した後、「Save To File」->「Comma-separated Values (CSV)」を選択します。

f:id:ksby:20151017203714p:plain

「Save Data To File」ダイアログが表示されます。TESTDATA_DIR 定数に設定したディレクトリを選択した後、「File name」に [テーブル名].csv のファイル名を入力して「OK」ボタンをクリックします。

f:id:ksby:20151017203949p:plain

以下のようなデータが出力された CSV ファイルが作成されます。こちらも1点注意があり、空文字列と null の取り扱いが異なるデータベースで null で値を登録したい場合には CSV ファイル出力後に対象のカラムの値を "[null]" という文字列に変更してください。

role_id,user_id,role
1,1,ROLE_USER
2,1,ROLE_ADMIN
3,1,ROLE_APPROVER
4,2,ROLE_USER
5,3,ROLE_USER
6,4,ROLE_USER
7,5,ROLE_USER

TestDataResource クラスの使用方法ですが、データのバックアップ/リストア+初期テストデータ投入を行いたいテストクラスに @Rule, @Autowired アノテーションを付加して宣言するだけです。

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

    @Rule
    @Autowired
    public TestDataResource testDataResource;

    .....

    @Test
    public void testIncCntBadcredentials() throws Exception {
        .....
    }

}

TestDataLoaderResource クラス、TestDataLoader アノテーション

テストデータを投入するためのクラスです。JUnit の TestWatcher を継承したクラスで、JUnit のルールとして使用します。

通常 TestDataResource クラスと組み合わせて使用し、TestDataResource クラスがバックアップ取得+初期テストデータ投入をした後に TestDataLoaderResource クラスでテストに必要なデータを追加で投入します。

package ksbysample.common.test;

import org.dbunit.database.DatabaseConnection;
import org.dbunit.database.IDatabaseConnection;
import org.dbunit.dataset.IDataSet;
import org.dbunit.dataset.ReplacementDataSet;
import org.dbunit.dataset.csv.CsvDataSet;
import org.dbunit.operation.DatabaseOperation;
import org.junit.rules.TestWatcher;
import org.junit.runner.Description;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

import javax.sql.DataSource;
import java.io.File;
import java.lang.annotation.Annotation;
import java.util.Collection;

@Component
public class TestDataLoaderResource extends TestWatcher {

    @Autowired
    private DataSource dataSource;

    @Override
    protected void starting(Description description) {
        try {
            IDatabaseConnection conn = null;
            try {
                conn = new DatabaseConnection(dataSource.getConnection());

                Collection<Annotation> annotationList = description.getAnnotations();
                for (Annotation annotation : annotationList) {
                    if (annotation instanceof TestDataLoader) {
                        TestDataLoader testDataLoader = (TestDataLoader)annotation;
                        IDataSet dataSet = new CsvDataSet(new File(testDataLoader.value()));
                        ReplacementDataSet replacementDataset = new ReplacementDataSet(dataSet);
                        replacementDataset.addReplacementObject("[null]", null);
                        DatabaseOperation.CLEAN_INSERT.execute(conn, replacementDataset);
                    }
                }
            } finally {
                if (conn != null) conn.close();
            }
        }
        catch (Exception e) {
            e.printStackTrace();
        }
    }

}
package ksbysample.common.test;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface TestDataLoader {
        String value();
}

テスト開始前に1点準備が必要です。

TestDataLoader アノテーションに指定するディレクトリにテストデータのファイルを用意します。テストデータのファイルは TestDataResource クラスと同じで table-ordering.txt 1ファイルと [テーブル名].csv という名前の CSV ファイルを初期テストデータを投入したいテーブル分用意します。データの作成方法については上に書いた TestDataResource クラスの説明を参照してください。

使用方法ですが、テストデータを投入したいテストメソッドに @TestDataLoader アノテーションを付加してテストデータのディレクトリを指定し、テストメソッドを持つテストクラスに TestDataLoaderResource クラスを @Rule, @Autowired アノテーションを付加して宣言します。

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

    @Rule
    @Autowired
    public TestDataResource testDataResource;

    @Rule
    @Autowired
    public TestDataLoaderResource testDataLoaderResource;

    .....
    
    @Test
    @TestDataLoader("src/test/resources/ksbysample/webapp/lending/web/admin/library/testdata/001")
    public void testDeleteAndInsertLibraryForSearch() throws Exception {
        .....
    }

}

MailServerResource クラス

メールの送信テストを行うために、GreenMail を利用してテスト用のメールサーバを起動、終了するためのクラスです。JUnit のルールとして使用します。

テストでは送信したメール ( メールサーバで受信した1件目のメール ) を確認することが多いので、1件目のメールを取得するメソッドも提供しています。

package ksbysample.common.test;

import com.icegreen.greenmail.util.GreenMail;
import com.icegreen.greenmail.util.ServerSetup;
import org.junit.rules.ExternalResource;
import org.springframework.stereotype.Component;

import javax.mail.internet.MimeMessage;
import java.util.Arrays;
import java.util.List;

@Component
public class MailServerResource extends ExternalResource {

    private GreenMail greenMail = new GreenMail(new ServerSetup(25, "localhost", ServerSetup.PROTOCOL_SMTP));

    @Override
    protected void before() {
        greenMail.start();
    }

    @Override
    protected void after() {
        greenMail.stop();
    }

    public int getMessagesCount() {
        return greenMail.getReceivedMessages().length;
    }

    public List<MimeMessage> getMessages() {
        return Arrays.asList(greenMail.getReceivedMessages());
    }

    public MimeMessage getFirstMessage() {
        MimeMessage message = null;
        MimeMessage[] receivedMessages = greenMail.getReceivedMessages();
        if (receivedMessages.length > 0) {
            message = receivedMessages[0];
        }
        return message;
    }

}

GreenMail を利用しますので、build.gradle の dependencies タスクに以下の設定を記述し、必要なライブラリをダウンロードしてください。

dependencies {
    .....
    testCompile("com.icegreen:greenmail:1.4.1")
    .....
}

使用方法は、メールの送信テストを行うテストクラスで MailServerResource クラスを @Rule, @Autowired アノテーションを付加して宣言します。

送信したメールの内容を確認したい場合には MailServerResource クラスの getFirstMessage メソッドを呼び出して MimeMessage クラスのインスタンスを取得して確認します。

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

    @Rule
    @Autowired
    public MailServerResource mailServer;

    .....

    @Test
    public void mailsendFormSimpleでテキストメールを送信する場合() throws Exception {
        .....

        // メールが送信されているか確認する
        assertThat(mailServer.getMessagesCount(), is(1));
        MimeMessage receiveMessage = mailServer.getFirstMessage();
        assertThat(receiveMessage.getFrom()[0], is(new InternetAddress("test@sample.com")));
        assertThat(receiveMessage.getAllRecipients()[0], is(new InternetAddress("xxx@yyy.zzz")));
        assertThat(receiveMessage.getSubject(), is("テスト"));
        String mailsendFormSimpleMail
                = Files.toString(new File(getClass().getResource("mailsendForm_simple_mail.txt").toURI()), StandardCharsets.UTF_8);
        assertThat(receiveMessage.getContent(), is(mailsendFormSimpleMail));
    }

}

Assert 用クラス

TableDataAssert クラス

CSV ファイルのデータとテーブルのデータを比較検証するクラスです。比較検証時に無視するカラムを指定することができます。

テスト開始前に1点準備が必要です。

Assert 用のデータのファイルを用意します。Assert 用のデータのファイルは TestDataResource クラスと同じで table-ordering.txt 1ファイルと [テーブル名].csv という名前の CSV ファイルを比較検証したいテーブル分用意します。データの作成方法については上に書いた TestDataResource クラスの説明を参照してください。

使用方法ですが、テストメソッドの中で以下の要領で実装して使用します。また DataSource Bean を利用しますので、テストクラスに @Autowired アノテーションを付加した DataSource クラスのフィールドを定義します。

  1. new CsvDataSet に Assert 用のデータのファイルを置いてあるディレクトリの Path を引数に渡して IDataSet インターフェースの実装クラスのインスタンスを生成します。
  2. new TableDataAssert に IDataSet インターフェースを持つインスタンスと DataSource Bean を引数に渡してインスタンスを生成します。
  3. assertEquals メソッドを呼び出します。第1引数は比較検証するテーブル名、第2引数は無視するカラムを指定した String 型の配列です ( 無視するカラムがない場合には null を渡します )。
@RunWith(SpringJUnit4ClassRunner.class)
@SpringApplicationConfiguration(classes = Application.class)
@WebAppConfiguration
public class BooklistServiceTest {

    .....

    @Autowired
    private DataSource dataSource;
    
    @Test
    public void testTemporarySaveBookListCsvFile() throws Exception {
        .....
        
        IDataSet dataSet = new CsvDataSet(new File("src/test/resources/ksbysample/webapp/lending/web/booklist/assertdata/001"));
        TableDataAssert tableDataAssert = new TableDataAssert(dataSet, dataSource);
        tableDataAssert.assertEquals("lending_app", new String[]{"lending_app_id", "approval_user_id"});
        tableDataAssert.assertEquals("lending_book", new String[]{"lending_book_id", "lending_app_id", "lending_state", "lending_app_flg", "lending_app_reason", "approval_result", "approval_reason"});

        .....
    }

}

比較検証で差異が見つかると以下の画像のようなエラーが表示されます。

f:id:ksby:20151018213133p:plain

※この記事を書いていて、TableDataAssert を TestWatcher の継承クラスにして、テストメソッドに @AssertDataLoader(...) アノテーションを付加して、テストメソッドの中では単に assertEquals メソッドを呼ぶだけにする、という感じで使用できるようにした方が良さそうに思えたのでメモしておきます。

※TableDataAssert クラスの actualTable メソッドで、getTable(tableName) ではなく createQueryTable(...) メソッドを呼び出してデータの順番が保証されるようにしないといけないことに気づいたので、これもメモしておきます。

MockMvc によるテストのヘルパークラス

TestHelper クラス

以下2つのメソッドを提供するヘルパークラスです。

package ksbysample.common.test;

import org.springframework.http.MediaType;
import org.springframework.test.web.servlet.request.MockHttpServletRequestBuilder;

import java.lang.reflect.Field;
import java.util.List;

import static org.hamcrest.Matchers.is;
import static org.junit.Assert.assertThat;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;

public class TestHelper {

    /**
     * Formクラスをパラメータに持つMockHttpServletRequestBuilderクラスのインスタンスを生成する
     * 
     * @param urlTemplate
     * @param form
     * @return
     * @throws IllegalAccessException
     */
    public static MockHttpServletRequestBuilder postForm(String urlTemplate, Object form) throws IllegalAccessException {
        MockHttpServletRequestBuilder request = post(urlTemplate).contentType(MediaType.APPLICATION_FORM_URLENCODED);
        for (Field field : form.getClass().getDeclaredFields()) {
            field.setAccessible(true);
            if (field.get(form) == null) {
                request = request.param(field.getName(), "");
            }
            else if (field.get(form) instanceof List<?>) {
                for (Object str : (List<?>)field.get(form)) {
                    request = request.param(field.getName(), str.toString());
                }
            }
            else {
                request = request.param(field.getName(), field.get(form).toString());
            }
        }
        return request;
    }

    /**
     * EntityクラスとFormクラスの値が同じかチェックする
     * 
     * @param entity
     * @param form
     * @throws IllegalAccessException
     */
    public static void assertEntityByForm(Object entity, Object form) throws IllegalAccessException {
        for (Field entityField : entity.getClass().getDeclaredFields()) {
            entityField.setAccessible(true);
            try {
                Field formField = form.getClass().getDeclaredField(entityField.getName());
                formField.setAccessible(true);
                assertThat(entityField.get(entity), is(formField.get(form)));
            }
            catch (NoSuchFieldException ignored) {}
        }
    }

}

postForm メソッドは以下の要領で使用します。

  1. YAMLファイルにテストデータを定義しておき、SnakeYAML を利用して Form クラスのインスタンスを生成します。
  2. MockMvc クラスの perform メソッド内で TestHelper.postForm を呼び出して MockHttpServletRequestBuilder クラスのインスタンスを生成します ( MockHttpServletRequestBuilder クラスは RequestBuilder インターフェースの実装クラスで )。TestHelper.postForm の第1引数にはテストで呼び出す URL を、第2引数には Form クラスを渡します。
@RunWith(Enclosed.class)
public class MailsearchControllerTest {

    @RunWith(SpringJUnit4ClassRunner.class)
    @SpringApplicationConfiguration(classes = Application.class)
    @WebAppConfiguration
    public static class 検索条件は何も入力しないで検索する場合 {

        .....

        @Rule
        @Autowired
        public MockMvcResource mvc;

        // テストデータ
        private MailsearchForm mailsearchFormEmpty
                = (MailsearchForm) new Yaml().load(getClass().getResourceAsStream("mailsearchForm_empty.yml"));

        @Test
        @TestDataLoader("src/test/resources/ksbysample/webapp/email/web/mailsearch/testdata")
        public void 検索ボタンをクリックすると1ページ目が表示される() throws Exception {
            mvc.nonauth.perform(TestHelper.postForm("/mailsearch", this.mailsearchFormEmpty))
                    .andExpect(status().isOk())
                    .andExpect(content().contentType("text/html;charset=UTF-8"))
                    .andExpect(view().name("mailsearch/mailsearch"))
                    .andExpect(modelEx().property("page.number", is(0)))
                    .andExpect(modelEx().property("page.size", is(2)))
                    .andExpect(modelEx().property("page.totalPages", is(3)))
                    .andExpect(modelEx().property("ph.page1PageValue", is(0)))
                    .andExpect(modelEx().property("ph.hiddenPrev", is(true)))
                    .andExpect(modelEx().property("ph.hiddenNext", is(false)));
        }

    }

}

assertEntityByForm メソッドは以下の要領で使用します。

  1. MockMvc の perform メソッドで更新用の URL を呼び出して、テストデータをセットした Form クラスを渡してデータを更新します。
  2. Spring Data JPADoma 2 等を利用して更新したデータの Entity クラスのインスタンスを取得します。
  3. TestHelper.assertEntityByForm に Form クラスと Entity クラスを渡して呼び出し、比較検証します。
                @Test
                public void データが問題なければDBにデータが登録され完了画面へリダイレクトされる() throws Exception {
                    // データが問題なくCSRFトークンもある場合にはDBにデータが登録され、登録画面(完了)へリダイレクトされることを確認する
                    secmvc.auth.perform(TestHelper.postForm("/country/update", this.countryFormSuccess)
                                    .with(csrf())
                    )
                            .andExpect(status().isFound())
                            .andExpect(redirectedUrl("/country/complete"))
                            .andExpect(model().hasNoErrors())
                            .andExpect(model().errorCount(0));

                    Country country = countryRepository.findOne("xxx");
                    assertThat(country, is(notNullValue()));
                    TestHelper.assertEntityByForm(country, this.countryFormSuccess);
                }

CustomModelResultMatchers クラス

ResultActions の andExpect メソッド内で Model にセットされたインスタンスのフィールドの値を容易に比較検証できるようにするための カスタム Matcher クラスです。

package ksbysample.common.test;

import org.hamcrest.Matcher;
import org.springframework.expression.EvaluationContext;
import org.springframework.expression.Expression;
import org.springframework.expression.ExpressionParser;
import org.springframework.expression.spel.standard.SpelExpressionParser;
import org.springframework.expression.spel.support.StandardEvaluationContext;
import org.springframework.test.web.servlet.ResultMatcher;
import org.springframework.test.web.servlet.result.ModelResultMatchers;
import org.springframework.ui.ModelMap;

import java.util.regex.Pattern;

import static org.hamcrest.Matchers.is;
import static org.hamcrest.Matchers.notNullValue;
import static org.springframework.test.util.MatcherAssertionErrors.assertThat;

public class CustomModelResultMatchers extends ModelResultMatchers {

    public static CustomModelResultMatchers modelEx() {
        return new CustomModelResultMatchers();
    }

    @SuppressWarnings("unchecked")
    public <T> ResultMatcher property(final String nameAndProperty, final Matcher<T> matcher) {
        return mvcResult -> {
            // <インスタンス名>.<プロパティ名> ( 例: page.number ) の形式の文字列を
            // インスタンス名とプロパティ名に分割する
            Pattern p = Pattern.compile("^(\\S+?)\\.(\\S+)$");
            java.util.regex.Matcher m = p.matcher(nameAndProperty);
            assertThat(m.find(), is(true));
            String name = m.group(1);
            String property = m.group(2);

            // プロパティの値を取得してチェックする
            ModelMap modelMap = mvcResult.getModelAndView().getModelMap();
            Object object = modelMap.get(name);
            assertThat(object, is(notNullValue()));
            EvaluationContext context = new StandardEvaluationContext(object);
            ExpressionParser parser = new SpelExpressionParser();
            Expression exp = parser.parseExpression(property);
            Object value = exp.getValue(context);
            assertThat((T) value, matcher);
        };
    }

}

使用方法ですが、andExpect メソッド内で modelEx().property(...) を呼び出し、第1引数には <インスタンス名>.<プロパティ名> ( 例: page.number ) の形式の文字列でチェックするインスタンスのフィールドを指定し、第2引数には期待値を指定した Matcher オブジェクトを渡します。

@RunWith(Enclosed.class)
public class MailsearchControllerTest {

    @RunWith(SpringJUnit4ClassRunner.class)
    @SpringApplicationConfiguration(classes = Application.class)
    @WebAppConfiguration
    public static class 検索条件は何も入力しないで検索する場合 {

        .....

        @Rule
        @Autowired
        public MockMvcResource mvc;

        // テストデータ
        private MailsearchForm mailsearchFormEmpty
                = (MailsearchForm) new Yaml().load(getClass().getResourceAsStream("mailsearchForm_empty.yml"));

        @Test
        @TestDataLoader("src/test/resources/ksbysample/webapp/email/web/mailsearch/testdata")
        public void 検索ボタンをクリックすると1ページ目が表示される() throws Exception {
            mvc.nonauth.perform(TestHelper.postForm("/mailsearch", this.mailsearchFormEmpty))
                    .andExpect(status().isOk())
                    .andExpect(content().contentType("text/html;charset=UTF-8"))
                    .andExpect(view().name("mailsearch/mailsearch"))
                    .andExpect(modelEx().property("page.number", is(0)))
                    .andExpect(modelEx().property("page.size", is(2)))
                    .andExpect(modelEx().property("page.totalPages", is(3)))
                    .andExpect(modelEx().property("ph.page1PageValue", is(0)))
                    .andExpect(modelEx().property("ph.hiddenPrev", is(true)))
                    .andExpect(modelEx().property("ph.hiddenNext", is(false)));
        }

    }

}

ErrorsResultMatchers クラス

ResultActions の andExpect メソッド内で GlobalError, FieldError を容易に比較検証できるようにするための カスタム Matcher クラスです。

GlobalError とは org.springframework.validation.Errors インターフェースの reject メソッドerrors.reject("mailsendForm.item.noSelect"); のように呼び出してセットされたエラーを指します。ちなみに BindingResult は Errors インターフェースを継承したインターフェースです。

FieldError とは org.springframework.validation.Errors インターフェースの rejectValue メソッドでフィールド名を指定してセットされたエラーを指します。@Size や @Pattern 等の Bean Validation でセットされるエラーも FieldError です。

package ksbysample.common.test;

import org.springframework.test.web.servlet.ResultMatcher;
import org.springframework.test.web.servlet.result.ModelResultMatchers;
import org.springframework.validation.BindingResult;
import org.springframework.validation.FieldError;
import org.springframework.validation.ObjectError;
import org.springframework.web.servlet.ModelAndView;

import java.util.List;
import java.util.stream.Collectors;

import static org.hamcrest.Matchers.hasItem;
import static org.junit.Assert.assertThat;
import static org.springframework.test.util.AssertionErrors.assertTrue;

public class ErrorsResultMatchers extends ModelResultMatchers {

    private ErrorsResultMatchers() {
    }

    public static ErrorsResultMatchers errors() {
        return new ErrorsResultMatchers();
    }

    public ResultMatcher hasGlobalError(String name, String error) {
        return mvcResult -> {
            BindingResult bindingResult = getBindingResult(mvcResult.getModelAndView(), name);
            List<ObjectError> objectErrorList = bindingResult.getGlobalErrors();
            List<String> objectErrorListAsCode
                    = objectErrorList.stream().map(ObjectError::getCode).collect(Collectors.toList());
            assertThat("Expected error code '" + error + "'", objectErrorListAsCode, hasItem(error));
        };
    }

    public ResultMatcher hasFieldError(String name, String fieldName, String error) {
        return mvcResult -> {
            BindingResult bindingResult = getBindingResult(mvcResult.getModelAndView(), name);
            List<FieldError> fieldErrorList = bindingResult.getFieldErrors(fieldName);
            List<String> fieldErrorListAsCode
                    = fieldErrorList.stream().map(FieldError::getCode).collect(Collectors.toList());
            assertThat("Expected error code '" + error + "'", fieldErrorListAsCode, hasItem(error));
        };
    }

    private BindingResult getBindingResult(ModelAndView mav, String name) {
        BindingResult bindingResult = (BindingResult) mav.getModel().get(BindingResult.MODEL_KEY_PREFIX + name);
        assertTrue("No BindingResult for attribute: " + name, bindingResult != null);
        return bindingResult;
    }

}

GlobalError は hasGlobalError メソッドで比較検証します。andExpect メソッド内で errors().hasGlobalError(...) を呼び出し、第1引数にエラーがセットされる Model の名前を、第2引数にエラーコード ( messages_ja_JP.properties や ValidationMessages_ja_JP.properties の中で定義する message key ) を指定します。

        @Test
        public void 項目が資料請求_商品に関する苦情の場合に商品が何も選択されていない場合には入力チェックエラー() throws Exception {
            mvc.nonauth.perform(TestHelper.postForm("/mailsend/send", this.mailsendFormFv01))
                    .andExpect(status().isOk())
                    .andExpect(content().contentType("text/html;charset=UTF-8"))
                    .andExpect(view().name("mailsend/mailsend"))
                    .andExpect(model().hasErrors())
                    .andExpect(model().errorCount(1))
                    .andExpect(errors().hasGlobalError("mailsendForm", "mailsendForm.item.noSelect"));
        }

FieldError は hasFieldError メソッドで比較検証します。andExpect メソッド内で errors().hasFieldError(...) を呼び出し、第1引数にエラーがセットされる Model の名前を、第2引数にフィールド名を、第3引数にエラーコード ( message key も指定できますが Bean Validation のエラーチェックの場合には Bean Validation のアノテーション名を指定します ) を指定します。

        @Test
        public void 最大文字数オーバー_パターンエラー時には入力チェックエラー() throws Exception {
            // Email/Size/Pattern のテスト
            mvc.nonauth.perform(TestHelper.postForm("/mailsend/send", this.mailsendFormMaxPatternerr))
                    .andExpect(status().isOk())
                    .andExpect(content().contentType("text/html;charset=UTF-8"))
                    .andExpect(view().name("mailsend/mailsend"))
                    .andExpect(model().hasErrors())
                    .andExpect(model().errorCount(7))
                    .andExpect(errors().hasFieldError("mailsendForm", "fromAddr", "Email"))
                    .andExpect(errors().hasFieldError("mailsendForm", "toAddr", "Email"))
                    .andExpect(errors().hasFieldError("mailsendForm", "subject", "Size"))
                    .andExpect(errors().hasFieldError("mailsendForm", "name", "Size"))
                    .andExpect(errors().hasFieldError("mailsendForm", "sex", "Pattern"))
                    .andExpect(errors().hasFieldError("mailsendForm", "type", "Pattern"))
                    .andExpect(errors().hasFieldError("mailsendForm", "naiyo", "Size"));
        }

履歴

2015/10/19
初版発行。