前回は、マイクロサービス(Backend)やそれを呼び出すWebアプリケーション(BFF:BackendForFrontend)のパッケージ/コンポーネント構成を示し、テスト観点を例示しました。今回からは、テストを実装する際のポイントやテスト戦略を説明していきます。

まずは、バックエンドで実行されるマイクロサービスの単体テストです。アプリケーションおよびテストのパッケージ/コンポーネント構成は以下のようになっています。

アプリケーションおよびテストのパッケージ/コンポーネント構成

一般に、ソフトウエアの単体テストでは、以下のような項目に応じてその定義や検証観点が異なります。

  • 開発組織やプロジェクト
  • プログラミング言語
  • テストのスコープとする処理や機能、プログラム単位
  • Webアプリケーションやモバイルなどアプリケーション特性

JavaやSpringにおける単体テストでは、テスト対象のクラスが依存するコンポーネントをモックやスタブで置き換えてテストを行えるよう、さまざまな機能が提供されています。SpringBootを使ってアプリケーションを実装する場合は、主にController、Service、Repositoryという単位で単体コンポーネントと考え、以下のイメージの通り、モックやスタブの設定を行います。

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を用いて、テーブルデータをまとめて検証したほうが容易なため、ここでは検証対象には含めないでおきます。今回、サンプルでは以下のようなユースケースと検証観点でテストコードを実装しています。

ユースケース 主な処理実装クラスメソッド 検証観点
テストメソッド
[正常系]ログインIDを元にユーザーを検索する UserRepository#findByLoginIdUser(Entity) ・エンティティクラスがテーブル定義と一致しているか
・O/Rマッピング設定が妥当か
・ 命名規約によるSQLクエリの自動組立が正しく実行されるか
UserRepositoryTest#testFindByLoginIdNormalCase()
[異常系]ログインIDを元にユーザーを検索する UserRepository#findByLoginId ・該当しないデータが発生した場合に期待された戻り値が返されるか
UserRepositoryTest#testFindByLoginIdAbnormalCase()
[正常系]ログインIDを元にユーザーが存在するか確認する UserRepository#existsByLoginId ・ 命名規約によるSQLクエリの自動組立が正しく実行されるか
UserRepositoryTest#testExistsByLoginIdNormalCase()
[異常系]ログインIDを元にユーザーが存在するか確認する UserRepository#existsByLoginId ・該当しないデータが発生した場合に期待された戻り値が返されるか
UserRepositoryTest#testExistsByLoginIdAbnormalCase()
[正常系]指定された郵便番号の住所を持つユーザーを検索する FindUsersHavingAddressOfZipCode ・テーブルの結合条件が妥当か
UserRepositoryTest#testFindUsersHavingAddressOfZipCodeNormalCase()
[正常系]指定された郵便番号の住所を持たないユーザーを検索する FindUsersNotHavingAddressOfZipCode ・テーブルの結合条件が妥当か
UserRepositoryTest#testFindUsersNotHavingAddressOfZipCodeNormalCase()
[正常系]指定されたグループ名のグループに所属するユーザーを検索する FindUsersByGroup ・テーブルの結合条件が妥当か
UserRepositoryTest#testFindUsersByGroupNormalCase1()
[正常系]指定されたグループIDのグループに所属するユーザーを検索する FindUsersByGroup ・テーブルの結合条件が妥当か
UserRepositoryTest#testFindUsersByGroupNormalCase2()
[正常系]指定されたグループ名のグループに所属しないユーザーを検索する FindUsersByNotGroup ・テーブルの結合条件が妥当か
UserRepositoryTest#testFindUsersByNotGroupNormalCase1()
[正常系]指定されたグループIDのグループに所属しないユーザーを検索する FindUsersByNotGroup ・テーブルの結合条件が妥当か
UserRepositoryTest#testFindUsersByNotGroupNormalCase2()
[正常系]userIdでMaxの値を持つIDを検索する UserRepository#getMaxUserId ・記載したSQLクエリや集合関数が正しく実行されるか
UserRepositoryTest#testGetMaxUserIdNormalCase()
[正常系]指定されたメールアドレスを持つメールを検索する EmailRepository#findByEmail
Email(Entity)
・エンティティクラスがテーブル定義と一致しているか
・O/Rマッピング設定が妥当か
・ 命名規約によるSQLクエリの自動組立 が正しく実行されるか
EmailRepositoryTest#testFindByEmailNormalCase()
[正常系]指定されたグループ名を持つグループを検索する GroupRepository#findByGroupName
Group(Entity)
・エンティティクラスがテーブル定義と一致しているか
・O/Rマッピング設定が妥当か
・ 命名規約によるSQLクエリの自動組立が正しく実行されるか
GroupRepositoryTest#testFindByGroupNameNormalCase()
[正常系]指定されたユーザーIDを持つユーザーが所属するグループを検索する FindGroupsByUserId ・テーブルの結合条件が妥当か
GroupRepositoryTest#testFindGroupsByUserIdNormalCase()

※ 結合条件を指定した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選出。