Software Engineer Blog

エンジニアブログです。技術情報(Go/TypeScript/k8s)や趣味の話も書くかもです。

JUnit4を利用したJavaのテスト概要

概要

JUnitを利用した、Javaユニットテストを作成する際の基本的な部分について記載しました。
JUnit実践入門の内容 + 経験から記載しました。

テストコード基本

ユニットテスト対象物

テストクラスに対する、テスト対象物はテストクラスのコードのみとします。
そのため、外部オブジェクトの作成等は基本的にモック化します。
(外部クラスのコードのテストは、外部クラスのテスト上で行えば良いとの考え方です。)
またテストメソッドの対象物ですが、基本的に publicメソッド を対象とします。
privateメソッドは、publicメソッドを正確にテストすることで、同時にテストされていなければ、ならないと考えているためです。

各命名と役割

命名 役割 備考
XXXTest テストクラス名
xxx_条件_結果 テストメソッド - xxx()メソッド
- @Testアノテーションを付ける
sut テスト対象オブジェクト変数名 - テスト対象のオブジェクトを明確化するために同一名称で宣言する。
- System Under Testの略
setUp() テストごとの前処理 - 各テスト実行ごとの前処理を記載
- @Before アノテーションを付ける
tearDown() テストごとの後処理 - 各テスト実行ごとの後処理を記載
- @After アノテーションを付ける
setUpClass() テストクラスごとの前処理 - テストクラス内のテストが一つでも実行される前の処理を記載
- @BeforeClass アノテーション記載
- public static methodで宣言
tearDownClass() テストクラスごとの後処理 - テストクラス内のテストが全て実行された後の処理を記載
- @AfterClass アノテーション記載
- public static methodで宣言

テストのフェーズ

  1. 事前準備 = set up
    下記を行います。

    • テストデータ作成
    • DBの接続
    • モックの作成
    • モックからの返り値の設定
    • Testクラスに渡すオブジェクトの作成
    • Testクラスのオブジェクト化
    • ..etc
  2. 実行 = exercise
    テスト対象のメソッドを実行します

  3. 検証 = verify
    メソッドの実行結果を検証します。
    メソッドの返り値が期待値通りかを判定するフェーズです。

  4. 後処理 = tear down
    テスト実行後の後処理を行います。

    • テストデータの破棄
    • DBの切断

Assert

テスト結果を検証するための方法についてです。
検証とは基本的に、テスト結果が期待値通りかを確かめるフェーズとなります。

  • Assertは、org.junit.Assert.assertThat() を利用
     - `assertThat` 一つで基本的に事足りる
     - static importを行っておく
    
  • Matcher
    • org.hamcrest.CoreMatchers
      • is() equalsメソッドによる比較
      • nullValue() nullであることを検証する
        • 利用する際は、 is(nullValue()) と利用する
      • not() 評価値を反転させる
    • org.junit.matchers.JUnitMatchers
      • hasItem() Iterableインタフェースを実装したクラスに、期待値が含まれているか検証
      • hasItems() 複数指定可能
    • BaseMatcher<> を継承することで、カスタムMatcherが作成できる

サンプルコード

上記までの内容を踏まえた、テストサンプルコードです。

public class TargetClassTest {
  @Before
  public void setUp() {
    // テストごとの共通の前(初期化)処理
  }

  @After
  public void tearDown() {
    // テストごとの共通の後処理
  }

  @Test
  public void hasUserName_ユーザー名称をもつ場合_trueを返却() throws Exception {
   // set up
   /** テスト対象のオブジェクトの変数名は sut とする **/
   TargetClass sut = new TargetClass)();

   // exercise
   boolean actual = sut.hasUserName();

   // verify
   /** 英語の構文 "assert that actual is expected" を意識する**/
   asseertThat(actual, is(true));
  }

(追記) Exception発生時のテスト

  /**
  * Exception発生時のテスト
  */
  @Test(expected = NullPointerException.class)
  public void fetchUserName_ユーザー名称を取得に失敗_exception発生() {
   // set up
   /** テスト対象のオブジェクトの変数名は sut とする **/
   TargetClass sut = new TargetClass)();

   // exercise
   /** 下記メソッド実行時にException発生 **/
   Result actual = sut.fetchUserName();
  }
}

テスト作成にあたっての他用語

Fixture

事前準備にて、設定する情報のことで、下記2パターンのFixtureの設定方法があります。

  • inline set up
    • 各テストメソッドごとに、fixtureのset upを行う
    • simpleに設定を記述すれば良い
    • コードが長くなり、可読性が悪くなりがち
  • implicit set up

パラメータ化テスト

テストに対してパラメータを設定したい際に、利用します。

Theories を利用して実現します。

  • Theories
    • @RunWith(Theories.class) をクラス宣言の前に宣言
      • テストランナーの一つ
    • @Theory テストメソッドのアノテーション
    • @DataPoint パラメータ
