10.2.2. レイヤごとのテスト実装¶
レイヤごとの単体テスト対象クラス、テスト方法およびその概要の一覧を以下に示す。
なお、本章で提示するテスト方法および実装はあくまで一例であり、実際はテスト方針に合わせたテスト方法および実装を検討いただきたい。
レイヤ  | 
テスト方法  | 
概要  | 
|---|---|---|
インフラストラクチャ層  | 
Spring Testの標準的な機能を使用してデータアクセスのテストを行う。  | 
|
インフラストラクチャ層  | 
DBUnitとSpring Test DBUnitの機能を使用してデータアクセスのテストを行う。  | 
|
ドメイン層  | 
Spring TestのDI機能を使用して  | 
|
ドメイン層  | 
Mockitoを使用して依存するクラスをモック化し  | 
|
アプリケーション層  | 
Spring TestのDI機能を使用して  | 
|
アプリケーション層  | 
Spring TestのMockMvcを使用して業務で作成した  | 
|
アプリケーション層  | 
Mockitoを使用して依存するクラスをモック化し  | 
|
アプリケーション層  | 
  | 
10.2.2.1. インフラストラクチャ層の単体テスト¶
本節では、開発ガイドラインのインフラストラクチャ層の単体テストについて説明する。
RepositoryImplはSpringのDIコンテナ上で実行されるため、テストには、本番同様のBean定義と、SpringのDI機能を提供するSpring TestのSpringJUnit4ClassRunnerを使用する。テスト実行後のデータベースの状態をSELECT文を使用して取得し検証する。
DBUnitとSpring Test DBUnitを使用して検証する。
JdbcTemplateを使用した場合を例に説明する。JdbcTemplateとはSpring JDBCサポートのコアクラスである。JDBC APIではデータソースからコネクションの取得、PreparedStatementの作成、ResultSetの解析、コネクションの解放などを行う必要があるが、JdbcTemplateを使用することでこれらの処理の多くが隠蔽され、より簡単にデータアクセスを行うことができる。Note
アプリケーションのレイヤ化では、Repositoryインターフェイスはドメイン層の成果物であるが、インフラストラクチャ層の単体テスト対象として紹介している。Serviceとのインターフェイスが正しいことは、ドメイン層の単体テストでも確認することを推奨する。
10.2.2.1.1. Repositoryの単体テスト¶
本節では、以下のRepositoryの単体テスト実装方法を説明する。
テスト方法  | 
説明  | 
|---|---|
  | 
|
DBUnit、Spring Test DBUnitの機能を使用してテスト結果の検証を行う。  | 
Repositoryインタフェース(MemberRepository)の更新処理(updateMemberLoginメソッド)マッピングファイル(
MemberRepository.xml)
以下に、テスト対象の実装例を示す。
MemberRepository.java
public interface MemberRepository {
    int updateMemberLogin(Member member);
}
MemberRepository.xml
<mapper namespace="com.example.domain.repository.member.MemberRepository">
  <update id="updateMemberLogin" parameterType="Member">
    UPDATE member_login SET
        last_password = password,
        password = #{memberLogin.password}
    WHERE
        customer_no = #{membershipNumber}
  </update>
</mapper>
10.2.2.1.1.1. Spring Test標準機能のみを利用したテスト¶
Repositoryの単体テストにおいて、作成するファイルを以下に示す。
作成するファイル名  | 
説明  | 
|---|---|
  | 
  | 
  | 
Spring Testを使用して単体テストを行う際に必要な設定を補うための設定ファイル。  | 
  | 
単体テストで利用するデータベースのデータをセットアップするためのSQLファイル。  | 
Note
単体テストで利用するSQLファイルの作成単位
ここでは、1テストメソッドに1つのSQLを作成している。実際の作成単位については、テスト方針や内容に応じて適宜検討されたい。なお、@SqlにSQLファイルパスを省略した場合、@Sqlの指定場所に基づいてSQLファイルの検索が行われる。
詳細は、@SqlのSQLファイルパスの省略を参照されたい。
Spring Testを使用する場合のRepositoryのテストクラス作成方法を説明する。
以下に、データアクセスを利用してテストするために使用する設定ファイルを示す。
SampleInfraConfig.java
@Bean
public MapperScannerConfigurer mapperScannerConfigurer() {
    MapperScannerConfigurer bean = new MapperScannerConfigurer();
    bean.setBasePackage("com.example.domain.repository");
    return bean;
}
@Bean("sqlSessionFactory")
public SqlSessionFactoryBean sqlSessionFactory(DataSource dataSource) {
    SqlSessionFactoryBean bean = new SqlSessionFactoryBean();
    bean.setDataSource(dataSource);
    bean.setConfiguration(MybatisConfig.configuration());
    return bean;
}
SampleEnvConfig.java
/**
 * DataSource.driverClassName property.
 */
@Value("${database.driverClassName}")
private String driverClassName;
/**
 * DataSource.url property.
 */
@Value("${database.url}")
private String url;
/**
 * DataSource.username property.
 */
@Value("${database.username}")
private String username;
/**
 * DataSource.password property.
 */
@Value("${database.password}")
private String password;
/**
 * DataSource.maxTotal property.
 */
@Value("${cp.maxActive}")
private Integer maxActive;
/**
 * DataSource.maxIdle property.
 */
@Value("${cp.maxIdle}")
private Integer maxIdle;
/**
 * DataSource.minIdle property.
 */
@Value("${cp.minIdle}")
private Integer minIdle;
/**
 * DataSource.maxWaitMillis property.
 */
@Value("${cp.maxWait}")
private Integer maxWait;
/**
 * Property databaseName.
 */
@Value("${database}")
private String database;
/**
 * Configure {@link TransactionManager} bean.
 * @param dataSource Bean defined by #dataSource()
 * @see #dataSource()
 * @return Bean of configured {@link DataSourceTransactionManager}
 */
@Bean("transactionManager")
public TransactionManager transactionManager(DataSource dataSource) {
    DataSourceTransactionManager bean = new DataSourceTransactionManager();
    bean.setDataSource(dataSource);
    return bean;
}
/**
 * Configure {@link ClockFactory} bean.
 * @return Bean of configured {@link DefaultClockFactory}
 */
@Bean("dateFactory")
public ClockFactory dateFactory() {
    return new DefaultClockFactory();
}
/**
 * Configure {@link DataSource} bean.
 * @return Bean of configured {@link BasicDataSource}
 */
@Bean(name = "dataSource", destroyMethod = "close")
public DataSource dataSource() {
    BasicDataSource bean = new BasicDataSource();
    bean.setDriverClassName(driverClassName);
    bean.setUrl(url);
    bean.setUsername(username);
    bean.setPassword(password);
    bean.setDefaultAutoCommit(false);
    bean.setMaxTotal(maxActive);
    bean.setMaxIdle(maxIdle);
    bean.setMinIdle(minIdle);
    bean.setMaxWait(Duration.ofMillis(maxWait));
    return bean;
}
作成するファイル名  | 
説明  | 
|---|---|
  | 
  | 
  | 
Spring Testを使用して単体テストを行う際に必要な設定を補うための設定ファイル。  | 
  | 
単体テストで利用するデータベースのデータをセットアップするためのSQLファイル。  | 
Note
単体テストで利用するSQLファイルの作成単位
ここでは、1テストメソッドに1つのSQLを作成している。実際の作成単位については、テスト方針や内容に応じて適宜検討されたい。なお、@SqlにSQLファイルパスを省略した場合、@Sqlの指定場所に基づいてSQLファイルの検索が行われる。
詳細は、@SqlのSQLファイルパスの省略を参照されたい。
Spring Testを使用する場合のRepositoryのテストクラス作成方法を説明する。
以下に、データアクセスを利用してテストするために使用する設定ファイルを示す。
sample-infra.xml
<import resource="classpath:/META-INF/spring/sample-env.xml" />
<!-- define the SqlSessionFactory -->
<bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
  <property name="dataSource" ref="dataSource" />
  <property name="configLocation" value="classpath:/META-INF/mybatis/mybatis-config.xml" />
</bean>
<!-- scan for Mappers -->
<mybatis:scan base-package="com.example.domain.repository" />
sample-env.xml
<bean id="dataSource" class="org.apache.commons.dbcp2.BasicDataSource" destroy-method="close">
  <property name="driverClassName" value="org.postgresql.Driver" />
  <property name="url" value="jdbc:postgresql://localhost:5432/sample" />
  <property name="username" value="sample" />
  <property name="password" value="xxxx" />
  <property name="defaultAutoCommit" value="false" />
  <property name="maxTotal" value="96" />
  <property name="maxIdle" value="16" />
  <property name="minIdle" value="0" />
  <property name="maxWait" >
    <bean class="java.time.Duration" factory-method="ofMillis">
      <constructor-arg value="60000" />
    </bean>
  </property>
</bean>
<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
  <property name="dataSource" ref="dataSource" />
