前回までに、マイクロサービス(Backend)の単体・結合テストコードや効率的なテスト戦略のポイントなどを解説して来ました。今回からはマイクロサービスを呼び出す側のWebアプリケーション(BackendForFrontend:BFFアプリケーション)における単体テストおよび、 マイクロサービスを含めたEndToEndテストを実装する際のポイントやテスト戦略を説明していきます。
アプリケーションおよびテストのパッケージ・コンポーネント構成は以下としています。実際のソースコードは GitHub 上にありますので、必要に応じて適宜参照ください。
[backend-for-frontend]
└src
├main
│ ├java
│ │ └org
│ │ └debugroom
│ │ └mynavi
│ │ └sample
│ │ └continuous
│ │ └integration
│ │ └bff
│ │ └app ... アプリケーション層のパッケージ
│ │ │ └model ... リクエストパラメータのモデルクラスパッケージ
│ │ │ │ ├XxxxxForm.java ... HTMLフォームを表現するモデルクラス
│ │ │ │ ├Xxxxx.java ... 入力チェックルール等定義するモデルクラス
│ │ │ │ └XxxxxMapper.java ... commonプロジェクトにあるResourceクラスと相互変換するマッパークラス
│ │ │ └web ... MvcConfigでコンポーネントスキャンの対象とするパッケージ
│ │ │ └BackendForFrontendController.java ... リクエストハンドリング・ドメインサービス呼び出し後、テンプレートViewへ遷移するコントローラクラス
│ │ └domain ... ドメイン層のパッケージ
│ │ │ ├repository ... レポジトリクラスパッケージ
│ │ │ │ ├XxxxxResourceRepository.java ... Resourceレポジトリインタフェースクラス
│ │ │ │ └XxxxxResourceRepositoryImpl.java ... マイクロサービスへアクセスするRestClientを使用したレポジトリ実装クラス
│ │ │ └service ... DomainConfigでコンポーネントスキャンの対象とするサービスクラスパッケージ
│ │ │ ├SampleService.java ... シンプルに1つのマイクロサービスにアクセスするサービスクラス
│ │ │ ├SampleServiceImpl.java ... SampleServiceの実装クラス
│ │ │ ├OrchestrateService.java ... マイクロサービスの実行フロー制御を行うサービスクラス
│ │ │ └OrchestrateServiceImpl.java ... OrchestrateServiceインタフェースの実装クラス
│ │ └config ... 設定クラス用のパッケージ
│ │ ├WebApp.java ... Webアプリケーション起動クラス
│ │ ├DomainConfig.java ... ドメイン層に関する設定クラス
│ │ └MvcConfig.java ... アプリケーション層に関する設定クラス
│ └resources
│ ├static ... 静的リソースフォルダ
│ │ ├css ... CSSフォルダ
│ │ │ ├Xxxxx.css ... 各ページごとのCSSファイル(幅1280px以上)
│ │ │ ├Xxxxx_mobile.css ... 各ページごとのCSSファイル(幅320px以上)
│ │ │ └Xxxxx_tablet.css ... 各ページごとのCSSファイル(幅768px以上)
│ │ └js ... JavaScriptフォルダ
│ │ └Xxxxx.js ... 各ページごとのJSファイル
│ ├template ... Thymeleaf用テンプレートフォルダ
│ │ └Xxxxx.html ... ThymeleafテンプレートHTML
│ ├application.yml ... アプリケーション設定ファイル
│ ├application-dev.yml ... プロファイル"dev"で有効になるアプリケーション設定ファイル
│ ├messages.properties ... デフォルトメッセージ定義ファイル
│ ├messages_ja.properties ... ロケールがjaの際に有効になるメッセージ定義ファイル
│ └ValidationMessages.properties ... 入力チェックエラーメッセージ定義ファイル
└test ... テストパッケージフォルダ
├java
│ └org
│ └debugroom
│ └mynavi
│ └sample
│ └continuous
│ └integration
│ └bff
│ ├app
│ │ └web
│ │ ├selenium ... seleniumで使用するクラスパッケージ
│ │ │ ├PortalPage.java ... Pageオブジェクトクラス
│ │ │ └SeleniumProperties.java ... Selenium実行に使用するプロパティクラス
│ │ └BackendForFrontendControllerTest.java ... Controllerのテストクラス
│ ├domain
│ │ ├repository ... Repositoryテストパッケージ
│ │ │ └XxxxResoutceRepositoryTest.java ... XxxxxResourceRepositoryのテストクラス
│ │ └service ... Serviceテストパッケージ
│ │ └OrchestrateServiceImplTest.java ... OrchestrateServiceのテストクラス
│ └config
│ └TestConfig.java ... Testの汎用設定クラス
└resources
└application.yml ... テスト用のアプリケーション設定ファイル
マイクロサービスのアプリケーション構成同様、BFFアプリケーションでも主にController、Service、Repositoryという単位でコンポーネントを構成しています。 主なビジネス処理はバックエンド側のサービスにあるため、ControllerやServiceなどから直接RestTemplateなどのRestClientを使ってマイクロサービスを呼び出した方がシンプルで効率的に思えるかもしれませんが、 マイクロサービスの呼び出しが、レスポンスを待つ同期型中心となる場合、呼び出し側アプリケーションもこのようなレイヤ構造にしておくことで以下のような2つのメリットがあります。
それなりの規模のアプリケーションになってくると、単純にRestTemplateを使って呼び出すだけでは、バックエンドのマイクロサービスを呼び出す際のTryCatch文が乱立してコードの見通しも悪くなるため、 バックエンドのマイクロサービスをResourceの永続先レポジトリと捉え、ControllerからService経由で呼び出した方がスッキリします。本アプリケーションではこうした構成を前提にテストクラスを以下のような観点で実装していきます。
アプリケーション | 試験 | コンポーネント | 検証観点 |
Webアプリケーション (BFF) |
単体試験 | Respository(RestClient) | ・正しくビジネス例外が返されるか ・異常なレスポンスを受け取った場合正しくシステム例外が返されるか ・例外に正しくメッセージが設定されるか ・マイクロサービス側のサーバエラー発生時に正しくシステム例外が返されるか |
Service | ・Service実行の結果、正しくアウトプットが返されるか ・Service実行の結果、正しくビジネス例外が返されるか ・例外に正しくメッセージが設定されているか |
||
(View⇔)Controller | ・指定したHTTPメソッドやURLで正しくリクエストハンドリングされるか ・リクエストパラメータやパス変数が正しくマッピングされるか ・入力チェックが正しく行われているか ・入力チェックやビジネスエラー発生時に正しいメッセージやパラメータを返却するか ・サービス実行結果が正しく画面に表示されるか ・非同期通信の実行結果が正しく画面に表示されるか |
||
EndToEndテスト | [BFF] ⇔ [Backend] | ・ユースケースシナリオ通り操作した時に、正しく画面に結果が表示されるか ・ユースケースシナリオ通り操作した時に、エラーメッセージが正しく表示されるか ・データベースへ正しくデータが反映できるか ・画面表示のレイアウトが崩れていないか ・ブラウザごとに表示が異なっていないか |
また、以降、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>
前節でも説明した通り、このBFFアプリケーションでは、バックエンドのマイクロサービスの呼び出しを ResourceクラスのRepository として実装します。 リンク先のソースコードをみるとわかる通り、マイクロサービスから返却されるレスポンスはResourceオブジェクトに加え、マイクロサービスで発生した、HTTPステータスコードが400(BadRequest)でセットされたビジネスエラーやバリデーションエラー、ステータスコードが500のサーバエラーや通信エラーなどで返される場合もあります。 単体テストでは主にエラーが発生した場合の異常系のバリエーションケースを中心に、正しく例外ハンドリングが行われるかを検証します。とはいえ、実際にバックエンドのマイクロサービスを起動させてテストを実施するわけではなく、 REST通信に関わるエラーレスポンスなどを擬似的に生成可能な、Springから提供されているorg.springframework.test.web.client.MockRestServiceServerを使って、マイクロサービスの呼び出しをスタブ化して実行します。 また、RestTemplateを使ったテスト環境を簡易的に構築するorg.springframework.boot.test.autoconfigure.web.client.RestClientTestアノテーションを使用します。サンプルのテストコードは以下の通りです。
package org.debugroom.mynavi.sample.continuous.integration.bff.domain.repository;
// omit
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.client.RestClientTest;
import org.springframework.context.MessageSource;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.test.web.client.MockRestServiceServer;
import org.springframework.test.web.client.match.MockRestRequestMatchers;
import org.springframework.test.web.client.response.MockRestResponseCreators;
import org.springframework.web.client.RestTemplate;
@RunWith(Enclosed.class)
public class UserResourceRepositoryImplTest {
@RunWith(SpringRunner.class) // …(A)
@RestClientTest // …(B)
@ContextConfiguration(classes = {UnitTest.Config.class,
TestConfig.UnitTestConfig.class}) // …(C)
public static class UnitTest{
//omit
@Autowired
RestTemplate restTemplate;
@Autowired
MessageSource messageSource;
@Autowired
ObjectMapper objectMapper;
@Autowired
UserResourceRepository userResourceRepository;
@Rule
public ExpectedException expectedException = ExpectedException.none();
@Test
public void findOneAbnormalTest1() throws Exception{
MockRestServiceServer mockRestServiceServer = MockRestServiceServer
.bindTo(restTemplate).build(); // …(D)
Long userId = 0L;
String errorCode = "E0001";
BusinessException businessException = new BusinessException(errorCode,
messageSource.getMessage(errorCode, new Long[]{userId},
Locale.getDefault()), Long.toString(userId));
String jsonResponseBody1 = objectMapper.writeValueAsString(
BusinessExceptionResponse.builder()
.businessException(businessException)
.build()); // …(E)
mockRestServiceServer
.expect(MockRestRequestMatchers.requestTo("/backend/api/v1/users/0"))
.andExpect(MockRestRequestMatchers.method(HttpMethod.GET))
.andRespond(MockRestResponseCreators.withBadRequest().body(
jsonResponseBody1)); // …(F)
expectedException.expect(BusinessException.class);
expectedException.expect(BusinessExceptionMatcher.builder()
.businessException(businessException).build());
userResourceRepository.findOne(userId);
}
// omit
}
項番 | 説明 |
テストランナーとして、SpringRunnerを指定します。 | |
@RestClientTestアノテーションを付与します。 | |
UserResourceRepositoryでは、異常系のエラーメッセージの取得にMessageSourceを使用するため、テスト用のDIコンテナから取得できるように設定しておきます。@ContextConfigurationについては、 TERASOLUNAガイドライン Spring TestのDI機能 を適宜参照してください。 | |
MockRestServiceServerを動作させるRestTemplateを設定します。なお、@AutowiredでMockRestServiceServerをインジェクションしてもよいです。 | |
エラーメッセージを設定したビジネス例外をJSON表現の文字列化します。 | |
MockRestServiceServerが指定したURLでHTTPステータスコード400で(E)の文字列を返却するように設定します。 |
サンプルとして実装したテストケースと検証観点は以下になります。
警告
本来であればシステム例外は各コンポーネントの中で個別にハンドリングするのではなく、AOPなどで一律ハンドリングし、各業務開発者が意識せずに済むようにAP基盤部品として作成しておく方がベターです。今回はサンプルとしてシステム例外時のハンドリングもテストケースに含めています。
前節でも説明した通り、このBFFアプリケーションでは。主なビジネス処理であるバックエンドのマイクロサービスを呼び出すかたちですが、 Repositoryに実際の呼び出し処理を委譲し、Serviceではエラー発生時のリトライや、複数のマイクロサービスを呼び出した際に発生したエラー時のロールバック処理などの責務をもたせて実装します。 このようなサービスのフロー制御を実行する役割をオーケストレーションと呼び、エラー発生時のマイクロサービスの処理結果をロールバックする補償トランザクションを実行するパターンをSAGAパターンと呼びます。 SAGAパターンの詳細は microservices.io Pattern:Saga によくまとめられていますのでこちらも適宜参考にしてください。 以下のサンプルは、ユーザデータを複数保存する場合に、マイクロサービスを複数回呼び出し、エラーが発生した際、SAGAパターンに従ってロールバック処理するServiceコードの例です。
package org.debugroom.mynavi.sample.continuous.integration.bff.domain.service;
// omit
@Service
public class OrchestrateServiceImpl implements OrchestrateService {
@Autowired
UserResourceRepository userResourceRepository;
@Override
public List<UserResource> addUsers(List<UserResource> addUserResources)
throws BusinessException{
List<UserResource> userResources = new ArrayList<>();
for (UserResource addUserResource : addUserResources){
try{
userResources.add(userResourceRepository.save(addUserResource));
}catch (BusinessException e){
// Rollback for SAGA Pattern.
for(UserResource userResource : userResources){
userResourceRepository.delete(userResource.getUserId());
}
throw e;
}
}
return userResources;
}
}
こうした処理が実装されたServiceですが、実際のマイクロサービスの呼び出しはRepositoryに隠蔽されているため、テストコードもマイクロサービスにおけるServiceのそれと基本的に違いはありません。 Repositoryをモック化して、DIコンテナとともに実行に必要なコンポーネントをオートコンフィグレーションする@SpringBootTestアノテーションを用いて、 MessageSourceなどのコンポーネントはSpringのDIコンテナから取得できるようにしてテストコードを実装します。
Serviceの単体サンプルコードは以下の通りです。
package org.debugroom.mynavi.sample.continuous.integration.bff.domain.service;
// omit
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.test.context.junit4.SpringRunner;
@RunWith(Enclosed.class)
public class OrchestrateServiceImplTest {
// omit
@RunWith(SpringRunner.class)
@SpringBootTest(classes = {
OrchestrateServiceImplTest.UnitTest.Config.class
}, webEnvironment = SpringBootTest.WebEnvironment.NONE)
public static class UnitTest{
// omit
@MockBean
UserResourceRepository userResourceRepositoryMock;
@Autowired
OrchestrateService orchestrateService;
@Rule
public ExpectedException expectedException = ExpectedException.none();
// omit
@Before
public void setUp() throws Exception{
// omit
when(userResourceRepositoryMock.save(mockUser1)).thenReturn(mockUser1);
when(userResourceRepositoryMock.save(mockUser2)).thenThrow(BusinessException.class);
// omit
}
@Test
public void addUsersAbnormalTest1() throws Exception{
//omit
UserResource user1 = UserResource.builder()
.userId(userId1).firstName("taro").familyName("mynavi")
.loginId("taro.mynavi").address(address1)
.emailList(Arrays.asList(new EmailResource[]{email1, email2})).build();
UserResource user2 = UserResource.builder()
.userId(userId2).familyName("mynavi").firstName("hanako")
.loginId("hanako.mynavi").address(address2)
.emailList(Arrays.asList(new EmailResource[]{email3})).build();
//omit
expectedException.expect(BusinessException.class);
orchestrateService.addUsers(Arrays.asList(
new UserResource[]{user1, user2}));
}
// omit
上記のテスト実装により、Sevice実行時のアウトプットオブジェクトの妥当性やビジネス例外発生の妥当性、ビジネス例外のメッセージなどを 検証できます。サンプルで作成したテストケースは、Serviceの異常系処理を中心に、以下のようなユースケース・検証観点をもとに実装しています。 Serviceが複数のサービスにアクセスする場合のSAGAパターンによるロールバックや、リトライ処理などオーケストレーションの責務を負う場合は、Seviceの単体テストで適宜異常系のバリエーションを追加して検証するのがベターです。
ユースケース | 主な処理実装クラス・メソッド テストメソッド |
検証観点 |
[異常系]複数のユーザ情報を追加する(1) | OrchestrateService#addUsers OrchestrateServiceImplTest#addUsersAbnormalTest1() |
・Service実行の結果、正しくビジネス例外が返されるか |
[異常系]複数のユーザ情報を追加する(2) | OrchestrateService#addUsers OrchestrateServiceImplTest#addUsersAbnormalTest2() |
・マイクロサービス側のサーバエラー発生時に正しくシステム例外が返されるか |
注釈
このサンプルでは、ユーザを保存するマイクロサービスを複数回呼び出し、処理の途中でビジネスエラーが発生した場合、それまで成功した保存データを逆に削除していくロールバック処理をcatch節の中で実装します。ロールバック処理も含めて正常に完了した場合はビジネスエラーを返却し、また、マイクロサービスへの呼び出しの途中でサーバーエラーが発生した場合は、Repositoryからシステムエラーをスローする仕様を想定したテストケースとしています。
次回は、HTMLUnitを使用したBFFアプリケーションでのControllerの単体テスト、Seleniumを使用したEndToEndのテストコードをSpringBootを使って実装していきます。
川畑 光平(KAWABATA Kohei) - NTTデータ 課長代理
金融機関システム業務アプリケーション開発・システム基盤担当を経て、現在はソフトウェア開発自動化関連の研究開発・推進に従事。
Red Hat Certified Engineer、Pivotal Certified Spring Professional、AWS Certified Solutions Architect Professional等の資格を持ち、アプリケーション基盤・クラウドなど様々な開発プロジェクト支援にも携わる。