優れたユニットテストは、コードが現に動作していること、また今後も動作し続けることを保証する素晴らしい手段です。 コードと機能の両方について高いカバレッジを達成する包括的なテストスイートがあれば、組織は多くの時間と頭痛の種を軽減できます。 とは言え、十分なテストが作成されていないプロジェクトは珍しくありません。 実際の開発者の中には、ユニットテストの実施に反対する人たちさえ存在します。
テストはどこに?
開発者が十分なユニットテストを作成しない理由はさまざまです。 最も大きな理由の1つは、特に大規模で複雑なプロジェクトの場合、構築および保守に要する時間です。 複雑なプロジェクトでは、ユニットテストで多数のオブジェクトをインスタンス化して構成する必要があるのが普通です。 セットアップには時間がかかり、テスト対象のコード自体と同じくらい(またはそれ以上に)テストコードが複雑になる場合もあります。
Javaの例を見てみましょう:
public LoanResponse requestLoan(LoanRequest loanRequest, LoanStrategy strategy)
{
LoanResponse response = new LoanResponse();
response .setApproved(true );
if (loanRequest .getDownPayment().compareTo(loanRequest .getAvailableFunds()) > 0) {
response .setApproved(false );
response .setMessage("error.insufficient.funds.for.down.payment ");
return response;
}
if (strategy .getQualifier(loanRequest) < strategy .getThreshold(adminManager)) {
response .setApproved(false );
response .setMessage(getErrorMessage());
}
return response;
}
これはLoanRequest を処理してLoanResponse を生成するメソッドです。 LoanRequest の処理に使用されるLoanStrategy 引数に注目してください。Strategyオブジェクトは複雑であるかもしれません。データベースや外部システムにアクセスしたり、 RuntimeExceptionを 投げるかもしれません。requestLoan() のテストを作成するには、テストするLoanStrategy の型を考慮する必要があります。おそらく、さまざまなLoanStrategyの 実装とLoanRequest の設定でメソッドをテストする必要があります。
requestLoan() のユニットテストは次のようになります。
@Test
public void testRequestLoan() throws Throwable
{
// Set up objects
DownPaymentLoanProcessor processor = new DownPaymentLoanProcessor();
LoanRequest loanRequest = LoanRequestFactory.create(1000, 100, 10000);
LoanStrategy strategy = new AvailableFundsLoanStrategy();
AdminManager adminManager = new AdminManagerImpl();
underTest.setAdminManager(adminManager);
Map<String, String> parameters = new HashMap<>();
parameters.put("loanProcessorThreshold", "20");
AdminDao adminDao = new InMemoryAdminDao(parameters);
adminManager.setAdminDao(adminDao);
// Call the method under test
LoanResponse response = processor.requestLoan(loanRequest, strategy);
// Assertions and other validations
}
ご覧のように、このテストには、オブジェクトを作成してパラメータを設定するだけのセクションがあります。 requestLoan() メソッドを見ただけでは、どのオブジェクトとパラメータを設定する必要があるのか、すぐには分かりませんでした。このサンプルを作成するには、テストを実行し、設定をいくつか追加してからテストを再実行するという作業を何度も繰り返さなければなりませんでした。 テスト対象のメソッドや何をテストするべきかを考えることもよりも、AdminManager とLoanStrategy をどのように設定するかを判断することのほうに、はるかに時間がかかりました。 それでも、さらに多くのLoanRequestの ケース、strategy、AdminDao のパラメータをカバーするには、さらにテストを拡張する必要があります。
また、実オブジェクトを使ってテストすると、requestLoan() の動作以外も検証することになります。テストの実行はAvailableFundsLoanStrategy 、AdminManagerImpl 、AdminDaoの 動作に依存しています。 つまり、実質的には、それらのクラスもテストしていることになります。 これは、場合によっては望ましいが、そうでない場合もあります。 さらに、これらの他のクラスのいずれかが変更された場合、 requestLoan() の動作が変更されていなくても、テストが失敗することがあります。 このテストでは、テスト対象のクラスを依存関係から分離するほうがよいでしょう。
モックオブジェクトの使用
複雑さの問題を解決する方法の1つは、複雑なオブジェクトをモックすることです。 この例では、 まずLoanStrategy パラメータのモックを使用します。
@Test
public void testRequestLoan() throws Throwable
{
// Set up objects
DownPaymentLoanProcessor processor = new DownPaymentLoanProcessor();
LoanRequest loanRequest = LoanRequestFactory.create(1000, 100, 10000);
LoanStrategy strategy = Mockito.mock(LoanStrategy.class);
Mockito.when(strategy.getQualifier(any(LoanRequest.class))).thenReturn(20.0d);
Mockito.when(strategy.getThreshold(any(AdminManager.class))).thenReturn(20.0d);
// Call the method under test
LoanResponse response = processor.requestLoan(loanRequest, strategy);
// Assertions and other validations
}
何が変わったのかを見てみましょう。 この例では、Mockito.mock を使用してLoanStrategy のモック化されたインスタンスを作成しています。 strategyオブジェクトでgetQualifier() およびgetThreshold() が呼び出されることがわかっているので、 Mockito.when(…).thenReturn() を使ってこれらの呼び出しの戻り値を定義します。 このテストでは、 LoanRequest インスタンスの値が何であるか気にする必要はなく、 AdminManager は実際のLoanStrategy によってのみ使用されるため、実際のAdminManager は必要ありません。
さらに、実際のLoanStrategy を使用していないため、 LoanStrategy の具体的な実装の処理には左右されません。 テスト環境、依存関係、複雑なオブジェクトを設定する必要はありません。 LoanStrategy やAdminManager ではなく、requestLoan() をテストすることに集中できます。テスト対象メソッドのコードフローは、モックによって直接制御されます。
Mockitoを利用すると、複雑なLoanStrategy インスタンスを手動で作成しなければならない場合に比べて、はるかに簡単にテストを作成できます。 しかし、まだいくつかの課題があります。
アプリケーションが複雑な場合、テストに多数のモックが必要な場合があります。
Mockitoの初心者の場合、構文とパターンを学ぶ必要があります。
どのメソッドをモック化する必要があるか分からない場合があります。
アプリケーションが変更されると、テスト(およびモック)も更新する必要があります。
Parasoft Jtestによる課題の解決
上記の課題への対処として、Parasoft Jtest JUnit 単体テスト アシスタント が開発されました。 単体テストアシスタントは、Parasoft Jtestのコンポーネントであり、モックを使用してユニットテストの作成と保守の最も困難な部分の一部を自動化します。 上記の例では、単体テストアシスタントは、1回のボタンクリックで、サンプルテストで示されているすべてのモックと検証を含むrequestLoan() のテストを自動生成できます。
単体テストアシスタント(UTA)の “Regular”アクションを使用して、次のテストを生成しました。
@Test
public void testRequestLoan() throws Throwable
{
// Given
DownPaymentLoanProcessor underTest = new DownPaymentLoanProcessor();
// When
double availableFunds = 0.0d; // UTA: default value
double downPayment = 0.0d; // UTA: default value
double loanAmount = 0.0d; // UTA: default value
LoanRequest loanRequest = LoanRequestFactory.create(availableFunds, downPayment, loanAmount);
LoanStrategy strategy = mockLoanStrategy();
LoanResponse result = underTest.requestLoan(loanRequest, strategy);
// Then
// assertNotNull(result);
}
このテストのすべてのモック化は、ヘルパーメソッドで行われます。
private static LoanStrategy mockLoanStrategy() throws Throwable
{
LoanStrategy strategy = mock(LoanStrategy.class);
double getQualifierResult = 0.0d; // UTA: default value
when(strategy.getQualifier(any(LoanRequest.class))).thenReturn(getQualifierResult);
double getThresholdResult = 0.0d; // UTA: default value
when(strategy.getThreshold(any(AdminManager.class))).thenReturn(getThresholdResult);
return strategy;
}
必要なすべてのモックがすでにセットアップされています。単体テストアシスタントはgetQualifier() およびgetThreshold() メソッドの呼び出しを検出し、メソッドをモック化しました。 availableFunds 、downPayment などの値を設定すると、テストの実行準備が整います(さらにカバレッジを上げるためにパラメータ化されたテストを生成することもできます)。単体テストアシスタントは、変更する必要がある値について「UTA: default value」というコメントでガイドを提供し、テストを容易にします。
これは、特に何をモック化するべきかやMockito APIの使い方がわからない場合に、テスト作成にかかる時間を大幅に節約します。
コードの変更に対処する
アプリケーションロジックが変わると、多くの場合、テストも変更する必要があります。 テストが適切に作成されている場合、テストを更新せずにコードを更新すると、テストが失敗します。多くの場合、テストを更新する上での最大の課題は、何を更新する必要があるか、どのように更新するべきかを理解することです。 多数のモックや値がある場合、どのような変更が必要かを把握するのは難しいでしょう。
これを説明するために、テスト対象コードを少し変更してみましょう:
public LoanResponse requestLoan(LoanRequest loanRequest, LoanStrategy strategy)
{
...
String result = strategy.validate(loanRequest);
if (result != null && !result.isEmpty()) {
response.setApproved(false);
response.setMessage(result);
return response;
}
...
return response;
}
LoanStrategy に新しいメソッドvalidate()が追加され、requestLoan() から呼び出されています。そのため、テストを更新してvalidate() の戻り値を指定する必要があるかもしれません。
生成されたテストを変更せずにUnit Test Assistant内で実行してみましょう:
単体テストアシスタントは、テスト実行中にモック化されたLoanStrategy 引数に対してvalidate() が呼び出されたことを検出しました。 メソッドはモック用に設定されていないので、単体テストアシスタントはvalidate() メソッドをモック化するよう推奨します。 即時修正アクションは、テストを自動的に更新します。 これは簡単な例ですが、足らないモックを見つけるのが大変な複雑なコードの場合、推奨事項と即時修正によってデバッグ時間を大幅に節約することができます。
即時修正を使ってテストを更新した後、新しいモックを参照して、validateResultの 期待値を設定することができます:
private static LoanStrategy mockLoanStrategy() throws Throwable {
LoanStrategy strategy = mock(LoanStrategy.class);
String validateResult = ""; // UTA: default value
when(strategy.validate(any(LoanRequest.class))).thenReturn(validateResult);
double getQualifierResult = 20.0d;
when(strategy.getQualifier(any(LoanRequest.class))).thenReturn(getQualifierResult);
double getThresholdResult = 20.0d;
when(strategy.getThreshold(any(AdminManager.class))).thenReturn(getThresholdResult);
return strategy;
}
メソッドの処理が新しいコードブロックに入るユースケースをテストするために、validateResultに空ではない値を設定したり、新しいブロックに入らないときの振る舞いを検証するために空の値(またはnull)を使用できます。
単体テストアシスタントには、テストフローを分析するための便利なツールもあります。 たとえば、テスト実行のフローツリーを次に示します。
テストを実行すると、LoanStrategy の新しいモックが作成され、 validate() 、getQualifier() 、およびgetThreshold() メソッドがモック化されたことがわかります。 メソッド呼び出しを選択し、呼び出しに使用された引数と返された値(またはスローされた例外)を変数ビューで確認できます。 テストをデバッグするとき、この機能はログファイルを調べるよりもずっと使いやすく分かりやすいでしょう。
Parasoft Jtest JUnit 単体テスト アシスタント は、時間と労力をかけずにユニットテストを作成して維持するのに役立ち、モックに伴う複雑さを軽減します。ほかにも、単体テストアシスタントは実行時データに基づいて、既存のテストを改善するのに役立つ多くの推奨事項を作成し、パラメータライズドテスト、Springアプリケーションテスト、PowerMock(静的メソッドおよびコンストラクターのモック化)をサポートしています。 さらに、Parasoft Jtestは、カバレッジデータ、静的解析、 Parasoft DTP をはじめとする洗練されたレポートおよび分析ツールとの統合などの機能も提供します。
(この記事は、開発元Parasoft社 Blog 「How to automate a Java unit test, including mocking and assertions 」2018年7月19日の翻訳記事です。)
Parasoft Jtestについて
Java対応静的解析・単体テストツール Parasoft Jtest
Jtestは、テスト工数の大幅削減とセキュアで高品質なJavaシステムの開発を強力にサポートするJava対応テストツールです。1,000個以上のコーディング規約をもとにソースコードを静的に解析し、プログラムの問題点や処理フローに潜む検出困難なエラーを検出します。さらに、JUnitを用いた単体テストについて、作成、実行、テストカバレッジ分析、テスト資産の管理といった単体テストに係る作業をサポートし、単体テストの効率化を促進します。