単体テストは開発の標準的な部分となっています。多くのツールがさまざまな方法で活用できます。この記事では、私にとって効果的ないくつかのヒントや、いわばベストプラクティスを紹介します。
この記事で学ぶ内容
- どのようにクリーンで読みやすい単体テストを書くか、JUnitとアサーションフレームワークを使って
- 特定のケースで偽陽性のテストを避ける方法
- 単体テストを書く際に避けるべきこと
NPEチェックの過度な使用を避ける
私たちは皆、NullPointerException
をできるだけ避けようとしますが、メインコードではそれが醜い結果をもたらす可能性があります。テストでNPEを避けることが私たちの主な懸念事項ではなく、テストされたコンポーネントの動作をクリーンで読みやすく、信頼性の高い方法で検証することが目的です。
悪い実践
過去に何度もisNotNull
アサーションを必要以上に使用していました。以下の例のように:
@Test
public void getMessage() {
assertThat(service).isNotNull();
assertThat(service.getMessage()).isEqualTo("Hello world!");
}
このテストは次のようなエラーを生成します:
java.lang.AssertionError:
Expecting actual not to be null
at com.github.aha.poc.junit.spring.StandardSpringTest.test(StandardSpringTest.java:19)
良い実践
追加のisNotNull
アサーションは実際に害はないかもしれませんが、以下の理由から避けるべきです:
- それは追加の価値をもたらさず、読みと維持するためのコードが増えるだけです。
- テストは
service
がnull
の場合に失敗するが、失敗の本当の原因が明らかになる。テストは依然としてその目的を果たしている。 - 断言がAssertJを使用しているおかげで、エラーメッセージはさらに良くなっている。
以下に修正されたテストの断言を示す。
@Test
public void getMessage() {
assertThat(service.getMessage()).isEqualTo("Hello world!");
}
修正されたテストは以下のようなエラーを生成する。
java.lang.NullPointerException: Cannot invoke "com.github.aha.poc.junit.spring.HelloService.getMessage()" because "this.service" is null
at com.github.aha.poc.junit.spring.StandardSpringTest.test(StandardSpringTest.java:19)
注意: 例はSimpleSpringTestで見つけることができる。
値をアサートし、結果ではない
時折、私たちは正しいテストを書くが、「悪い」方法で書かれている。つまり、テストは意図した通りに機能し、コンポーネントを検証するが、失敗が十分な情報を提供していない。そこで、私たちの目標は値をアサートし、比較結果ではないことである。
悪いプラクティス
そのような悪いテストのいくつかを見てみよう。
// #1
assertThat(argument.contains("o")).isTrue();
// #2
var result = "Welcome to JDK 10";
assertThat(result instanceof String).isTrue();
// #3
assertThat("".isBlank()).isTrue();
// #4
Optional<Method> testMethod = testInfo.getTestMethod();
assertThat(testMethod.isPresent()).isTrue();
以下に上記のテストからのいくつかのエラーを示す。
#1
Expecting value to be true but was false
at java.base/jdk.internal.reflect.DirectConstructorHandleAccessor.newInstance(DirectConstructorHandleAccessor.java:62)
at java.base/java.lang.reflect.Constructor.newInstanceWithCaller(Constructor.java:502)
at com.github.aha.poc.junit5.params.SimpleParamTests.stringTest(SimpleParamTests.java:23)
#3
Expecting value to be true but was false
at java.base/jdk.internal.reflect.DirectConstructorHandleAccessor.newInstance(DirectConstructorHandleAccessor.java:62)
at java.base/java.lang.reflect.Constructor.newInstanceWithCaller(Constructor.java:502)
at com.github.aha.poc.junit5.ConditionalTests.checkJdk11Feature(ConditionalTests.java:50)
良いプラクティス
解決策はAssertJとそのfluent APIを使うと非常に簡単である。上記のすべてのケースは簡単に書き換えることができる。
// #1
assertThat(argument).contains("o");
// #2
assertThat(result).isInstanceOf(String.class);
// #3
assertThat("").isBlank();
// #4
assertThat(testMethod).isPresent();
以前に言及された同じエラーが、今やより価値のあるものを提供している。
#1
Expecting actual:
"Hello"
to contain:
"f"
at com.github.aha.poc.junit5.params.SimpleParamTests.stringTest(SimpleParamTests.java:23)
#3
Expecting blank but was: "a"
at com.github.aha.poc.junit5.ConditionalTests.checkJdk11Feature(ConditionalTests.java:50)
Note: The example can be found in SimpleParamTests.
グループ関連のアサーションをまとめる
アサーションの連鎖と関連するコードのインデントは、テストの明確さと読みやすさを大幅に向上させます。
悪い例
テストを書く際に、正しいが読みにくいテストになることがあります。国を検索して、以下のチェックを行いたいテストを想像してみましょう。
- 見つかった国の数を数える。
- 最初のエントリーを複数の値でアサートする。
このようなテストは、次の例のようになることがあります。
@Test
void listCountries() {
List<Country> result = ...;
assertThat(result).hasSize(5);
var country = result.get(0);
assertThat(country.getName()).isEqualTo("Spain");
assertThat(country.getCities().stream().map(City::getName)).contains("Barcelona");
}
良い例
前述のテストは正しいものの、関連するアサーションをまとめることで読みやすさを大幅に改善できます(行9-11)。ここでの目標は、一度だけresult
をアサートし、必要に応じて多くの連鎖されたアサーションを書くことです。以下に修正版を示します。
@Test
void listCountries() {
List<Country> result = ...;
assertThat(result)
.hasSize(5)
.singleElement()
.satisfies(c -> {
assertThat(c.getName()).isEqualTo("Spain");
assertThat(c.getCities().stream().map(City::getName)).contains("Barcelona");
});
}
Note: The example can be found in CountryRepositoryOtherTests.
偽陽性の成功したテストを防ぐ
任意のアサーションメソッドがThrowingConsumer
引数を使用する場合、その引数にはコンシューマ内にassertThat
も含まれている必要があります。そうでなければ、テストは常にパスすることになります – 比較が失敗しても、つまり間違ったテストです。テストはアサーションがRuntimeException
またはAssertionError
例外をスローするときにのみ失敗します。わかりやすいとは思いますが、忘れがちで間違ったテストを書くことがあります。私も時々そうなります。
バッドプラクティス
国コードのカップルがあり、各コードが特定の条件を満たしていることを検証したいと想像してみましょう。ダミーのケースでは、各国コードに”a”文字が含まれていることをアサートしたいとします。見ての通り、これは無意味です:大文字のコードがありますが、アサーションで大文字小文字を区別しないことを適用していません。
@Test
void assertValues() throws Exception {
var countryCodes = List.of("CZ", "AT", "CA");
assertThat( countryCodes )
.hasSize(3)
.allSatisfy(countryCode -> countryCode.contains("a"));
}
驚くべきことに、私たちのテストは無事に合格しました。
グッドプラクティス
このセクションの冒頭で述べたように、テストはコンシューマ内に追加のassertThat
を含めることで簡単に修正できます(行7)。正しいテストは次のようになります。
@Test
void assertValues() throws Exception {
var countryCodes = List.of("CZ", "AT", "CA");
assertThat( countryCodes )
.hasSize(3)
.allSatisfy(countryCode -> assertThat( countryCode ).containsIgnoringCase("a"));
}
今度はテストが期待通りに失敗し、正しいエラーメッセージが表示されます。
java.lang.AssertionError:
Expecting all elements of:
["CZ", "AT", "CA"]
to satisfy given requirements, but these elements did not:
"CZ"
error:
Expecting actual:
"CZ"
to contain:
"a"
(ignoring case)
at com.github.aha.sat.core.clr.AppleTest.assertValues(AppleTest.java:45)
アサーションチェーン
最後のヒントは実践というよりも推奨事項です。AssertJのfluentAPIを利用して、より読みやすいテストを作成するべきです。
チェーンされていないアサーション
listLogs
テストを考えてみましょう。その目的は、コンポーネントのロギングをテストすることです。ここでの目標は、以下をチェックすることです:
- 収集されたログの主張された数
- 存在を主張する
DEBUG
およびINFO
ログメッセージ
@Test
void listLogs() throws Exception {
ListAppender<ILoggingEvent> logAppender = ...;
assertThat( logAppender.list ).hasSize(2);
assertThat( logAppender.list ).anySatisfy(logEntry -> {
assertThat( logEntry.getLevel() ).isEqualTo(DEBUG);
assertThat( logEntry.getFormattedMessage() ).startsWith("Initializing Apple");
});
assertThat( logAppender.list ).anySatisfy(logEntry -> {
assertThat( logEntry.getLevel() ).isEqualTo(INFO);
assertThat( logEntry.getFormattedMessage() ).isEqualTo("Here's Apple runner" );
});
}
アサーションの連鎖
前述のフルーティッドAPIと連鎖を使用して、テストをこのように変更できます:
@Test
void listLogs() throws Exception {
ListAppender<ILoggingEvent> logAppender = ...;
assertThat( logAppender.list )
.hasSize(2)
.anySatisfy(logEntry -> {
assertThat( logEntry.getLevel() ).isEqualTo(DEBUG);
assertThat( logEntry.getFormattedMessage() ).startsWith("Initializing Apple");
})
.anySatisfy(logEntry -> {
assertThat( logEntry.getLevel() ).isEqualTo(INFO);
assertThat( logEntry.getFormattedMessage() ).isEqualTo("Here's Apple runner" );
});
}
注: 例は AppleTest で見つけることができます。
まとめとソースコード
AssertJフレームワークは、フルーティッドAPIで多くの支援を提供しています。この記事では、より明確で信頼性の高いテストを生成するためのいくつかのヒントとヒントが提示されました。これらの推奨事項の多くは主観的であることに注意してください。それは個人の好みとコードスタイルに依存します。
使用されたソースコードは私のリポジトリにあります:
Source:
https://dzone.com/articles/hints-for-unit-testing-with-assertj