// サンプルコード
@RunWith(Theories.class)
public class TargetClassTest {
  @DataPoint
  public static int PARAM_1 = 1;

  @DataPoint
  public static int PARAM_2 = 2;

  public TargetClassTest() {
    // 初期化処理
  }

  @Theory
  public void testCase(int x) throws Exception {

  }
}

Rule

テストをプラグイン的に拡張できる機能のことです。
テストに関係なく、初期化+後処理をしたい場合 に便利です。
publicなfiledにアノテーションをつけて利用します。

// サンプル
@Rule
public Timeout timeout = new Timeout(100);
  • 処理はテストごとに実行される
    → テストごとに実行したい共通処理をRuleとしてまとめられる
  • @ClassRule にてテストClassごとに1回のRuleも作成可能
  • カスタムルールの作成 方法1
    • org.junit.rules.TestRule インターフェイスを継承する
    • Statement apply(final Statement base, Description description) をOverrideする
      • 引数で渡される Statement base が各テストメソッドのイメージ
      • base.evaluate() を実行することでテストが実行される
    • これを利用することでテストに共通の前後処理を定義することができる
public class HogeRule implements TestRule {

  private void before() {}
  private void after() {}

  @Override
  public Statement apply(final Statement base, Description description) {
    // new Statement()することで実際のテストを拡張している
    return new Statement() {
      // 前処理
      before()
      
      // テスト実行 (@Before -> テスト実行 -> @After)
      base.evaluate();

      // 後処理
      after()
    }
  }
}
  • カスタムルールの作成 方法2 (← こちらのほうが効率よく作れます)
    • 上記の設定を、事前に行っているクラス(ExternalResource)を利用
    • ExternalResourceを継承して作成
    • 利用する際は、before()after()をOverrideして、前後処理を記載する
    • Rule内で利用する変数はprivate fieldに宣言しておき、コンストラクタで受け取る
// 上記のサンプルコードをすでに実装した抽象クラス
public abstract class ExternalResource implements TestRule {
    public Statement apply(Statement base, Description description) {
        return statement(base);
    }

    private Statement statement(final Statement base) {
        return new Statement() {
            @Override
            public void evaluate() throws Throwable {
                before();
                try {
                    base.evaluate();
                } finally {
                    after();
                }
            }
        };
    }

    /**
     * Override to set up your specific external resource.
     *
     * @throws if setup fails (which will disable {@code after}
     */
    protected void before() throws Throwable {
        // do nothing
    }

    /**
     * Override to tear down your specific external resource.
     */
    protected void after() {
        // do nothing
    }
}

モック

モックとは、テスト対象クラス以外のクラスを擬似的に作成して、対象外クラスのメソッドの 返り値を設定する事ができます。
テスト対象クラス に絞ったテストを行うために重要な要素です。

  • org.mockito.Mockito ライブラリを利用する
    • JavaDoc
      • mockの利用方法について詳細が記載されている
    • モックの作成 mock(TargetClass.class)
    • 返り値の設定 when(sut.exec(anyInt(), any(Date.class))).thenReturn(obj)

データベースのテスト

(手法が確立できていないため一時保留)

  • H2 Databaseの利用
    • ピュアJavaSQLデータベース
    • インメモリ or ファイルの2種類の作成方法がある
    • 組み込みモード、サーバーモード、ミックスモードで動作
    • jarファイル1つ(1.5MB)動作
    • JDBCサポート
  • DBUnit
    • @Ruleとして、作成することでDB接続周りを一手に引き受けられる
    • org.dbunit.AbstractDatabaseTester を継承する

コードカバレッジ

カバレッジの種類

  • C0(命令網羅)
    • プログラム中に定義された命令が1回以上実行されたかを測定
    • line coverageと同等
    • 基準
  • C1(分岐網羅)
    • すべての分岐(if)を1回以上実行したかを測定
    • if条件の true or false のどちらも通す必要がある
  • C2(条件網羅)
    • すべての条件を1回以上実行したかを測定
    • if条件のtrueになる全ての条件とfalseになるすべての条件を見る必要がある

カバレッジ測定ツール

# テスト実行 + カバレッジ測定
% mvn clean jacoco:prepare-agent test jacoco:report

# カバレッジレポート参照
% target/site/jacoco/index.html

おわりに

JUnitを利用した、ユニットテストに関してざっくりとした記事を書きました。
昨今は、マイクロサービス化の利用等でますますユニットテストの価値が上がってきているかと思います。
(テストがないコードはレガシーとまで言われるかもしれないですね。。)
テストを何のために書くのかや、どういったテストが良いテストなのかについては特に記載しませんでした。
そのあたりは自分で考えていただいて、テストの有用性に対して正確な理解を個人でしてもらえればと思います。
また、自分で利用したことがないため、テストランナーについてあまり記載できていませんでしたので、 学んだ際に追記できればと思います。

参考文献

JUnit実践入門 ~体系的に学ぶユニットテストの技法 (WEB+DB PRESS plus)