</bean>
<bean id="dateFactory" class="org.terasoluna.gfw.common.time.DefaultClockFactory" />
Repositoryのテスト作成方法について説明する。@Sqlアノテーションを使用してMemberLoginテーブルをセットアップし、MemberLoginのパスワード「ABCDE」が新しいパスワード「FGHIJ」に更新されることを更新後のMemberLoginテーブルを取得して確認している。MemberRepositoryTest.java
import static org.hamcrest.CoreMatchers.is;
import static org.hamcrest.MatcherAssert.assertThat;
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = { TestContextConfig.class, SampleEnvConfig.class, SampleInfraConfig.class }) // (1)
@Transactional // (2)
public class MemberRepositoryTest {
    @Inject
    MemberRepository target; // (3)
    @Inject
    JdbcTemplate jdbctemplate; // (4)
    @Test
    @Sql(scripts = "classpath:META-INF/sql/setupMemberLogin.sql", config = @SqlConfig(encoding = "utf-8"))
    public void testUpdateMemberLogin() {
        // (5)
        // setup test data
        MemberLogin memberLogin = new MemberLogin();
        memberLogin.setPassword("FGHIJ");
        Member member = new Member();
        member.setMembershipNumber("0000000001");
        member.setMemberLogin(memberLogin);
        // (6)
        // run the test
        int updateCounts = target.updateMemberLogin(member);
        // (7)
        MemberLogin updateMemberLogin = getMemberLogin("0000000001");
        // (8)
        // assertion
        assertThat(updateCounts, is(1));
        assertThat(updateMemberLogin.getPassword(), is("FGHIJ"));
        assertThat(updateMemberLogin.getLastPassword(), is("ABCDE"));
    }
    private Member getMemberLogin(String customerNo) {
        MemberLogin memberLogin = jdbctemplate.queryForObject(
                "SELECT * FROM member_login WHERE customer_no=?",
                new RowMapper<MemberLogin>() {
                    public MemberLogin mapRow(ResultSet rs,
                                int rowNum) throws SQLException {
                            MemberLogin mapMemberLogin = new MemberLogin();
                            mapMemberLogin.setPassword(rs.getString(
                                    "password"));
                            mapMemberLogin.setLastPassword(rs.getString(
                                    "last_password"));
                            mapMemberLogin.setLoginDateTime(rs.getDate(
                                    "login_date_time"));
                            mapMemberLogin.setLoginFlg(rs.getBoolean(
                                    "login_flg"));
                            return mapMemberLogin;
                    }
                }, customerNo);
        return memberLogin;
    }
項番  | 
説明  | 
|---|---|
(1) 
 | 
MemberRepositoryクラスを動作させるために必要なアプリケーションが保持するTestContextConfig.class、SampleEnvConfig.classとSampleInfraConfig.classを読み込む。 | 
(2) 
 | 
@Transactionalアノテーションを付与すると、テスト実行開始から終了まで一トランザクションとなり、デフォルトではテスト終了後にロールバックされる。クラスレベルでアノテーションを定義すると、全テストメソッドに対して@Transactionalアノテーションが有効になる。 | 
(3) 
 | 
テスト対象である 
MemberRepositoryクラスをインジェクションする。 | 
(4) 
 | 
JdbcTemplateクラスをインジェクションする。 | 
(5) 
 | 
テスト対象メソッドを実行するためのテストデータを作成する。 
 | 
(6) 
 | 
テスト対象メソッドを実行する。 
 | 
(7) 
 | 
更新後のデータベースの情報を取得する。 
org.springframework.jdbc.core.RowMapper<T>を使用することで、データベースから取得したResultSetを特定のPOJOクラスにマッピングすることができる。 | 
(8) 
 | 
更新件数、更新結果を確認する。 
 | 
MemberRepositoryTest.java
import static org.hamcrest.CoreMatchers.is;
import static org.hamcrest.MatcherAssert.assertThat;
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations = {
        "classpath:META-INF/spring/sample-infra.xml",   // (1)
        "classpath:META-INF/spring/test-context.xml" }) // (1)
@Transactional // (2)
public class MemberRepositoryTest {
    @Inject
    MemberRepository target; // (3)
    @Inject
    JdbcTemplate jdbctemplate; // (4)
    @Test
    @Sql(scripts = "classpath:META-INF/sql/setupMemberLogin.sql", config = @SqlConfig(encoding = "utf-8"))
    public void testUpdateMemberLogin() {
        // (5)
        // setup test data
        MemberLogin memberLogin = new MemberLogin();
        memberLogin.setPassword("FGHIJ");
        Member member = new Member();
        member.setMembershipNumber("0000000001");
        member.setMemberLogin(memberLogin);
        // (6)
        // run the test
        int updateCounts = target.updateMemberLogin(member);
        // (7)
        MemberLogin updateMemberLogin = getMemberLogin("0000000001");
        // (8)
        // assertion
        assertThat(updateCounts, is(1));
        assertThat(updateMemberLogin.getPassword(), is("FGHIJ"));
        assertThat(updateMemberLogin.getLastPassword(), is("ABCDE"));
    }
    private Member getMemberLogin(String customerNo) {
        MemberLogin memberLogin = jdbctemplate.queryForObject(
                "SELECT * FROM member_login WHERE customer_no=?",
                new RowMapper<MemberLogin>() {
                    public MemberLogin mapRow(ResultSet rs,
                                int rowNum) throws SQLException {
                            MemberLogin mapMemberLogin = new MemberLogin();
                            mapMemberLogin.setPassword(rs.getString(
                                    "password"));
                            mapMemberLogin.setLastPassword(rs.getString(
                                    "last_password"));
                            mapMemberLogin.setLoginDateTime(rs.getDate(
                                    "login_date_time"));
                            mapMemberLogin.setLoginFlg(rs.getBoolean(
                                    "login_flg"));
                            return mapMemberLogin;
                    }
                }, customerNo);
        return memberLogin;
    }
項番  | 
説明  | 
|---|---|
(1) 
 | 
MemberRepositoryクラスを動作させるために必要なアプリケーションが保持するsample-infra.xmlとtest-context.xmlを読み込む。 | 
(2) 
 | 
@Transactionalアノテーションを付与すると、テスト実行開始から終了まで一トランザクションとなり、デフォルトではテスト終了後にロールバックされる。クラスレベルでアノテーションを定義すると、全テストメソッドに対して@Transactionalアノテーションが有効になる。 | 
(3) 
 | 
テスト対象である 
MemberRepositoryクラスをインジェクションする。 | 
(4) 
 | 
JdbcTemplateクラスをインジェクションする。 | 
(5) 
 | 
テスト対象メソッドを実行するためのテストデータを作成する。 
 | 
(6) 
 | 
テスト対象メソッドを実行する。 
 | 
(7) 
 | 
更新後のデータベースの情報を取得する。 
org.springframework.jdbc.core.RowMapper<T>を使用することで、データベースから取得したResultSetを特定のPOJOクラスにマッピングすることができる。 | 
(8) 
 | 
更新件数、更新結果を確認する。 
 | 
Note
テスト時のトランザクションをロールバックさせない方法
@Transactionalアノテーションをテストケースに指定した場合、デフォルトでテストメソッド実行後にロールバックされる。
後続のテストでテストデータを使用するなどの目的でロールバックをさせたくない場合は、@Transactionalアノテーションに加えて@Rollback(false)アノテーションまたは@Commitアノテーションを指定することで、テスト時のトランザクションをコミットすることができる。
Warning
Spring Framework 4.2 以降の@TransactionConfigurationについて
Spring Framework 4.2 以降、クラスレベルで@Rollbackまたは@Commitの設定が可能となった。
これに伴い@TransactionConfigurationが非推奨となった。但し、Spring Framework 4.2 より前のバージョンでクラスレベルでロールバックをする場合は@TransactionConfiguration(defaultRollback = true)を設定すること。
10.2.2.1.1.2. Spring Test DBUnitを利用したテスト¶
Repositoryの単体テスト実装方法について説明する。@TestExecutionListenersアノテーションを使って、com.github.springtestdbunit.TransactionDbUnitTestExecutionListenerを登録する必要がある。DBUnitを利用したRepositoryの単体テストにおいて、作成するファイルを以下に示す。
作成するファイル名  | 
説明  | 
|---|---|
  | 
  | 
  | 
Excel形式に対応する  | 
  | 
テストの期待結果検証用ファイル  | 
  | 
テストデータセットアップ用ファイル  | 
  | 
Spring Testを使用して単体テストを行う際に使用する設定ファイル。Spring Test標準機能のみを利用したテストで作成した設定ファイルと同じものを使用する。  | 
Note
単体テストで利用するExcelファイルの作成単位
ここでは、1テストメソッドにデータセットアップ用のファイルと期待結果検証用のファイルをそれぞれ1つずつ作成している。
実際の作成単位については、テスト方針や内容に応じて適宜検討されたい。
DBUnitを使用する場合のRepositoryのテストクラス作成方法を説明する。
ここでは、テスト用のスキーマは作成済みであることを前提に、@DatabaseSetupアノテーションを使用してMemberLoginテーブルをセットアップし、MemberLoginのパスワード「ABCDE」が新しいパスワード「FGHIJ」に更新されることを@ExpectedDatabaseアノテーションを使用して確認している。
以下に、Spring TestとDBUnitを使用したRepositoryのテスト作成方法を説明する。
MemberRepositoryDbunitTest.java
import static org.hamcrest.CoreMatchers.is;
import static org.hamcrest.MatcherAssert.assertThat;
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = { TestContextConfig.class, SampleEnvConfig.class, SampleInfraConfig.class }) // (1)
@TestExecutionListeners({
        DirtiesContextBeforeModesTestExecutionListener.class,
        DependencyInjectionTestExecutionListener.class,
        DirtiesContextTestExecutionListener.class,
        TransactionDbUnitTestExecutionListener.class})
