10.2.2. レイヤごとのテスト実装¶
目次
レイヤごとの単体テスト対象クラス、テスト方法およびその概要の一覧を以下に示す。
なお、本章で提示するテスト方法および実装はあくまで一例であり、実際はテスト方針に合わせたテスト方法および実装を検討いただきたい。
レイヤ | テスト方法 | 概要 |
---|---|---|
インフラストラクチャ層 | Spring Test標準機能のみを利用したテスト | Spring Testの標準的な機能を使用してデータアクセスのテストを行う。 |
インフラストラクチャ層 | Spring Test DBUnitを利用したテスト | DBUnitとSpring Test DBUnitの機能を使用してデータアクセスのテストを行う。 |
ドメイン層 | 依存クラスを利用したテスト | Spring TestのDI機能を使用してService をインジェクションし、インフラストラクチャ層と結合しService のテストを行う。 |
ドメイン層 | モックを利用したテスト | Mockitoを使用して依存するクラスをモック化しService のテストを行う。 |
アプリケーション層 | StandaloneSetupを利用したテスト | Spring TestのDI機能を使用してController をインジェクションし、ドメイン層、インフラストラクチャ層と結合した状態でController のテストを行う。 |
アプリケーション層 | WebAppContextSetupを利用したテスト | Spring TestのMockMvcを使用して業務で作成したspring-mvc.xml とapplicationContext.xml を適用し、Controller のテストを行う。 |
アプリケーション層 | モックを利用したテスト | Mockitoを使用して依存するクラスをモック化しController のテストを行う。 |
アプリケーション層 | Helperの単体テスト | Helper のテストを行う。テスト方法の詳細はService のテスト方法を参照。 |
10.2.2.1. インフラストラクチャ層の単体テスト¶
本節では、開発ガイドラインのインフラストラクチャ層の単体テストについて説明する。
インフラストラクチャ層では、RepositoryからMyBatis(O/R Mapper)を利用したデータアクセスのテストを行う。 MyBatis3の使用方法の詳細については、MyBatis3を使ってRepositoryを実装を参照されたい。
MyBatisにより自動生成されるRepositoryImpl
はSpringのDIコンテナ上で実行されるため、テストには、
本番同様のBean定義と、SpringのDI機能を提供するSpring TestのSpringJUnit4ClassRunner
を使用する。
Spring Testの詳細はSpring Testを参照されたい。
テスト実行後のデータ検証方法には以下の2通りある。 どちらを使用するかは別途業務要件に合わせて検討いただきたい。
- テスト実行後のデータベースの状態をSELECT文を使用して取得し検証する。
- DBUnitとSpring Test DBUnitを使用して検証する。
本節では、SELECT文を使用した検証方法としてJdbcTemplate
を使用した場合を例に説明する。
JdbcTemplate
とはSpring JDBCサポートのコアクラスである。JDBC APIではデータソースからコネクションの取得、
PreparedStatement
の作成、ResultSet
の解析、コネクションの解放などを行う必要があるが、
JdbcTemplate
を使用することでこれらの処理の多くが隠蔽され、より簡単にデータアクセスを行うことができる。
Note
アプリケーションのレイヤ化では、Repository
インターフェイスはドメイン層の成果物であるが、
インフラストラクチャ層の単体テスト対象として紹介している。Service
とのインターフェイスが正しいことは、
ドメイン層の単体テストでも確認することを推奨する。
10.2.2.1.1. Repositoryの単体テスト¶
本節では、以下のRepository
の単体テスト実装方法を説明する。
テスト方法 | 説明 |
---|---|
Spring Test標準機能のみを利用したテスト | JdbcTemplate を使用してテスト結果の検証を行う。 |
Spring Test DBUnitを利用したテスト | DBUnit、Spring Test DBUnitの機能を使用してテスト結果の検証を行う。 |
ここでは、以下の成果物に対するテストを例に説明する。 なお、Repositoryの実装の詳細は、MyBatis3を使ってRepositoryを実装を参照されたい。
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標準機能のみを利用したテスト¶
Spring Testを使用したRepository
の単体テストにおいて、作成するファイルを以下に示す。
なお、データベースのセットアップ方法についてはスキーマとテストデータのセットアップ(Spring Test標準機能のみを利用したテストの場合) を参照されたい。
また、Spring Testを使用して単体テストを行う際に使用する設定ファイルはテスト実装例で使用する設定ファイルを参照されたい。
作成するファイル名 | 説明 |
---|---|
MemberRepositoryTest.java |
MemberRepository.java のテストクラス。 |
test-context.xml |
Spring Testを使用して単体テストを行う際に必要な設定を補うための設定ファイル。 |
setupMemberLogin.sql |
単体テストで利用するデータベースのデータをセットアップするための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="maxWaitMillis" value="60000" />
</bean>
<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="dataSource" />
</bean>
<bean id="dateFactory" class="org.terasoluna.gfw.common.date.jodatime.DefaultJodaTimeDateFactory" />
以下に、Spring Testを使用したRepository
のテスト作成方法について説明する。
ここでは、テスト用のスキーマは作成済みであることを前提に、@Sql
アノテーションを使用してMemberLogin
テーブル
をセットアップし、MemberLogin
のパスワード「ABCDE」が新しいパスワード「FGHIJ」に更新されることを更新後の
MemberLogin
テーブルを取得して確認している。
MemberRepositoryTest.java
import static org.hamcrest.CoreMatchers.*;
import static org.junit.Assert.*;
@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 = (MemberLogin) jdbctemplate.queryForObject(
"SELECT * FROM member_login WHERE customer_no=?",
new Object[] {customerNo },
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;
}
});
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を利用したテスト¶
データアクセスにDBUnitを使用する場合のRepository
の単体テスト実装方法について説明する。
なお、ここではDBUnitのデータ定義ファイルにExcel形式(.xlsx)のファイルを使用した場合を例に説明する。
データ定義ファイルとデータベースのセットアップ方法については、テストデータのセットアップ(Spring Test DBUnitを利用したテスト場合)を参照されたい。
また、DBUnitにSpring Test DBUnitの機能を組み合わせて使用するには、@TestExecutionListeners
アノテーションを使って、
com.github.springtestdbunit.TransactionDbUnitTestExecutionListener
を登録する必要がある。
登録方法ついては、TestExecutionListenerの登録を参照されたい。
Warning
データ定義ファイルにExcel形式のファイルを使用する場合のApache POIについて
本フレームワークで利用しているDBUnitはApache POI 3.17に依存しており、4.xではDBUnitが利用するいくつかのメソッドが廃止されているため、Excel形式のデータ定義ファイルを読み込む際に実行時エラーとなることが確認されている。
共通ライブラリの提供するApache POIは4.xであるため、Excel形式のファイルを使用する場合は、3.17にダウングレードする必要がある。 なお、テスト対象のアプリケーションがExcelファイルのダウンロードに示すようにApache POIを利用している場合は、ダウングレードにより非互換が発生する可能性があることに留意されたい。
また、Apache POI 3.17では、CVE-2019-12415で報告されているXXEの脆弱性を含むため、使用はテスト用途のみに留め、本番では使用しないことを推奨する。
DBUnit 2.7.1においてApache POI 4.xに対応される予定であるが、Spring Test DBUnitが個人開発のライブラリで既に開発を停止していると見られることから、 正式に対応される見通しがついていないのが現状である。Spring Testの標準機能を利用することも併せて検討されたい。
DBUnitを利用したRepository
の単体テストにおいて、作成するファイルを以下に示す。
作成するファイル名 | 説明 |
---|---|
MemberRepositoryDbunitTest.java |
MemberRepository.java のテストクラス(DBUnitと連携する場合) |
XlsDataSetLoader.java |
Excel形式に対応するDataSetLoader インタフェースの実装クラス。
実装方法については、テストデータのセットアップ(Spring Test DBUnitを利用したテスト場合)を参照されたい。 |
expected_testUpdateMemberLogin.xlsx |
テストの期待結果検証用ファイル |
setup_MemberLogin.xlsx |
テストデータセットアップ用ファイル |
test-context.xml |
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.*;
import static org.junit.Assert.*;
@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
を使用してテストを行う。
Spring Testの詳細はSpring Testを参照されたい。
10.2.2.2.1. Serviceの単体テスト¶
本節では、以下のService
のテスト実装方法を説明する。
テスト方法 | 説明 |
---|---|
依存クラスを利用したテスト | Service をインジェクションし、インフラストラクチャ層と結合してテストを行う。 |
モックを利用したテスト | Service の実装クラスが依存するクラスをすべてモック化してテストを行う。 |
ここでは、以下の成果物に対するテストを例に説明する。 なお、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
のテストにおいて、
作成するファイルを以下に示す。
作成するファイル名 | 説明 |
---|---|
TicketReserveServiceImplTest.java |
TicketReserveServiceImpl.java のテストクラス |
test-context.xml |
テスト実装例で使用する設定ファイルで定義した設定ファイルを使用する。 |
テスト対象の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.*;
import static org.junit.Assert.*;
@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-domain.xml )を読み込む。 |
(2)
|
@Sql のstatements 属性を使用することでSQL文を直接指定することもできる。
ここではテストメソッド実行前にシーケンスの初期化を行っている。 |
Warning
テスト時のトランザクション管理
テストケースに@Transactional
アノテーションを付与すると、テスト実行開始から終了まで一トランザクションとなる。
そのため、テストケースから@Transactional
アノテーションを付与したService
クラスを呼び出した場合、
テストケースからトランザクションが引き継がれる点に注意すること。
例えば、トランザクションの伝播方法がデフォルト(REQUIRED
)の場合、テストケースで開始した
トランザクションでテスト対象の処理が行われ、コミット/ロールバックのタイミングもテスト終了時になる。
トランザクションの伝播方法については「宣言型トランザクション管理」で必要となる情報を参照されたい。
10.2.2.2.1.2. モックを利用したテスト¶
Service
の依存クラスをすべてモック化して行うService
の単体テストにおいて、作成するファイルを以下に示す。
作成するファイル名 | 説明 |
---|---|
TicketReserveServiceImplMockTest.java |
TicketReserveServiceImpl.java のテストクラス(モックを使用する場合) |
テスト対象のService
の実装クラスが依存するクラスをモック化する場合のテスト作成方法を説明する。
ここでは、ReservationRepository#insert()
メソッドをモック化し、テスト対象の
TicketReserveServiceImpl#registerReservation()
メソッドでモック化したメソッドが呼び出されることとテスト対象の
戻り値を確認している。
TicketReserveServiceImplMockTest.java
import static org.hamcrest.CoreMatchers.*;
import static org.junit.Assert.*;
import static org.mockito.Mockito.*;
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)
|
|
(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
については、本来アプリケーション層に含まれるが、本ガイドラインでは対象外とする。
Spring TestはController
クラスをテストするためのサポートクラス(org.springframework.test.web.servlet.MockMvc
など)を用意している。
Controller
はMockMVC
を使用して疑似リクエストを送信してテストをするため、MockMVC
を提供する
Spring Testの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
の単体テスト実装方法を説明する。
テスト方法 | 説明 |
---|---|
StandaloneSetupを利用したテスト | Spring Testが提供するデフォルトのコンテキストを使用し指定した設定ファイルを読み込むことでテストを行う。 |
WebAppContextSetupを利用したテスト | 実際に使用するapplicationContext.xml とspring-mvc.xml を使用してテストを行う。 |
モックを利用したテスト | Controller が依存するクラスをすべてモック化してテストを行う。 |
ここでは、以下の成果物に対するテストを例に説明する。Controller
の実装の詳細は、Controllerの実装を参照されたい。
Controller
クラス(TicketSearchController)Controller
クラス(MemberRegisterController)
なお、インジェクションとモック化を組み合わせてテストを行いたい場合は、適宜以下に説明する実装方法を組み合わせて 実装されたい。
10.2.2.3.2.1. StandaloneSetupを利用したテスト¶
Controller
の依存クラスが利用できモック化する必要がない場合のController
の単体テストにおいて、StandaloneSetup
で作成するファイルを以下に示す。
作成するファイル名 | 説明 |
---|---|
MemberRegisterControllerStandaloneTest.java |
MemberRegisterController.java のテストクラス |
spring-mvc-test.xml |
アプリケーション層に依存するコンポーネントを読み込むためのcomponent-scan をテスト用に抽出した設定ファイル。 |
test-context.xml |
Controller をドメイン層、インフラストラクチャ層と結合してテストを行う場合に使用する設定ファイル。 |
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)
@RequestMapping(method = RequestMethod.POST)
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.*;
import static org.junit.Assert.*;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*;
import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.*;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;
@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(NestedServletException.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 を有効にしていないため、例外ハンドリングされずにNestedServletException がサーブレットコンテナに通知される。
NestedServletException のgetCause メソッドにより取得された例外から、Controller で期待した例外がthrowされていることを検証する。 |
10.2.2.3.2.2. WebAppContextSetupを利用したテスト¶
Controller
の依存クラスが利用できモック化する必要がない場合のController
の単体テストにおいて、WebAppContextSetup
で作成するファイルを以下に示す。
作成するファイル名 | 説明 |
---|---|
MemberRegisterControllerWebAppContextTest.java |
MemberRegisterController.java のテストクラス |
StandaloneSetupを利用したテストの例では、パスへのリクエストやController
が返すView
名などは確認できるが、
TransactionTokenInterceptor
やSystemExceptionResolver
といったSpringに追加して利用する機能は適用されていないため、
トランザクショントークンチェックが正しく設定されているか、エラーページへの遷移が正しいかを判断することはできない。
そのような場合は、MockMvc
をwebAppContextSetup
でセットアップすることにより、
Springに追加して利用するInterceptor
やExceptionResolver
などをテスト時に自動で適用させることができる。
ここでは、StandaloneSetupを利用したテストで説明したテストと、
@TransactionTokenCheck
アノテーション、SystemExceptionResolver
が有効になった場合のテストとを比べた時の相違点について説明する。
以下に、テスト対象となるController
の実装例を示す。
MemberRegisterController.java
@Controller
@RequestMapping("member/register")
@TransactionTokenCheck("member/register")
public class MemberRegisterController {
@TransactionTokenCheck(type = TransactionTokenType.BEGIN) // (1)
@RequestMapping(method = RequestMethod.POST, 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
を有効にした場合におけるテスト作成方法の相違点について説明する。
なお、テストでデータアクセスする場合の検証方法はRepositoryの単体テストを参照されたい。
MemberRegisterControllerWebAppContextTest.java
import static org.hamcrest.CoreMatchers.*;
import static org.junit.Assert.*;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*;
import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.*;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;
@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().is(200))
.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().is(302)) // (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
の定義例を示す。
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>
<value>org.springframework.web.util.NestedServletException</value>
</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.*;
import static org.junit.Assert.*;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*;
import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.*;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;
@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().is(400)) // (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("不正リクエスト(パラメータ改竄)")); // (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
の単体テストにおいて、
作成するファイルを以下に示す。
作成するファイル名 | 説明 |
---|---|
TicketSearchControllerMockTest.java |
TicketSearchController.java のテストクラス |
テスト対象のController
クラスが依存するクラスを、モック化する場合のテスト作成方法を説明する。
以下に、テスト対象となるController
の実装例を示す。
TicketSearchController.java
@Controller
@RequestMapping("ticket/search")
public class TicketSearchController {
@Inject
TicketSearchHelper ticketSearchHelper;
@RequestMapping(method = RequestMethod.GET, 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.hamcrest.CoreMatchers.*;
import static org.junit.Assert.*;
import static org.mockito.Mockito.*;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*;
import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.*;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;
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().is(200))
.andExpect(view().name("B1/flightSearch"))
.andReturn();
// assert
verify(ticketSearchHelper).createDefaultTicketSearchForm(); // (5)
// omitted
}
}
項番 | 説明 |
---|---|
(1)
|
モックの初期化とインジェクションをアノテーションベースで行うための宣言。
詳細はモックの生成を参照されたい。
|
(2)
|
|
(3)
|
@InjectMocks アノテーションを付与することで、自動的にモックオブジェクトが代入される。
詳細はモックの生成を参照されたい。 |
(4)
|
すべてのテストメソッドにおいて、
ticketSearchHelper のcreateDefaultTicketSearchForm メソッドの返り値として
createMockForm メソッドの返り値を設定する。メソッドのモック化については、メソッドのモック化を参照されたい。 |
(5)
|
ticketSearchHelper のcreateDefaultTicketSearchForm メソッドについて1回呼び出されたことを検証する。
モック化したメソッドの検証については、モック化したメソッドの検証を参照されたい。 |
10.2.2.3.3. Helperの単体テスト¶
Helper
の単体テストは、Service
と同様の実装でテストすることができる。
実装方法については、Serviceの単体テストを参照されたい。