前回は、マイクロサービス(Backend)の単体テストの実装例や検証観点、テスト戦略のポイントを説明しました。今回は、バックエンドで実行されるマイクロサービスの結合テストを進めていきましょう。アプリケーションとテストのパッケージ/コンポーネント構成は前回と同様、以下のようにしています。
各コンポーネントはTERASOLUNAのガイドライン「レイヤの依存関係」を基本的に踏襲していますが、Controller→Service→Repositoryという単方向の呼び出ししかありません。そのため、結合試験としては、下記のイメージ通り、Service→Repositoryおよび、Controller→Service→Repositoryといった順に積み上げ式で試験を進めることにします(Repositoryをスタブ化し、ControllerとServiceのみの結合試験は除外できます)。
積み上げ式で進める結合試験のイメージ |
各テストの観点は以下の通りです。コンポーネントの内部構造は意識せず、ブラックボックス的に処理実行後のIOやデータベース反映結果を中心に検証します。
アプリケーション | 試験 | コンポーネント | 検証観点 |
---|---|---|---|
マイクロサービス(Backend) | 結合試験 | Service⇔Repository | ・データベースから正しく値が取得できるか ・データベースへ正しくデータが反映できるか ・設定ファイルが正しく動作するか |
Controller⇔Service⇔Repository | ・期待したレスポンスが返却されるか ・モデル間のデータマッピングが正しく実行されているか ・設定ファイルが正しく動作するか |
Service⇔Repositoryの結合テスト実装
データベースからの基本的なデータ取得については、Repositoryの単体テストで妥当性確認は取れているので、データベースへの更新結果を中心にDBUnitを用いて検証します(複雑な条件のデータ取得は処理結合レベルでバリエーションテストを実施したほうがよりベターです)。
また、Service単体でも分岐条件などで発生するビジネスエラーや設定されるメッセージの確認は取れているので、ServiceがRepositoryを正しく呼び出すことができるか、プロパティなどの設定が正しく動作するかを@SpringBootTestアノテーションを使って、SpringBootApplicationを起動した際と同様に検証します。
Serviceクラスを起点としたサンプル結合テストコードは以下の通りです。
package org.debugroom.mynavi.sample.continuous.integration.backend.domain.service;
// omit
import com.github.springtestdbunit.DbUnitTestExecutionListener;
import com.github.springtestdbunit.annotation.DbUnitConfiguration;
import com.github.springtestdbunit.annotation.ExpectedDatabase;
import com.github.springtestdbunit.annotation.ExpectedDatabases;
import com.github.springtestdbunit.assertion.DatabaseAssertionMode;
import com.github.springtestdbunit.dataset.AbstractDataSetLoader;
import org.dbunit.dataset.IDataSet;
import org.dbunit.dataset.csv.CsvDataSet;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.TestExecutionListeners;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.test.context.support.DependencyInjectionTestExecutionListener;
import org.springframework.test.context.support.DirtiesContextTestExecutionListener;
import org.springframework.test.context.transaction.TransactionalTestExecutionListener;
// omit
@RunWith(SpringRunner.class) // ...(A)
@SpringBootTest(classes = {
TestConfig.ServiceTestConfig.class,
}, webEnvironment = SpringBootTest.WebEnvironment.NONE) // ...(B)
@TestExecutionListeners({ DependencyInjectionTestExecutionListener.class,
DirtiesContextTestExecutionListener.class,
TransactionalTestExecutionListener.class,
DbUnitTestExecutionListener.class }) // ...(C)
@DbUnitConfiguration(dataSetLoader = IntegrationTest.CsvDataSetLoader.class) // ...(D)
@ActiveProfiles("dev")
public static class IntegrationTest{
public static class CsvDataSetLoader extends AbstractDataSetLoader{ // ...(E)
@Override
protected IDataSet createDataSet(Resource resource) throws Exception {
return new CsvDataSet(resource.getFile());
}
}
// omit
@Autowired
SampleService sampleService;
@Test
@ExpectedDatabases({ // ...(F)
@ExpectedDatabase(
value = "classpath:/META-INF/dbunit/domain/service/SampleServiceImplTest/add",
table = "usr", assertionMode = DatabaseAssertionMode.NON_STRICT),
@ExpectedDatabase(
value = "classpath:/META-INF/dbunit/domain/service/SampleServiceImplTest/add",
table = "address", assertionMode = DatabaseAssertionMode.NON_STRICT),
@ExpectedDatabase(
value = "classpath:/META-INF/dbunit/domain/service/SampleServiceImplTest/add",
table = "email", assertionMode = DatabaseAssertionMode.NON_STRICT_UNORDERED),
@ExpectedDatabase(
value = "classpath:/META-INF/dbunit/domain/service/SampleServiceImplTest/add",
table = "membership", assertionMode = DatabaseAssertionMode.NON_STRICT_UNORDERED),
})
public void addNormalTest() throws BusinessException{
// omit
User addUser = User.builder()
.firstName("saburo")
.familyName("mynavi")
.loginId("saburo.mynavi")
.isLogin(false)
.addressByUserId(addAddress)
.emailsByUserId(Arrays.asList(new Email[]{addEmail1, addEmail2}))
.membershipsByUserId(Arrays.asList(new Membership[]{membership1}))
.build();
sampleService.add(addUser);
}
項番 | 結合テストコードの説明 |
---|---|
A | テストランナーとして、SpringRunnerを指定します |
B | @SpringBootTestアノテーションには、テスト向け固有の設定クラスを任意に指定し、Controllerを介さない場合、Webコンテナ(Server)を起動しないオプションを指定しておきます |
C | DBUnitで使用するTestExecutionListenerの設定を行います。ここでは詳しい説明は割愛しますが、詳細についてはTERASOLUNAガイドライン「TestExecutionListenerの登録」を参照してください |
D | テストに使うデータベースへのデータ設定にはCSV形式のデータファイルを使用します。なお、データはExcel形式でも良いのですが、実行パフォーマンスの問題やコードコミット時にバイナリファイルで差分比較ができなくなるため、CSVのほうがベターです。詳細は「テストデータのセットアップ」を参照してください |
E | CSV形式でデータロードするための拡張クラスです |
F | テストメソッド実行後に期待するデータベースのデータを各テーブルごとに設定します。詳細については「Spring Test DBUnitを利用したテスト」を参照してください |
サンプルとして実装したテストケースと検証観点は、以下の通りです。サンプルでは、テストケースの順序をうまく設定すること(AddしてからFindAllする)で、トランザクションの有効化なども合わせて、処理結合テストレベルで検証するようにしています。なお、プロファイル「dev」で有効化する設定クラスを作成し、データベースとテストデータは事前にHSQLなどのインメモリDBに設定しておきます。
package org.debugroom.mynavi.sample.continuous.integration.backend.config;
import javax.sql.DataSource;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Profile;
import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseBuilder;
import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseType;
@Profile("dev")
@Configuration
public class DevConfig {
@Bean
public DataSource dataSource(){
return (new EmbeddedDatabaseBuilder())
.setType(EmbeddedDatabaseType.HSQL)
.addScript("classpath:ddl/schema.sql")
.addScript("classpath:ddl/data.sql")
.build();
}
}
Controller⇔Service⇔Repositoryの結合テスト実装
Controller⇔Service⇔Repositoryの結合テストでは、実際にアプリケーションを起動したときと同様、HTTPリクエストを送信し、期待したHTTPレスポンスが返ってくるかどうかを検証します。データベースへの反映結果は、前節のService⇔Repositoryで確認が取れているので、「Service実行後のアウトプットが期待通り、HTTPレスポンスに変換されるか」「モデルのデータマッピングが正しく実行されているか」「アプリケーションの設定が正しく動作するか」といった項目が主なチェックポイントになります。
Controllerクラスを起点とした結合テストサンプルコードは以下の通りです。
package org.debugroom.mynavi.sample.continuous.integration.backend.app.web;
// omit
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.web.client.TestRestTemplate;
import org.springframework.boot.web.server.LocalServerPort;
import org.springframework.test.context.junit4.SpringRunner;
// omit
@RunWith(SpringRunner.class) // ...(A)
@SpringBootTest(classes = TestConfig.ControllerTestConfig.class,
webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) // ...(B)
public static class IntegrationTest{
private static final String testServer = "localhost";
@Autowired
TestRestTemplate testRestTemplate; // ...(C)
@LocalServerPort
int port; // ...(D)
private String testServerURL;
@Before
public void setUp(){
testServerURL = "http://" + testServer + ":" + port; // ...(E)
}
@Test
public void getUsersNormalTest(){
ResponseEntity responseEntity = testRestTemplate
.getForEntity(testServerURL + "/api/v1/users", UserResource[].class);
List userResources = Arrays.asList(responseEntity.getBody());
assertThat(userResources.size(), is(3));
userResources.forEach(userResource -> {
switch (Long.toString(userResource.getUserId())){
case "1":
assertThat(userResource.getFirstName(), is("hanako"));
assertThat(userResource.getFamilyName(), is("mynavi"));
break;
// omit
}
});
}
項番 | 結合テストコードの説明 |
---|---|
A | テストランナーとして、SpringRunnerを指定します |
B | @SpringBootTestアノテーションには、テスト向け固有の設定クラスを任意に指定し、Webコンテナ(Server)の起動時のポートをランダムで指定しておきます |
C | 起動したテストアプリケーションに対して、リクエストを送信するTestRestTemplateをインジェクションします |
D | (B)でランダム指定したポートを取得します |
E | セットアップメソッドで、HTTPリクエストを送信するテストサーバURLを作成します |
マイクロサービスにおける結合テスト戦略と品質評価
本稿では、Service⇔Repository、Controller⇔Service⇔Repositoryの結合テストコード実装を解説してきました。単体テストと似通った試験を実施してもあまり意味がありません。効率的な結合テスト戦略策定のポイントとしては、以下の項目が挙げられます。
- コンポーネントの内部構造は意識せず、ブラックボックス的に処理実行後のInput/Outputやデータベース反映結果を中心に検証する
- テストケースの順序をうまく設定すること(AddしてからFindAllするなど)で、トランザクションの有効化なども合わせて、処理結合テストレベルで検証する
- 探索的テストを導入し、実装状況に応じてテストケースの重複を極力減らしながらテストコードを作成する
- 機能や処理の重要度に応じて、テスト実施内容に濃淡をつける(ビジネス的にそこまで重要ではない処理の参照系はテストしないなど)
もしテストクラスの量を増やしたくないのであれば、Service⇔Repositoryの結合テストを割愛し、Controller⇔Service⇔Repositoryの結合テストに含めるかたちで検証しても問題ありません。テスト品質は、ユースケース数に対するテストケースの割合、テストケースの定性的評価などを加えつつ評価するとよいでしょう。
次回は引き続き、マイクロサービスを呼び出すWebアプリケーションの単体テストコードをSpringBootでどのように実装するかについて解説していきます。
著者紹介
川畑 光平(KAWABATA Kohei) - NTTデータ 課長代理
金融機関システム業務アプリケーション開発・システム基盤担当を経て、現在はソフトウェア開発自動化関連の研究開発・推進に従事。
Red Hat Certified Engineer、Pivotal Certified Spring Professional、AWS Certified Solutions Architect Professional等の資格を持ち、アプリケーション基盤・クラウドなどさまざまな開発プロジェクト支援にも携わる。2019 APN AWS Top Engineers & Ambassadors選出。