@Transactional
@DbUnitConfiguration(dataSetLoader = XlsDataSetLoader.class)
public class MemberRepositoryDbunitTest {
    @Inject
    MemberRepository target;
    @Test
    @DatabaseSetup("classpath:META-INF/dbunit/setup_MemberLogin.xlsx")
    @ExpectedDatabase( // (2)
            value = "classpath:META-INF/dbunit/expected_testUpdateMemberLogin.xlsx",
            assertionMode = DatabaseAssertionMode.NON_STRICT_UNORDERED)
    public void testUpdate() {
        // setup
        MemberLogin memberLogin = new MemberLogin();
        memberLogin.setPassword("FGHIJ");
        Member member = new Member();
        member.setMembershipNumber("0000000001");
        member.setMemberLogin(memberLogin);
        // run the test
        int updateCounts = target.updateMemberLogin(member);
        // assertion
        assertThat(updateCounts, is(1));
    }
}
項番  | 
説明  | 
|---|---|
(1) 
 | 
MemberRepositoryクラスを動作させるために必要な設定ファイル(アプリケーションが保持するSampleInfraConfig.classとそれを補うTestContextConfig.class)を読み込む。 | 
(2) 
 | 
@ExpectedDatabaseアノテーションにテストの期待結果検証用ファイルを指定することでテストメソッド実行後にDBUnitによってテーブルと期待結果データファイルが自動で比較検証される。@DatabaseSetupアノテーション同様に、クラスレベルとメソッドレベルで付与できる。ファイルフォーマットはテストセットアップ用データファイルと同じである。 
assertionMode属性には、以下の値が設定可能である。
  | 
作成するファイル名  | 
説明  | 
|---|---|
  | 
  | 
  | 
Excel形式に対応する  | 
  | 
テストの期待結果検証用ファイル  | 
  | 
テストデータセットアップ用ファイル  | 
  | 
Spring Testを使用して単体テストを行う際に使用する設定ファイル。Spring Test標準機能のみを利用したテストで作成した設定ファイルと同じものを使用する。  | 
Note
単体テストで利用するExcelファイルの作成単位
ここでは、1テストメソッドにデータセットアップ用のファイルと期待結果検証用のファイルをそれぞれ1つずつ作成している。
実際の作成単位については、テスト方針や内容に応じて適宜検討されたい。
DBUnitを使用する場合のRepositoryのテストクラス作成方法を説明する。
ここでは、テスト用のスキーマは作成済みであることを前提に、@DatabaseSetupアノテーションを使用してMemberLoginテーブルをセットアップし、MemberLoginのパスワード「ABCDE」が新しいパスワード「FGHIJ」に更新されることを@ExpectedDatabaseアノテーションを使用して確認している。
以下に、Spring TestとDBUnitを使用したRepositoryのテスト作成方法を説明する。
MemberRepositoryDbunitTest.java
import static org.hamcrest.CoreMatchers.is;
import static org.hamcrest.MatcherAssert.assertThat;
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations = {
        "classpath:META-INF/spring/sample-infra.xml",   // (1)
        "classpath:META-INF/spring/test-context.xml" }) // (1)
@TestExecutionListeners({
        DirtiesContextBeforeModesTestExecutionListener.class,
        DependencyInjectionTestExecutionListener.class,
        DirtiesContextTestExecutionListener.class,
        TransactionDbUnitTestExecutionListener.class})
@Transactional
@DbUnitConfiguration(dataSetLoader = XlsDataSetLoader.class)
public class MemberRepositoryDbunitTest {
    @Inject
    MemberRepository target;
    @Test
    @DatabaseSetup("classpath:META-INF/dbunit/setup_MemberLogin.xlsx")
    @ExpectedDatabase( // (2)
            value = "classpath:META-INF/dbunit/expected_testUpdateMemberLogin.xlsx",
            assertionMode = DatabaseAssertionMode.NON_STRICT_UNORDERED)
    public void testUpdate() {
        // setup
        MemberLogin memberLogin = new MemberLogin();
        memberLogin.setPassword("FGHIJ");
        Member member = new Member();
        member.setMembershipNumber("0000000001");
        member.setMemberLogin(memberLogin);
        // run the test
        int updateCounts = target.updateMemberLogin(member);
        // assertion
        assertThat(updateCounts, is(1));
    }
}
項番  | 
説明  | 
|---|---|
(1) 
 | 
MemberRepositoryクラスを動作させるために必要な設定ファイル(アプリケーションが保持するsample-infra.xmlとそれを補うtest-context.xml)を読み込む。 | 
(2) 
 | 
@ExpectedDatabaseアノテーションにテストの期待結果検証用ファイルを指定することでテストメソッド。実行後にDBUnitによってテーブルと期待結果データファイルが自動で比較検証される。@DatabaseSetupアノテーション同様に、クラスレベルとメソッドレベルで付与できる。ファイルフォーマットはテストセットアップ用データファイルと同じである。 
assertionMode属性には、以下の値が設定可能である。
  | 
Warning
外部キー制約のあるテーブル
外部キー制約のあるテーブルに対し、DBUnitを用いてデータベースを初期化すると、参照条件によってはエラーが発生するため、参照整合性を保つようにデータセットの順序を指定する必要があることに注意されたい。
Note
シーケンスの検証方法
シーケンスは、トランザクションをロールバックしても進んだ値は戻らないという特徴を持つ。そのため、シーケンスから採番したカラムを持つレコードをDBUnitで検証する場合、以下のいずれかの対応を行う必要がある。
シーケンスから採番したカラムは検証対象外とする
明示的にシーケンスの初期化を行うSQLを実行し、テストの実施前に初期化する
テスト実行時にシーケンスの値を確認し、確認した値を基準値として検証を行う
10.2.2.2. ドメイン層の単体テスト¶
本節では、開発ガイドラインのドメイン層の単体テストについて説明する。
Serviceの業務ロジックと@Transactionalのテストを行う。Serviceをインジェクションし、インフラストラクチャ層を結合してテストを行う場合は、Repositoryのテスト実装方法と同様にBean定義と、Spring TestのSpringJUnit4ClassRunnerを使用してテストを行う。10.2.2.2.1. Serviceの単体テスト¶
本節では、以下のServiceのテスト実装方法を説明する。
テスト方法  | 
説明  | 
|---|---|
  | 
|
  | 
