前回は、マイクロサービス(Backend)やそれを呼び出すWebアプリケーション(BFF:BackendForFrontend)のパッケージ/コンポーネント構成を示し、テスト観点を例示しました。今回からは、テストを実装する際のポイントやテスト戦略を説明していきます。
まずは、バックエンドで実行されるマイクロサービスの単体テストです。アプリケーションおよびテストのパッケージ/コンポーネント構成は以下のようになっています。
一般に、ソフトウエアの単体テストでは、以下のような項目に応じてその定義や検証観点が異なります。
- 開発組織やプロジェクト
- プログラミング言語
- テストのスコープとする処理や機能、プログラム単位
- Webアプリケーションやモバイルなどアプリケーション特性
JavaやSpringにおける単体テストでは、テスト対象のクラスが依存するコンポーネントをモックやスタブで置き換えてテストを行えるよう、さまざまな機能が提供されています。SpringBootを使ってアプリケーションを実装する場合は、主にController、Service、Repositoryという単位で単体コンポーネントと考え、以下のイメージの通り、モックやスタブの設定を行います。
また、テストについては、以下に示すような観点で実施することを推奨します。
アプリケーション | 試験 | コンポーネント | 検証観点 |
---|---|---|---|
マイクロサービス(Backend) | 単体試験 | Respository | ・エンティティクラスがテーブル定義と一致しているか ・O/Rマッピング設定が妥当か ・記載したSQLクエリや集合関数が正しく実行されるか ・該当しないデータが発生した場合に期待された戻り値が返されるか ・ 命名規約によるSQLクエリの自動組立 が正しく実行されるか ・指定した結合条件でデータが正しく取得できるか |
Service | ・Service実行の結果、正しくアウトプットが返されるか ・Service実行の結果、正しくビジネス例外が返されるか ・例外に正しくメッセージが設定されているか |
||
Controller | ・指定したHTTPメソッドやURLで正しくリクエストハンドリングされるか ・リクエストパラメータやパス変数が正しくマッピングされるか ・入力チェックが正しく行われているか ・入力チェックエラーやビジネスエラー発生時に正しいHTTPステータスを返却するか ・入力チェックエラーやビジネスエラー発生時に正しいメッセージやパラメータを返却するか ・レイヤ間のモデルオブジェクト変換は正しくマッピングされるか |
以降では、SpringBootを使ってテストコード実装を進めていきますが、プロジェクトのpom.xmlにspring-boot-starter-testのライブラリを含めておいてください。
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
Repositoryの単体テスト実装
「Repository」は、書籍「エリック・エヴァンスのドメイン駆動設計」(発行:翔泳社)で有名になった、データを永続化するコンポーネントです。J2EEパターンで言うところの「DataAccessObject(DAO)」に相当しますが、主な違いとしてRepositoryはDAOよりも多くのビジネスドメインのルール/制約を含んでいます。そのため、低レベルなAPIを持つデータベースアクセスコンポーネントにはない、よりビジネス的な意味合いを含んだコンポーネントだと言えます。save()メソッドがRepositoryの持つAPIであり、Insert()メソッドがDAOが持つAPIと考えるとわかりやすいでしょう。AddressやEmailを持つUserを永続化する場合、AddressやEmailを設定したUserに対し、repository#save(User)を1度呼べば済むのか、emailDao#insert(email)、addressDao#insert(address)、userDao#insert(User)をセットでコールするのかによって、実装ではより顕著な差が現れます。
また、永続化する先のデータストアが何(ファイル、RDB、NoSQLデータベース、他システムのデータストア)なのかもRepositoryは問いません。つまり、Repositoryはデータ永続化のためのより抽象的なコンポーネントとして扱うことができます。
このマイクロサービスでは永続化先をRDBに設定しているため、前掲の表に記載したテスト観点を設定していますが、Repositoryクラスの永続化先や実装ライブラリに応じて、適切な観点でテストを実施するようにしてください。以降、RDBとSpringDataJPAを用いたSpringBoot実装のテストについて説明していきます。
RDBとSpringDataJPAを用いたSpringBoot実装のテスト
Springでは、テスト実行環境を自動構築するいくつかのアノテーションを提供しています。SpringBootを使用したアプリケーションのテスト向けに提供されている@SpringBootTestアノテーションを使ってRepositoryのテストを実行することも可能ですが、その場合、テストで使用しないコンポーネントを含めてDIコンテナを構築するなど起動時間のオーバーヘッドが生じます。そのため、実行速度の観点から、JPAのRepositoryの単体テストでは、@DataJpaTestを使用することを推奨します※。
@DataJpaTestの使用方法としては、JUnitテストランナーとしてorg.springframework.test.context.junit4.SpringRunnerを指定したテストクラスに、org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTestアノテーションを付与します。この設定により、テスト実行環境として、pom.xmlで依存性を定義したH2やHSQLなどインメモリDBが構築され、テスト環境向けのEntityMangerであるTestEntityManagerがテスト用DIコンテナに追加されるようになります。
※ @DataJpaTest以外にも、@JdbcTestや@DataRedisTest、@DataMongoTest、@MyBaitsTestなど、データストアやORマッパーライブラリのに応じて同様の機能を持つアノテーションがサードパーティ含め提供されています。
■pom.xmlの依存性定義
<dependency>
<groupId>org.hsqldb</groupId>
<artifactId>hsqldb</artifactId>
<scope>runtime</scope>
</dependency>
テストコード上では、@AutowiredでTestEntityManagerを取得し、@Beforeを付与したテストのセットアップメソッドを用意して、前もって準備しておきたいテストデータをtestEntityManager#persist()でインメモリDBへ事前保存し、@Testメソッドでテスト検証コードを記載するかたちで利用します。
package org.debugroom.mynavi.sample.continuous.integration.backend.domain.repository;
// omit
import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest;
import org.springframework.boot.test.autoconfigure.orm.jpa.TestEntityManager;
import org.springframework.test.context.junit4.SpringRunner;
// omit
@RunWith(SpringRunner.class)
@DataJpaTest
public class UserRepositoryTest {
@Autowired
TestEntityManager testEntityManager;
@Autowired
UserRepository userRepository;
@Before
public void before(){
// omit
testEntityManager.persist(
User.builder()
.userId(userIdA)
.firstName("taro")
.familyName("mynavi")
.loginId("taro.mynavi")
.addressByUserId(address1)
.membershipsByUserId(
Arrays.asList(new Membership[]{
membership1, membership3}))
.ver(0)
.lastUpdatedAt(DateUtil.now())
.build());
}
// omit
@Test
public void testFindByLoginIdNormalCase(){
Optional optionalUser = userRepository.findByLoginId("taro.mynavi");
User user = optionalUser.get();
assertThat(user.getUserId(), is(0L));
assertThat(user.getFirstName(), is("taro"));
}
// omit
}
【注意点】
@DataJpaTestアノテーションに限らずですが、@SpringBootTestをはじめ、SpringBootで提供されているテスト用のアノテーションはテストクラスと同じ、もしくはその上位にあるパッケージに@SpringBootApplicaitonが付与された起動クラスが必要になります。SpringBoot起動クラスがsrc/main上で、テストクラスと同一もしくはその上位にあるパッケージにあれば問題ありませんが、本アプリケーションのようにテストクラスのパッケージの上位ルート上でもないconfigパッケージにある場合は、テスト用のパッケージに@SpringBootApplicaitonが付与されたクラスを作成しておきましょう。Repositoryで定義したインタフェースのメソッドに対するテストを実装し、期待結果を検証することで、テーブル定義とエンティティクラスの整合性や、エンティティクラスへのデータマッピング、SQLクエリの実行可否など、Repositoryやエンティティクラスの定義、SQL定義の実装の妥当性を検証可能です。
テーブルの結合によるデータ取得やRDBの集計関数を使ったデータアクセスなども合わせて検証可能なので、データ取得に関するエラーはこの単体テストで検出できるようにしておきましょう。
ただし、データベースの更新については、データベースの反映結果を取得して個別にアサーションを記載するとアサーションコード量が膨大になり大変です。次回以降で解説する結合試験でDBUnitを用いて、テーブルデータをまとめて検証したほうが容易なため、ここでは検証対象には含めないでおきます。今回、サンプルでは以下のようなユースケースと検証観点でテストコードを実装しています。
※ 結合条件を指定したSpecificationクラスに実装しているJPAのメタモデルクラスは、IDEのジェネレータ機能を使用して自動生成しています。本アプリケーションでは、IntelliJの公式ページやHibernateが提供するGeneratorの手順にならって設定していますが、IntelliJでは、メタモデルクラスのデフォルトの出力先がtargetフォルダになっているため、GitHubソースコード上には掲載されていないことに注意してください(IntelliJでもEclipseでもメタモデルクラスの出力機能があるので、その設定を行えば出力されるようになります)。
次回は引き続き、Service、Controllerのテストコードについて解説していきます。
著者紹介
川畑 光平(KAWABATA Kohei) - NTTデータ 課長代理
金融機関システム業務アプリケーション開発・システム基盤担当を経て、現在はソフトウェア開発自動化関連の研究開発・推進に従事。
Red Hat Certified Engineer、Pivotal Certified Spring Professional、AWS Certified Solutions Architect Professional等の資格を持ち、アプリケーション基盤・クラウドなどさまざまな開発プロジェクト支援にも携わる。2019 APN AWS Top Engineers & Ambassadors選出。