きり丸の技術日記

技術・エンジニアのイベント・資格等はこちらにまとめる予定です

【Java】【テスト究極系】動的にテスト対象を増やして、アサーションルーレットを回避する

過去にJavaのテストについていろいろ書きました。

パラメタライズドテスト、assertEqualsとassertThatとの比較等々…。

色々と学んだのですが、ダイナミックにテスト項目を増やす方法が分からず、苦汁をなめながらアサーションルーレットなテストを書いていました。

今回、動的にテスト項目を増やし、複数のテストを行いつつ、アサーションルーレットを回避する方法が分かったので記事にします。

個人的にはテストの究極系を見つけた!という気持ちです。


アサーションルーレットパターンとは

テストに失敗すると、失敗した行以降のテストが評価されなくなります。
そうなると、TDDで目的通りにテストを回すことが難しくなります。
なので、基本的に1メソッドで検証するのは1つだけにしましょう。

というのが、アサーションルーレットアンチパターンと言われているそうです。

void test(){
    assertEquals(true, true); // テストされる
    assertEquals(true, false); // エラーになる
    assertEquals(true, true); // この行はテストされない
}

1メソッドで検証するのが1つにしておくと、テストがシンプルになるので可読性が非常に良くなります。

ただ、E2Eテストとか結合テストになると、テスト1件に対する時間が長くなります。
E2Eテストで、テスト1つに検証1つだとテスト時間が増えすぎてしまいます。

一気にアサーションできるのであればテスト1つで検証複数でも問題ないと考えています。

この記事の概要


覚えて欲しいこと。

  • テスト対象の行をExecutableにキャストする
  • ExecutableをassertAllで実行する

覚えておいたら幸せになれること。

  • 失敗時のエラーメッセージを設定する
  • SoftAssertionsも兼用する

環境ライブラリ


※ 基本ライブラリでできるのがすごい嬉しい。

実装コード


実装方法自体はシンプルです。
Executableへのキャストが分かれば、すぐに実践できます。

void test() {
  // Collection型のExecutableを準備する
  List<Executable> testList = new ArrayList<>();
  // テスト対象の行を無銘関数でExecutableにキャストする
  Executable exec = () -> assertThat(true).isTrue();

  // テスト対象の行をCollectionに追加する
  testList.add(exec);
  // 実行する
  assertAll(testList);
}

注意点


メッセージについて


テストに失敗した時には、assertAllの行でエラーが起きたことになります。
何も考えずにアサーションを増やしてしまうと、どこで失敗したのかが分からなくて時間を無駄にしてしまいます。

// 行情報としてはこれだけ
at AssertAllTests.test_01(AssertAllTests.java:42)
...以後略

なので、必ずテスト失敗時に分かるような目印を渡してあげましょう。
私はここに、jsonファイルとの比較の時は、ファイルパスを渡していました。

// 例
assertThat(true).as("falseになるエラー").isFalse();
Multiple Failures (2 failures)
    [falseになるエラー] 
Expecting:
 <true>
to be equal to:
 <false>
but was not.
    Multiple Failures (1 failure)

SoftAssertionsも兼用する


SoftAssertionsを兼用するともっと便利です。
失敗行もちゃんと出力されるので、メッセージを元に何行目で失敗したかを確認する必要がありません。

void test() {
  SoftAssertions softly = new SoftAssertions();
  softly.assertThat(true).isTrue(); // OK
  softly.assertThat(true).isFalse(); // NG

  Executable exec3 = () -> softly.assertAll();
  testList.add(exec3);
  assertAll(testList);
}
// エラーログ
at AssertAllTests.test_01(AssertAllTests.java:42)
// 実際に失敗した行情報
at jp.co.kelly.biz.domain.AssertAllTests.test_01(AssertAllTests.java:48)
...以後略

必ずassertAllを呼び出すこと


Executableにすると、検証を後回しにすることはできます。

ただ、逆にassertAllで呼び出さないと、検証されないままテストが終わってしまいます。

これが分かっている人ならいいんですが、分かっていない人がやると「テストが落ちていないからOK」としてしまう可能性があります。

なので、assertAllでの呼び出しを必ず行うように周知してから、チームに導入するといいでしょう。

ユースケース

基本的には何でも使えます。

調べるきっかけ自体は、困っていることがあり、LINEのJavaのオープンチャットに下記の質問をしました。
結論から言うと、相談に乗ってくださった方とは完全に別の手段での解決になりました。

【やりたいこと】
jsonの比較を一気に行いたい。
(assertjのsoftAssertionsに値を詰めて、 最後にassertAllするイメージ)

【現状】
skyscreamerのJSONassertで一つ一つ比較

【背景】
dbをログテーブルのように扱ってる。
その、複数レコードの値を比較したい。
dbunitだとjson比較できないので、検証時に検索して取得した項目を比較している。

全レコードの相違点を確認したいが、一つ一つしか比較できなくてアサーションルーレットになってしまい、困っている。

最終的には、Jsonの比較のみならず、何でも使える手段ができて非常に満足しています。

終わりに

Junit5を使用しているだけで実行できているので、Javaとは言いつつ、Kotlinでも行けると思います。

もともと、下記のやり方を知ってたから無意識的にExecutableにキャストしていたのに、なんで今回の方法を1年間も思いつかなかったのか不思議でしょうがないです。

知らないクラスがまだまだ大量にあるので、有用なクラスは学んでいきたいですね。
目指せ!Javaエンジニアとしての認知!

assertAll(
  () -> assertEquals(true, true),
  () -> assertEquals(true, false),
  () -> assertEquals(true, true)
);

過去記事


これらの過去記事は良く書けた記事なので、Javaエンジニアは見て欲しいです。

nainaistar.hatenablog.com

nainaistar.hatenablog.com

GitHubでの検証ソース


github.com


もしこの記事が役に立ったのであれば、はてぶ、Twitterでの記事の拡散、twitterのフォローもよろしくお願いします。