Serviceの実装クラス(
TicketReserveServiceImpl)
以下に、テスト対象の実装例を示す。
TicketReserveServiceImpl.java
@Service
@Transactional
public class TicketReserveServiceImpl implements TicketReserveService {
    @Inject
    ReservationRepository reservationRepository;
    @Override
    public TicketReserveDto registerReservation(Reservation reservation)
            throws BusinessException {
        List<ReserveFlight> reserveFlightList = reservation.getReserveFlightList();
        // repository access
        int reservationInsertCount = reservationRepository.insert(reservation);
        if (reservationInsertCount != 1) {
            throw new SystemException(LogMessages.E_AR_A0_L9002.getCode(),
                    LogMessages.E_AR_A0_L9002.getMessage(reservationInsertCount, 1));
        }
        String reserveNo = reservation.getReserveNo();
        Date paymentDate = reserveFlightList.get(0).getFlight().getDepartureDate();
        return new TicketReserveDto(reserveNo, paymentDate);
    }
}
以下に、テスト対象が使用するマッピングファイルを示す。
ReservationRepository.xml
<mapper namespace="com.example.domain.repository.reservation.ReservationRepository">
  <insert id="insert" parameterType="Reservation">
    <selectKey keyProperty="reserveNo" resultType="String" order="BEFORE">
      SELECT TO_CHAR(NEXTVAL('sq_reservation_1'), 'FM0999999999')
    </selectKey>
    INSERT INTO reservation
    (
        reserve_no,
        reserve_date,
        total_fare,
        rep_family_name,
        rep_given_name,
        rep_age,
        rep_gender,
        rep_tel,
        rep_mail,
        rep_customer_no
    )
    VALUES
    (
        #{reserveNo},
        #{reserveDate},
        #{totalFare},
        #{repFamilyName},
        #{repGivenName},
        #{repAge},
        #{repGender.code},
        #{repTel},
        #{repMail},
        NULLIF(#{repMember.membershipNumber}, '')
    )
  </insert>
10.2.2.2.1.1. 依存クラスを利用したテスト¶
Serviceをインジェクションし、インフラストラクチャ層を結合して行う。Serviceのテストにおいて、作成するファイルを以下に示す。
作成するファイル名  | 
説明  | 
|---|---|
  | 
  | 
  | 
テスト実装例で使用する設定ファイルで定義した設定ファイルを使用する。  | 
テスト対象のServiceの実装クラスをインジェクションしてインフラストラクチャ層と結合してテストを行う場合のテスト作成方法を説明する。
以下に、テスト時に読み込む設定ファイルを示す。
SampleDomainConfig.java
@Configuration
@EnableTransactionManagement
@ComponentScan(basePackages = {"com.example.domain"})
public class SampleDomainConfig {
    /**
     * Configure messages logging AOP.
     * @param exceptionLogger Bean defined by ApplicationContextConfig#exceptionLogger
     * @see com.example.config.app.ApplicationContextConfig#exceptionLogger(ExceptionCodeResolver)
     * @return Bean of configured {@link ResultMessagesLoggingInterceptor}
     */
    @Bean
    public ResultMessagesLoggingInterceptor resultMessagesLoggingInterceptor(ExceptionLogger exceptionLogger) {
        ResultMessagesLoggingInterceptor bean = new ResultMessagesLoggingInterceptor();
        bean.setExceptionLogger(exceptionLogger);
        return bean;
    }
    /**
     * Configure messages logging AOP advisor.
     * @param resultMessagesLoggingInterceptor Bean defined by #resultMessagesLoggingInterceptor
     * @see #resultMessagesLoggingInterceptor(ExceptionLogger)
     * @return Advisor configured for PointCut
     */
    @Bean
    public Advisor resultMessagesLoggingInterceptorAdvisor(ResultMessagesLoggingInterceptor resultMessagesLoggingInterceptor) {
       AspectJExpressionPointcut pointcut = new AspectJExpressionPointcut();
       pointcut.setExpression("@within(org.springframework.stereotype.Service)");
       return new DefaultPointcutAdvisor(pointcut, resultMessagesLoggingInterceptor);
    }
}
以下に、テスト実装例を示す。
テスト対象のTicketReserveServiceImpl#registerReservation()メソッドを実行し、戻り値を確認している。なお、データベースの状態の検証方法はRepositoryの単体テストを参照されたい。
TicketReserveServiceImplTest.java
import static org.hamcrest.CoreMatchers.is;
import static org.hamcrest.MatcherAssert.assertThat;
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = { TestContextConfig.class, SampleEnvConfig.class, SampleInfraConfig.class,
          SampleDomainConfig.class }) // (1)
@Transactional
public class TicketReserveServiceImplTest {
    @Inject
    TicketReserveService target;
    @Inject
    private JdbcTemplate jdbcTemplate;
    @Test
    @Sql(statements = "ALTER SEQUENCE sq_reservation_1 RESTART WITH 1") // (2)
    public void testRegisterReservation() {
        // setup
        Reservation inputReservation = new Reservation();
        inputReservation.setTotalFare(39200);
        inputReservation.setReserveNo("0000000001");
        // omitted
        // run the test
        TicketReserveDto actTicketReserveDto = target.registerReservation(
                reservation);
        // assertion
        assertThat(actTicketReserveDto.getReserveNo(), is("0000000001"));
        // omitted
    }
}
項番  | 
説明  | 
|---|---|
(1) 
 | 
TicketReserveServiceImplクラスを動作させるために必要な設定ファイル(アプリケーションが保持するSampleDomainConfig.classとそれを補うTestContextConfig.class)を読み込む。 | 
(2) 
 | 
@Sqlのstatements属性を使用することでSQL文を直接指定することもできる。ここではテストメソッド実行前にシーケンスの初期化を行っている。 | 
Warning
テスト時のトランザクション管理
テストケースに@Transactionalアノテーションを付与すると、テスト実行開始から終了まで一トランザクションとなる。そのため、テストケースから@Transactionalアノテーションを付与したServiceクラスを呼び出した場合、テストケースからトランザクションが引き継がれる点に注意すること。
例えば、トランザクションの伝播方法がデフォルト(REQUIRED)の場合、テストケースで開始したトランザクションでテスト対象の処理が行われ、コミット/ロールバックのタイミングもテスト終了時になる。
トランザクションの伝播方法については「宣言型トランザクション管理」で必要となる情報を参照されたい。
作成するファイル名  | 
説明  | 
|---|---|
  | 
  | 
  | 
テスト実装例で使用する設定ファイルで定義した設定ファイルを使用する。  | 
テスト対象のServiceの実装クラスをインジェクションしてインフラストラクチャ層と結合してテストを行う場合のテスト作成方法を説明する。
以下に、テスト時に読み込む設定ファイルを示す。
sample-domain.xml
<context:component-scan base-package="com.example.domain" />
<tx:annotation-driven />
<import resource="classpath:META-INF/spring/sample-infra.xml" />
<import resource="classpath:META-INF/spring/sample-codelist.xml" />
<bean id="resultMessagesLoggingInterceptor"
  class="org.terasoluna.gfw.common.exception.ResultMessagesLoggingInterceptor">
  <property name="exceptionLogger" ref="exceptionLogger" />
</bean>
<aop:config>
  <aop:advisor advice-ref="resultMessagesLoggingInterceptor"
    pointcut="@within(org.springframework.stereotype.Service)" />
</aop:config>
以下に、テスト実装例を示す。テスト対象のTicketReserveServiceImpl#registerReservation()メソッドを実行し、戻り値を確認している。
なお、データベースの状態の検証方法はRepositoryの単体テストを参照されたい。
TicketReserveServiceImplTest.java
import static org.hamcrest.CoreMatchers.is;
import static org.hamcrest.MatcherAssert.assertThat;
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations = {
        "classpath:META-INF/spring/sample-domain.xml", // (1)
        "classpath:META-INF/spring/test-context.xml"}) // (1)
@Transactional
public class TicketReserveServiceImplTest {
    @Inject
    TicketReserveService target;
    @Inject
    private JdbcTemplate jdbcTemplate;
    @Test
    @Sql(statements = "ALTER SEQUENCE sq_reservation_1 RESTART WITH 1") // (2)
    public void testRegisterReservation() {
        // setup
        Reservation inputReservation = new Reservation();
        inputReservation.setTotalFare(39200);
        inputReservation.setReserveNo("0000000001");
        // omitted
        // run the test
        TicketReserveDto actTicketReserveDto = target.registerReservation(
                reservation);
        // assertion
        assertThat(actTicketReserveDto.getReserveNo(), is("0000000001"));
        // omitted
    }
}
項番  | 
説明  | 
|---|---|
(1) 
 | 
TicketReserveServiceImplクラスを動作させるために必要な設定ファイル(アプリケーションが保持するsample-domain.xmlとそれを補うtest-context.xml)を読み込む。 | 
(2) 
 | 
@Sqlのstatements属性を使用することでSQL文を直接指定することもできる。ここではテストメソッド実行前にシーケンスの初期化を行っている。 | 
Warning
テスト時のトランザクション管理
テストケースに@Transactionalアノテーションを付与すると、テスト実行開始から終了まで一トランザクションとなる。そのため、テストケースから@Transactionalアノテーションを付与したServiceクラスを呼び出した場合、テストケースからトランザクションが引き継がれる点に注意すること。
例えば、トランザクションの伝播方法がデフォルト(REQUIRED)の場合、テストケースで開始したトランザクションでテスト対象の処理が行われ、コミット/ロールバックのタイミングもテスト終了時になる。
トランザクションの伝播方法については「宣言型トランザクション管理」で必要となる情報を参照されたい。
10.2.2.2.1.2. モックを利用したテスト¶
Serviceの依存クラスをすべてモック化して行うServiceの単体テストにおいて、作成するファイルを以下に示す。
作成するファイル名  | 
説明  | 
|---|---|
  | 
  | 
Serviceの実装クラスが依存するクラスをモック化する場合のテスト作成方法を説明する。ReservationRepository#insert()メソッドをモック化し、テスト対象のTicketReserveServiceImpl#registerReservation()メソッドでモック化したメソッドが呼び出されることとテスト対象の戻り値を確認している。TicketReserveServiceImplMockTest.java
import static org.hamcrest.CoreMatchers.is;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
public class TicketReserveServiceImplMockTest {
    @Rule // (1)
    public MockitoRule mockito = MockitoJUnit.rule();
    @Mock // (2)
    ReservationRepository reservationRepository;
    @InjectMocks // (3)
    private TicketReserveServiceImpl target;
    @Test
    public void testRegisterReservation() {
        // setup
        Reservation inputReservation = new Reservation();
        inputReservation.setTotalFare(39200);
        inputReservation.setReserveNo("0000000001");
        // omitted
        when(reservationRepository.insert(inputReservation)).thenReturn(1); // (4)
        // run the test
        TicketReserveDto ticketReserveDto = target.registerReservation(inputReservation);
        // assertion
        verify(reservationRepository).insert(inputReservation); // (5)
        assertThat(ticketReserveDto.getReserveNo(), is("0000000001"));
        // omitted
    }
}
項番  | 
説明  | 
|---|---|
(1) 
 | 
モックの初期化とインジェクションをアノテーションベースで行うための宣言。 
詳細はモックの生成を参照されたい。 
 | 
(2) 
 | 
@Mockアノテーションを付与することで、TicketReserveServiceImplが依存しているMemberRepositoryをモック化している。詳細はモックの生成を参照されたい。 
 | 
(3) 
 | 
@InjectMocksアノテーションを付与することで、自動的にモックオブジェクトが代入される。詳細はモックの生成を参照されたい。 
 | 
(4) 
 | 
ReservationRepositoryのinsertメソッドについて、引数がinputReservationの場合、返り値として”1“を返すように設定する。メソッドのモック化については、メソッドのモック化を参照されたい。 
 | 
(5) 
 | 
ReservationRepositoryのinsertメソッドについて、引数にinputReservationが渡されて1回呼び出されたことを検証する。モック化したメソッドの検証については、モック化したメソッドの検証を参照されたい。 
 | 
10.2.2.3. アプリケーション層の単体テスト¶
10.2.2.3.1. アプリケーション層の単体テスト対象¶
本節では、開発ガイドラインのアプリケーション層の単体テストについて説明する。
ControllerとHelperのロジックを確認するためのテストを行う。Controllerについては以下の項目を確認する。@RequestMapping(リクエストパス、HTTPメソッド、リクエストパラメータ)
返却されるVIEW名
Viewについては、本来アプリケーション層に含まれるが、本ガイドラインでは対象外とする。
Controllerクラスをテストするためのサポートクラス(org.springframework.test.web.servlet.MockMvcなど)を用意している。ControllerはMockMVCを使用して疑似リクエストを送信してテストをするため、MockMVCを提供するSpringJUnit4ClassRunnerを使用する。MockMvcはControllerに疑似リクエストを送信する仕組みを持ち、デプロイしたアプリケーションを模したテストを行うことができる。MockMVCの詳細はMockMvcとはを参照されたい。Note
Formのバリデーションテスト
Formのテストは、本来Controllerと組み合わせて実際の動作に近い形で行う必要があるが、Validationの全パターンをControllerと組み合わせるとテストの負担が大きくなる。そのため、単純なValidationの確認であれば、Controllerと切り離して Form単体でValidationの確認を行うこともできる。
テスト方法はテスト対象のFormを使用してBean Validationで実装したValidatorの単体テストを実施すればよい。
10.2.2.3.2. Controllerの単体テスト¶
ここでは、以下のControllerの単体テスト実装方法を説明する。
テスト方法  | 
説明  | 
|---|---|
Spring Testが提供するデフォルトのコンテキストを使用し指定した設定ファイルを読み込むことでテストを行う。  | 
|
実際に使用する  | 
|
  | 
ここでは、以下の成果物に対するテストを例に説明する。Controllerの実装の詳細は、Controllerの実装を参照されたい。
Controllerクラス(TicketSearchController)Controllerクラス(MemberRegisterController)
なお、インジェクションとモック化を組み合わせてテストを行いたい場合は、適宜以下に説明する実装方法を組み合わせて実装されたい。
10.2.2.3.2.1. StandaloneSetupを利用したテスト¶
Controllerの依存クラスが利用できモック化する必要がない場合のControllerの単体テストにおいて、StandaloneSetupで作成するファイルを以下に示す。
作成するファイル名  | 
説明  | 
|---|---|
  | 
  | 
  | 
アプリケーション層に依存するコンポーネントを読み込むための  | 
  | 
  | 
SpringMvcConfig.javaを使ってテストをすることが望ましいが、Spring Testが作成したコンテキストとSpring MVCが作成したコンテキストが衝突しテスト実行ができないことがある。そのため対応策として、テストに必要な設定のみ抽出し、テスト用の設定ファイルを用意する。
以下に、必要な設定のみ抽出した設定ファイルを示す。
SpringMvcTestConfig.java
@ComponentScan(basePackages = { "com.example.app"})
@Configuration
public class SpringMvcTestConfig{
}
ServiceImplクラスなどテスト対象のControllerクラスが依存するクラスをインジェクションする場合のテスト作成方法を説明する。なお、テストでデータアクセスする場合の検証方法はRepositoryの単体テストを、呼び出すドメイン層のロジックを確認する方法はServiceの単体テストを参照されたい。
以下に、テスト対象となるControllerの実装例を示す。
MemberRegisterController.java
@Controller
@RequestMapping("member/register")
@TransactionTokenCheck("member/register")
public class MemberRegisterController {
    @TransactionTokenCheck(type = TransactionTokenType.IN)
    @PostMapping
    public String register(@Validated MemberRegisterForm memberRegisterForm,
        BindingResult result, Model model, RedirectAttributes redirectAttributes) {
        if (result.hasErrors()) {
            throw new BadRequestException(result);
        }
        // omitted
        return "redirect:/member/register?complete";
    }
}
ここでは、テスト対象のMemberRegisterControllerクラスのregisterメソッドを呼び出し、リクエストマッピングと返却されるVIEWおよびリダイレクトされること(testRegisterConfirm01)、不正な入力値を送信したときにBadRequestExceptionがthrowされていること(testRegisterConfirm02)の確認を行う。
以下に、ServiceImplクラスなどテスト対象のControllerクラスが依存するクラスをインジェクションする場合のテスト作成方法を説明する。なお、テストでデータアクセスする場合の検証方法はRepositoryの単体テストを参照されたい。
MemberRegisterControllerStandaloneTest.java
import static org.hamcrest.CoreMatchers.is;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.log;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.model;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.view;
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = { ApplicationContextConfig.class, TestContextConfig.class, SpringMvcTestConfig.class }) // (1)
public class MemberRegisterControllerStandaloneTest {
    @Inject
    MemberRegisterController target;
    MockMvc mockMvc;
    @Before
    public void setUp() {
        // setup
        mockMvc = MockMvcBuilders.standaloneSetup(target).alwaysDo(log()).build(); // (2)
    }
    @Test
    public void testRegisterConfirm01() throws Exception {
        // setup and run the test
        mockMvc.perform(post("/member/register")
                    // omitted
                    .param("password", "testpassword")          // (3)
                    .param("reEnterPassword", "testpassword"))) // (3)
                    // assert
                    .andExpect(status().is(302))                                  // (4)
                    .andExpect(view().name("redirect:/member/register?complete")) // (4)
                    .andExpect(model().hasNoErrors());                            // (4)
    }
    @Test
    public void testRegisterConfirm02() throws Exception {
        try {
            // setup and run the test
            mockMvc.perform(post("/member/register")
                    // omitted
                    .param("password", "testpassword")
                    .param("reEnterPassword", "")) // (5)
                    // assert
                    .andExpect(status().is(400))
                    .andExpect(view().name("common/error/badRequest-error"))
                    .andReturn();
            fail("test failure!");
        } catch (Exception e) {
            // assert
            assertThat(e, is(instanceOf(ServletException.class)));         // (6)
            assertThat(e.getCause(), is(instanceOf(BadRequestException.class))); // (6)
        }
    }
}
項番  | 
説明  | 
|---|---|
(1) 
 | 
MemberRegisterControllerクラスが依存するService、Repositoryを動作させるために必要な設定ファイル(アプリケーションが保持するApplicationContextConfig.classとそれを補うTestContextConfig.class、 SpringMvcTestConfig.class)を読み込む。TestContextConfig.classは、テスト実装例で使用する設定ファイルを使用している。 | 
(2) 
 | 
|
(3) 
 | 
MemberRegisterControllerクラスのregisterConfirmメソッドを呼び出すため、/member/registerに対してPOSTメソッドでリクエストを送信する。リクエストパラメータにはFormの情報を設定する。リクエストデータの設定方法についてはリクエストデータの設定を、リクエスト送信の実装方法についてはリクエスト送信の実装を参照されたい。 | 
(4) 
 | 
performメソッドから返却されたResultActionsのandExpectメソッドで取得したMvcResultを使用して実行結果の妥当性を検証する。検証方法の詳細については実行結果検証の実装を参照されたい。 | 
(5) 
 | 
不正な入力値を送信する。 
 | 
(6) 
 | 
SystemExceptionResolverを有効にしていないため、例外ハンドリングされずにServletExceptionがサーブレットコンテナに通知される。ServletExceptionのgetCauseメソッドにより取得された例外から、Controllerで期待した例外がthrowされていることを検証する。 | 
作成するファイル名  | 
説明  | 
|---|---|
  | 
  | 
  | 
アプリケーション層に依存するコンポーネントを読み込むための  | 
  | 
  | 
spring-mvc.xmlを使ってテストをすることが望ましいが、Spring Testが作成したコンテキストとSpring MVCが作成したコンテキストが衝突しテスト実行ができないことがある。そのため対応策として、テストに必要な設定のみ抽出し、テスト用の設定ファイルを用意する。
以下に、必要な設定のみ抽出した設定ファイルを示す。
spring-mvc-test.xml
<context:component-scan base-package="com.example.app" />
ServiceImplクラスなどテスト対象のControllerクラスが依存するクラスをインジェクションする場合のテスト作成方法を説明する。なお、テストでデータアクセスする場合の検証方法はRepositoryの単体テストを、呼び出すドメイン層のロジックを確認する方法はServiceの単体テストを参照されたい。
以下に、テスト対象となるControllerの実装例を示す。
MemberRegisterController.java
@Controller
 @RequestMapping("member/register")
 @TransactionTokenCheck("member/register")
 public class MemberRegisterController {
    @TransactionTokenCheck(type = TransactionTokenType.IN)
    @PostMapping
    public String register(@Validated MemberRegisterForm memberRegisterForm,
        BindingResult result, Model model, RedirectAttributes redirectAttributes) {
        if (result.hasErrors()) {
            throw new BadRequestException(result);
        }
        // omitted
        return "redirect:/member/register?complete";
    }
}
ここでは、テスト対象のMemberRegisterControllerクラスのregisterメソッドを呼び出し、リクエストマッピングと返却されるVIEWおよびリダイレクトされること(testRegisterConfirm01)、不正な入力値を送信したときにBadRequestExceptionがthrowされていること(testRegisterConfirm02)の確認を行う。
以下に、ServiceImplクラスなどテスト対象のControllerクラスが依存するクラスをインジェクションする場合のテスト作成方法を説明する。なお、テストでデータアクセスする場合の検証方法はRepositoryの単体テストを参照されたい。
MemberRegisterControllerStandaloneTest.java
import static org.hamcrest.CoreMatchers.is;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.log;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.model;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.view;
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations = {
        "classpath:META-INF/spring/applicationContext.xml", // (1)
        "classpath:META-INF/spring/test-context.xml",       // (1)
        "classpath:META-INF/spring/spring-mvc-test.xml"})   // (1)
public class MemberRegisterControllerStandaloneTest {
    @Inject
    MemberRegisterController target;
    MockMvc mockMvc;
    @Before
    public void setUp() {
        // setup
        mockMvc = MockMvcBuilders.standaloneSetup(target).alwaysDo(log()).build(); // (2)
    }
    @Test
    public void testRegisterConfirm01() throws Exception {
        // setup and run the test
        mockMvc.perform(post("/member/register")
                    // omitted
                    .param("password", "testpassword")          // (3)
                    .param("reEnterPassword", "testpassword"))) // (3)
                    // assert
                    .andExpect(status().is(302))                                  // (4)
                    .andExpect(view().name("redirect:/member/register?complete")) // (4)
                    .andExpect(model().hasNoErrors());                            // (4)
    }
    @Test
    public void testRegisterConfirm02() throws Exception {
        try {
            // setup and run the test
            mockMvc.perform(post("/member/register")
                    // omitted
                    .param("password", "testpassword")
                    .param("reEnterPassword", "")) // (5)
                    // assert
                    .andExpect(status().is(400))
                    .andExpect(view().name("common/error/badRequest-error"))
                    .andReturn();
            fail("test failure!");
        } catch (Exception e) {
            // assert
            assertThat(e, is(instanceOf(ServletException.class)));         // (6)
            assertThat(e.getCause(), is(instanceOf(BadRequestException.class))); // (6)
        }
    }
}
項番  | 
説明  | 
|---|---|
(1) 
 | 
MemberRegisterControllerクラスが依存するService、Repositoryを動作させるために必要な設定ファイル(アプリケーションが保持するapplicationContext.xmlとそれを補うtest-context.xml、 spring-mvc-test.xml)を読み込む。test-context.xmlは、テスト実装例で使用する設定ファイルを使用している。 | 
(2) 
 | 
|
(3) 
 | 
MemberRegisterControllerクラスのregisterConfirmメソッドを呼び出すため、
/member/registerに対してPOSTメソッドでリクエストを送信する。リクエストパラメータにはFormの情報を設定する。リクエストデータの設定方法についてはリクエストデータの設定を、リクエスト送信の実装方法についてはリクエスト送信の実装を参照されたい。 | 
(4) 
 | 
performメソッドから返却されたResultActionsのandExpectメソッドで取得したMvcResultを使用して実行結果の妥当性を検証する。検証方法の詳細については実行結果検証の実装を参照されたい。 | 
(5) 
 | 
不正な入力値を送信する。 
 | 
(6) 
 | 
SystemExceptionResolverを有効にしていないため、例外ハンドリングされずにServletExceptionがサーブレットコンテナに通知される。ServletExceptionのgetCauseメソッドにより取得された例外から、Controllerで期待した例外がthrowされていることを検証する。 | 
10.2.2.3.2.2. WebAppContextSetupを利用したテスト¶
Controllerの依存クラスが利用できモック化する必要がない場合のControllerの単体テストにおいて、WebAppContextSetupで作成するファイルを以下に示す。
作成するファイル名  | 
説明  | 
|---|---|
  | 
  | 
Controllerが返すView名などは確認できるが、TransactionTokenInterceptorやSystemExceptionResolverといったSpringに追加して利用する機能は適用されていないため、トランザクショントークンチェックが正しく設定されているか、エラーページへの遷移が正しいかを判断することはできない。MockMvcをwebAppContextSetupでセットアップすることにより、Springに追加して利用するInterceptorやExceptionResolverなどをテスト時に自動で適用させることができる。@TransactionTokenCheckアノテーション、SystemExceptionResolverが有効になった場合のテストとを比べた時の相違点について説明する。以下に、テスト対象となるControllerの実装例を示す。
MemberRegisterController.java
@Controller
@RequestMapping("member/register")
@TransactionTokenCheck("member/register")
public class MemberRegisterController {
    @TransactionTokenCheck(type = TransactionTokenType.BEGIN) // (1)
    @PostMapping(params = "confirm")
    public String registerConfirm(@Validated MemberRegisterForm memberRegisterForm,
        BindingResult result, Model model) {
        // omitted
        return "C1/memberRegisterConfirm";
    }
    @TransactionTokenCheck(type = TransactionTokenType.IN) // (1)
    @RequestMapping(method = RequestMethod.POST)
    public String register(@Validated MemberRegisterForm memberRegisterForm,
        BindingResult result, Model model, RedirectAttributes redirectAttributes) {
        if (result.hasErrors()) {
            throw new BadRequestException(result); // (2)
        }
        // omitted
        return "redirect:/member/register?complete";
    }
}
項番  | 
説明  | 
|---|---|
(1) 
 | 
@TransactionTokenCheckアノテーションを設定することで不正なリクエストを無効にする。トランザクショントークンチェックについては、トランザクショントークンチェックについてを参照されたい。 
 | 
(2) 
 | 
リクエスト時に検証エラーがある場合は改ざんとみなしてエラーをthrowする。 
 | 
@TransactionTokenCheckを有効にした場合におけるテスト作成方法の相違点について説明する。MemberRegisterControllerWebAppContextTest.java
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.log;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.view;
@RunWith(SpringJUnit4ClassRunner.class)
@ContextHierarchy({ @ContextConfiguration(classes = { ApplicationContextConfig.class, SpringSecurityConfig.class }),
      @ContextConfiguration(classes = { SpringMvcConfig.class }) }) // (1)
@WebAppConfiguration                                                        // (1)
public class MemberRegisterControllerWebAppContextTest {
    @Inject
    WebApplicationContext webApplicationContext; // (2)
    MockMvc mockMvc;
    @Before
    public void setUp() {
        // setup
        mockMvc = MockMvcBuilders.webAppContextSetup(webApplicationContext) // (2)
                .alwaysDo(log()).build();
    }
    @Test
    public void testRegisterConfirm01() throws Exception {
        // setup and run the test
        MvcResult mvcResult = mockMvc.perform(post("/member/register") // (3)
                    .param("confirm", "")                              // (3)
                    // omitted
                    .param("password", "testpassword")                 // (3)
                    .param("reEnterPassword", "testpassword"))         // (3)
                    // assert
                    .andExpect(status().isOk())
                    .andExpect(view().name("C1/memberRegisterConfirm"))
                    .andReturn();
        TransactionToken actTransactionToken = (TransactionToken) mvcResult.getRequest()
                .getAttribute(TransactionTokenInterceptor.NEXT_TOKEN_REQUEST_ATTRIBUTE_NAME); // (4)
        MockHttpSession mockSession = (MockHttpSession) mvcResult.getRequest().getSession();  // (5)
        // setup and run the test
        mockMvc.perform(post("/member/register")              // (6)
                    // omitted
                    .param("password", "testpassword")        // (6)
                    .param("reEnterPassword", "testpassword") // (6)
                    .param(TransactionTokenInterceptor.TOKEN_REQUEST_PARAMETER,
                            actTransactionToken.getTokenString()) // (6)
                    .session(mockSession)) // (6)
                    // assert
                    .andExpect(status().isFound())                                   // (7)
                    .andExpect(view().name("redirect:/member/register?complete")); // (7)
    }
}
項番  | 
説明  | 
|---|---|
(1) 
 | 
業務でカスタムした 
InterceptorやExceptionResolverなどを動作させるためにSpringMvcConfig.classを読み込む。 | 
(2) 
 | 
読み込んだBean定義から生成したWebアプリケーションコンテキストを使用して、 
MockMvcをセットアップする。 | 
(3) 
 | 
トランザクショントークンを生成するために、 
@TransactionTokenCheck(type = TransactionTokenType.BEGIN)が設定されたメソッドに対してリクエストを送信する。 | 
(4) 
 | 
BEGINしたリクエスト( 
registerConfirmメソッド)からINのリクエスト(registerメソッド)にトランザクショントークンを引き継ぐため、リクエスト属性からトランザクショントークンを取得する。 | 
(5) 
 | 
サーバ側は発行したトランザクショントークンをセッションに保持するため、次のリクエストでも同じセッションを参照する必要があるが、 
MockMvcでは1リクエストごとに新規セッションが使われてしまうため、明示的に同じセッションを使用するよう指定する。 | 
(6) 
 | 
再度、リクエストパス( 
/member/register)に対してPOSTメソッドでリクエストを送信する。リクエストパラメータにはFormの情報、(4)で取得したトランザクショントークンを設定し、セッションには(5)で取得したセッションを設定する。 | 
(7) 
 | 
トランザクショントークンチェックの設定が正しいことを確認するために、トークンチェックエラーになっていないことを検証する。 
 | 
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.log;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.view;
@RunWith(SpringJUnit4ClassRunner.class)
@ContextHierarchy({@ContextConfiguration(                                   // (1)
        "classpath:META-INF/spring/applicationContext.xml"),                // (1)
        @ContextConfiguration("classpath:META-INF/spring/spring-mvc.xml")}) // (1)
@WebAppConfiguration                                                        // (1)
public class MemberRegisterControllerWebAppContextTest {
    @Inject
    WebApplicationContext webApplicationContext; // (2)
    MockMvc mockMvc;
    @Before
    public void setUp() {
        // setup
        mockMvc = MockMvcBuilders.webAppContextSetup(webApplicationContext) // (2)
                .alwaysDo(log()).build();
    }
    @Test
    public void testRegisterConfirm01() throws Exception {
        // setup and run the test
        MvcResult mvcResult = mockMvc.perform(post("/member/register") // (3)
                    .param("confirm", "")                              // (3)
                    // omitted
                    .param("password", "testpassword")                 // (3)
                    .param("reEnterPassword", "testpassword"))         // (3)
                    // assert
                    .andExpect(status().isOk())
                    .andExpect(view().name("C1/memberRegisterConfirm"))
                    .andReturn();
        TransactionToken actTransactionToken = (TransactionToken) mvcResult.getRequest()
                .getAttribute(TransactionTokenInterceptor.NEXT_TOKEN_REQUEST_ATTRIBUTE_NAME); // (4)
        MockHttpSession mockSession = (MockHttpSession) mvcResult.getRequest().getSession();  // (5)
        // setup and run the test
        mockMvc.perform(post("/member/register")              // (6)
                    // omitted
                    .param("password", "testpassword")        // (6)
                    .param("reEnterPassword", "testpassword") // (6)
                    .param(TransactionTokenInterceptor.TOKEN_REQUEST_PARAMETER,
                            actTransactionToken.getTokenString()) // (6)
                    .session(mockSession)) // (6)
                    // assert
                    .andExpect(status().isFound())                                   // (7)
                    .andExpect(view().name("redirect:/member/register?complete")); // (7)
    }
}
項番  | 
説明  | 
|---|---|
(1) 
 | 
業務でカスタムした 
InterceptorやExceptionResolverなどを動作させるためにspring-mvc.xmlを読み込む。 | 
(2) 
 | 
読み込んだBean定義から生成したWebアプリケーションコンテキストを使用して、 
MockMvcをセットアップする。 | 
(3) 
 | 
トランザクショントークンを生成するために、 
@TransactionTokenCheck(type = TransactionTokenType.BEGIN)が設定されたメソッドに対してリクエストを送信する。 | 
(4) 
 | 
BEGINしたリクエスト( 
registerConfirmメソッド)からINのリクエスト(registerメソッド)にトランザクショントークンを引き継ぐため、リクエスト属性からトランザクショントークンを取得する。 | 
(5) 
 | 
サーバ側は発行したトランザクショントークンをセッションに保持するため、次のリクエストでも同じセッションを参照する必要があるが、 
MockMvcでは1リクエストごとに新規セッションが使われてしまうため、明示的に同じセッションを使用するよう指定する。 | 
(6) 
 | 
再度、リクエストパス( 
/member/register)に対してPOSTメソッドでリクエストを送信する。リクエストパラメータにはFormの情報、(4)で取得したトランザクショントークンを設定し、セッションには(5)で取得したセッションを設定する。 | 
(7) 
 | 
トランザクショントークンチェックの設定が正しいことを確認するために、トークンチェックエラーになっていないことを検証する。 
 | 
次に、SystemExceptionResolverを有効にした場合におけるテスト作成方法の相違点を説明する。
以下に、SystemExceptionResolverの定義例を示す。
SpringMvcConfig.java
@Bean
public SystemExceptionResolver systemExceptionResolver(ExceptionCodeResolver exceptionCodeResolver) {
    SystemExceptionResolver bean = new SystemExceptionResolver();
    bean.setOrder(3);
    Properties exceptionMappings = new Properties();
    exceptionMappings.setProperty("InvalidTransactionTokenException", "common/error/token-error");
    exceptionMappings.setProperty("BadRequestException", "common/error/badRequest-error");
    exceptionMappings.setProperty("Exception", "common/error/system-error");
    bean.setExceptionMappings(exceptionMappings);
    Properties statusCodes = new Properties();
    statusCodes.setProperty("common/error/token-error", String.valueOf(HttpStatus.CONFLICT.value()));
    statusCodes.setProperty("common/error/badRequest-error", String.valueOf(HttpStatus.BAD_REQUEST.value()));
    bean.setStatusCodes(statusCodes);
    bean.setDefaultStatusCode(HttpStatus.INTERNAL_SERVER_ERROR.value());
    bean.setExceptionCodeResolver(exceptionCodeResolver);
    bean.setPreventResponseCaching(true);
    return bean;
}
以下に、テスト作成方法の相違点について説明する。
MemberRegisterControllerWebAppContextTest.java
import static org.hamcrest.CoreMatchers.is;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.log;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.view;
@RunWith(SpringJUnit4ClassRunner.class)
@ContextHierarchy({ @ContextConfiguration(classes = { ApplicationContextConfig.class, SpringSecurityConfig.class }),
      @ContextConfiguration(classes = { SpringMvcConfig.class }) })
@WebAppConfiguration
public class MemberRegisterControllerWebAppContextTest {
    @Inject
    WebApplicationContext webApplicationContext;
    MockMvc mockMvc;
    @Before
    public void setUp() {
        // setup
        mockMvc = MockMvcBuilders.webAppContextSetup(webApplicationContext)
                .alwaysDo(log()).build();
    }
    @Test
    public void testRegisterConfirm02() throws Exception {
        // omitted
        // setup and run the test
        mvcResult = mockMvc.perform(post("/member/register")
                    .param("password", "testpassword")
                    .param("reEnterPassword", "") // (1)
                    .param(TransactionTokenInterceptor.TOKEN_REQUEST_PARAMETER,
                            actTransactionToken.getTokenString()) // (2)
                    .session(mockSession)) // (2)
                    // assert
                    .andExpect(status().isBadRequest())                      // (3)
                    .andExpect(view().name("common/error/badRequest-error")) // (3)
                    .andReturn();
        // assert
        Exception exception = mvcResult.getResolvedException();                   // (4)
        assertThat(exception, is(instanceOf(BadRequestException.class)));         // (4)
        assertThat(exception.getMessage(), is(
            "[e.ar.a0.L9001] 不正リクエスト(パラメータ改竄) org.springframework.validation.BeanPropertyBindingResult: 1 errors\n"
                    + "Error in object 'memberRegisterForm': codes [e.ar.c1.5001.memberRegisterForm,e.ar.c1.5001]; arguments []; default message [null]")); // (4)
    }
}
項番  | 
説明  | 
|---|---|
(1) 
 | 
Formの情報を不正な値にすることで、registerメソッドの内でエラーをthrowさせている。 | 
(2) 
 | 
前述と同様に、生成したトランザクショントークン情報を設定する。 
 | 
(3) 
 | 
ここでは 
SystemExceptionResolverが有効になっているため、定義したエラーのステータスコード、エラーページの遷移先が正しく設定されていることを検証する。 | 
(4) 
 | 
SystemExceptionResolverで例外ハンドリングされたエラーから、期待したエラーがthrowされていることを検証する。 | 
spring-mvc.xml
<bean class="org.terasoluna.gfw.web.exception.SystemExceptionResolver">
  <property name="order" value="3" />
  <property name="exceptionMappings">
    <map>
      <entry key="InvalidTransactionTokenException" value="common/error/token-error" />
      <entry key="BadRequestException" value="common/error/badRequest-error" />
      <entry key="Exception" value="common/error/system-error" />
    </map>
  </property>
  <property name="statusCodes">
    <map>
      <entry key="common/error/token-error" value="409" />
      <entry key="common/error/badRequest-error" value="400" />
    </map>
  </property>
  <property name="excludedExceptions">
    <array>
    </array>
  </property>
  <property name="defaultStatusCode" value="500" />
  <property name="exceptionCodeResolver" ref="exceptionCodeResolver" />
  <property name="preventResponseCaching" value="true" />
</bean>
以下に、テスト作成方法の相違点について説明する。
MemberRegisterControllerWebAppContextTest.java
import static org.hamcrest.CoreMatchers.is;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.log;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.view;
@RunWith(SpringJUnit4ClassRunner.class)
@ContextHierarchy({@ContextConfiguration(
        "classpath:META-INF/spring/applicationContext.xml"),
        @ContextConfiguration("classpath:META-INF/spring/spring-mvc.xml")})
@WebAppConfiguration
public class MemberRegisterControllerWebAppContextTest {
    @Inject
    WebApplicationContext webApplicationContext;
    MockMvc mockMvc;
    @Before
    public void setUp() {
        // setup
        mockMvc = MockMvcBuilders.webAppContextSetup(webApplicationContext)
                .alwaysDo(log()).build();
    }
    @Test
    public void testRegisterConfirm02() throws Exception {
        // omitted
        // setup and run the test
        mvcResult = mockMvc.perform(post("/member/register")
                    .param("password", "testpassword")
                    .param("reEnterPassword", "") // (1)
                    .param(TransactionTokenInterceptor.TOKEN_REQUEST_PARAMETER,
                            actTransactionToken.getTokenString()) // (2)
                    .session(mockSession)) // (2)
                    // assert
                    .andExpect(status().isBadRequest())                      // (3)
                    .andExpect(view().name("common/error/badRequest-error")) // (3)
                    .andReturn();
        // assert
        Exception exception = mvcResult.getResolvedException();                   // (4)
        assertThat(exception, is(instanceOf(BadRequestException.class)));         // (4)
        assertThat(exception.getMessage(), is(
            "[e.ar.a0.L9001] 不正リクエスト(パラメータ改竄) org.springframework.validation.BeanPropertyBindingResult: 1 errors\n"
                    + "Error in object 'memberRegisterForm': codes [e.ar.c1.5001.memberRegisterForm,e.ar.c1.5001]; arguments []; default message [null]")); // (4)
    }
}
項番  | 
説明  | 
|---|---|
(1) 
 | 
Formの情報を不正な値にすることで、registerメソッドの内でエラーをthrowさせている。 | 
(2) 
 | 
前述と同様に、生成したトランザクショントークン情報を設定する。 
 | 
(3) 
 | 
ここでは 
SystemExceptionResolverが有効になっているため、定義したエラーのステータスコード、エラーページの遷移先が正しく設定されていることを検証する。 | 
(4) 
 | 
SystemExceptionResolverで例外ハンドリングされたエラーから、期待したエラーがthrowされていることを検証する。 | 
Note
Sessionを利用する場合
ControllerクラスがSessionを利用している場合はorg.springframework.mock.web.MockHttpSessionを使ってテストを行う。
MockHttpSessionを利用したテストメソッドの例public class SessionControllerTest { // (1) MockHttpSession mockSession = new MockHttpSession(); // omitted @Test public void testSession() throws Exception { String formName = "todoForm"; TodoForm form = new TodoForm(); String todoId = "1111"; String todoTitle = "test"; form.setTodoId(todoId); form.setTodoTitle(todoTitle); // (2) mockSession.setAttribute(formName, form); // (3) ResultActions results = mockMvc.perform(post("/todo/operation") .param("create", "create") .param("todoId", todoId) .param("todoTitle", todoTitle) .session(mockSession)); // (4) results.andExpect(request().sessionAttribute(formName, isA(TodoForm.class))); // omitted // (5) results = mockMvc.perform(get("/todo/create").param("redo", "redo")); results.andExpect(request().sessionAttribute(formName, isA(TodoForm.class))); // omitted } }
項番
説明
(1)セッションのモックオブジェクトを生成する。クラスの詳細については、MockHttpSession のJavadocを参照されたい。(2)生成したセッションのモックオブジェクトに、格納したいオブジェクトをセットする。(3)MockMvcRequestBuildersのpostメソッドでリクエストのモックを生成し、生成したリクエストにsessionメソッドでセッションのモックを登録する。(4)(2)でセットしたオブジェクトが、セッションスコープに格納されていることを確認する。(5)再度リクエストを発行し、セッションスコープに格納したオブジェクトが保持されているか確認する。
10.2.2.3.2.3. モックを利用したテスト¶
Controllerの依存クラスをモック化する必要がある場合のControllerの単体テストにおいて、作成するファイルを以下に示す。
作成するファイル名  | 
説明  | 
|---|---|
  | 
  | 
テスト対象のControllerクラスが依存するクラスを、モック化する場合のテスト作成方法を説明する。
以下に、テスト対象となるControllerの実装例を示す。
TicketSearchController.java
@Controller
@RequestMapping("ticket/search")
public class TicketSearchController {
    @Inject
    TicketSearchHelper ticketSearchHelper;
    @GetMapping(params = "form")
    public String searchForm(Model model) {
        model.addAttribute(ticketSearchHelper.createDefaultTicketSearchForm());
        model.addAttribute(ticketSearchHelper.createFlightSearchOutputDto());
        model.addAttribute("isInitialSearchUnnecessary", true);
        return "B1/flightSearch";
    }
}
以下に、Controllerのテスト実装例を示す。
TicketSearchControllerMockTest.java
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.log;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.view;
public class TicketSearchControllerMockTest {
    @Rule // (1)
    public MockitoRule mockito = MockitoJUnit.rule();
    @InjectMocks // (2)
    TicketSearchController target;
    @Mock // (3)
    TicketSearchHelper ticketSearchHelper;
    MockMvc mockMvc;
    @Before
    public void setUp() {
        // setup
        TicketSearchForm ticketSearchForm = new TicketSearchForm();
        ticketSearchForm.setFlightType(FlightType.RT);
        ticketSearchForm.setDepAirportCd("HND");
        // omitted
        when(ticketSearchHelper.createDefaultTicketSearchForm()).thenReturn(ticketSearchForm); // (4)
        mockMvc = MockMvcBuilders.standaloneSetup(target).alwaysDo(log()).build();
    }
    @Test
    public void testSearchForm() throws Exception {
        // setup and run the test
        MvcResult mvcResult = mockMvc.perform(get("/ticket/search").param("form", ""))
                    // assert
                    .andExpect(status().isOk())
                    .andExpect(view().name("B1/flightSearch"))
                    .andReturn();
        // assert
        verify(ticketSearchHelper).createDefaultTicketSearchForm(); // (5)
        // omitted
    }
}
項番  | 
説明  | 
|---|---|
(1) 
 | 
モックの初期化とインジェクションをアノテーションベースで行うための宣言。 
詳細はモックの生成を参照されたい。 
 | 
(2) 
 | 
@InjectMocksアノテーションを付与することで、自動的にモックオブジェクトが代入される。詳細はモックの生成を参照されたい。 
 | 
(3) 
 | 
@Mockアノテーションを付与することで、TicketSearchControllerが依存しているTicketSearchHelperをモック化している。詳細はモックの生成を参照されたい。 
 | 
(4) 
 | 
すべてのテストメソッドにおいて、 
ticketSearchHelperのcreateDefaultTicketSearchFormメソッドの返り値としてcreateMockFormメソッドの返り値を設定する。メソッドのモック化については、メソッドのモック化を参照されたい。 | 
(5) 
 | 
ticketSearchHelperのcreateDefaultTicketSearchFormメソッドについて1回呼び出されたことを検証する。モック化したメソッドの検証については、モック化したメソッドの検証を参照されたい。 
 | 
10.2.2.3.3. Helperの単体テスト¶
Helperの単体テストは、Serviceと同様の実装でテストすることができる。