Gluegent Blog

Gluegent Blog

初めてのユニットテスト -Junit-

  • 技術
初めてのユニットテスト -Junit-

はじめまして、2023年度に新卒入社した塩山と申します。今回のテーマとして取り上げるのは、私が実務を任されてから一番苦戦してきたJunitを用いたテストコードの実装です。テストコードは効率よくプロダクトの品質を担保するために必要不可欠です。業務でテストを行う上で抑えておきたい基礎を一通りまとめました。初めてテストコードを触る方やもう一度勉強し直したい方は参考にしていただけたら幸いです。

目次

Junitとは
テストの作成
Junitの基本事項

  • 事前準備
  • テストメソッドの記述ルール
  • テストコードのおおまかな流れ
  • 結果確認

アサーション

  • assertEquals
  • assertThrows
  • fail
  • その他のアサーション

知っておくと便利な機能

  • 構造化(@Nested)
  • フィルタリング(@Tag)
  • 暗黙的セットアップ(@BeforeEach)
  • パラメータ化テスト(@ParameterizedTest)とカンマ区切りデータ(@CsvSource)

最後に

Junitとは

Junitは、Javaプログラムのユニットテストを行うためのフレームワークです。
ユニットテストとは、メソッドやクラスといったプログラムの個々の部分が期待通りに動作するかどうかを確認するためのテストです。プロジェクトの品質を向上させ、バグを早期に発見するためにユニットテストを導入する上でJUnitは非常に有用です。
※今回は現時点で最新かつ最も使われているJUnit5を使用していきます。

テストの作成

  1. プロジェクトの中にsrc/mainに対してsrc/testフォルダがあることを確認します。(なければ、プロジェクトを右クリック > New > Source Folder > Folder nameにsrc/testを入力し、フォルダを作成 )

  2. mainとtest内に同じパッケージがあることを確認します。(なければ、testフォルダを右クリック > New > Other > Package > Nameにパッケージ名(今回はjunit.sample)を入力)

  1. src/main/junit/sampleの中にあるテストしたいプロダクトコード(今回はCalculator.java)を右クリック > New > Other

  2. > junit > JUnit Test Caseを開きます。

5.以下のように入力して、テストクラスを作成します。

※Source Folderが(プロジェクト名)/src/testになっていることや、Packageがmainのものと同じになっていること、Nameがプロダクトコードのクラス名の後にTestがついている(今回はCalculatorTestとなっている)ことを確認します。

6.以下のようなテストクラスが作られていることを確認します。

Junitの基本事項

  • 事前準備

JunitはEclipseにデフォルトで組み込まれているため、pom.xmlやbuild.gradleといった設定ファイルに記述する必要はありません。
import文を書けば使えるようになります。今回は一般的によく用いられるorg.junit.jupiter.apiのモジュールを使用します。

  • テストメソッドの記述ルール
      • @Test アノテーションを付ける

      • 戻り値の型をvoidとする

      • テストメソッド名は"test"を先頭に付けるとわかりやすい

      • テストメソッドの中にテストコードを書く

@Test
void テストメソッド名(){
  //テストコード
}

  • テストコードのおおまかな流れ
    1. テスト対象のオブジェクト生成

    2. 期待値の定義

    3. メソッドを呼び出して実測値を出す

    4. 期待値と実測値が等しいか確認

[例] 今回の例では、Calculator.javaのaddメソッドをテストします。

[Calculator.java]package junit.sample;
public class Calculator {
   public int add(int x, int y) {
     return x + y;
   }
}

[CalculatorTest.java]
package junit.sample;
import static org.junit.jupiter.api.Assertions.*;
import org.junit.jupiter.api.Test;
class CalculatorTest {

@Test
void testAdd() {
   //1.テスト対象のオブジェクト生成
   Calculator cal = new Calculator();
   //2.期待値の定義
   int expected = 7;
   //3.メソッドを呼び出して実測値を出す
   int actual = cal.add(2, 5);
   //4. 期待値と実測値が等しいか確認(assertEqualsはアサーションの章で紹介)
   assertEquals(expected, actual);
}

}

  • 結果確認

動かしたいテストメソッド(今回はtestAdd)を右クリック > Debug as > Junit test を選択すると、下図のようなJunitのウィンドウが表示されます。下図のような緑のバーが出ていたら、成功となります。失敗する場合はErrorsかFailuresが出ます。Errorsはテストが正常に実行されなかったことを表します。コードにExceptionやErrorがあるときに生じます。一方で、Failuresは正常にテストは実行されたものの、期待通りのテスト結果が得られなかったときに出ます。

アサーション

アサーションとはJunitで値の比較検証を行うことを指します。 今回は一般的に使われることの多いorg.junit.jupiter.api.Assertionsクラスのアサーションメソッドをいくつか紹介します。

  • assertEquals

期待値と実測値が等しいときにテストが成功
※テストコードのおおまかな流れの節で例あり

  • assertThrows

テスト対象の処理を実行した際に、指定した例外が発生したらテストが成功 [例] 今回の例では、Calculator.javaのdivメソッドの引数yが0だった時に
IllegalArgumentExceptionが発生するかをテストします。

[Calculator.java]
package junit.sample;
public class Calculator {
  public float div(int x, int y) {
     if (y == 0) {
     throw new IllegalArgumentException("第二引数に0が指定されました");
  }
     return (float) x / (float) y;
  } 
}

[CalculatorTest.java]
package junit.sample;
import static org.junit.jupiter.api.Assertions.*;
import org.junit.jupiter.api.Test;
class CalculatorTest {

@Test
  void testDivException() {
     Calculator cal = new Calculator();
     assertThrows(IllegalArgumentException.class, () -> cal.div(1,
     0));
  }
}

  • fail

テストを失敗させるアサーションメソッド言い換えると、fail()が実行される分岐だとテストは失敗
assertThrowsと同様に例外をテストしたときに使われます。assertThrowsは例外が具体的に分かっているときに
使われるのに対して、failは分からないときに使われます。
[例] 今回の例では、Calculator.javaのdivメソッドの引数yが0だった時にtry内のdivの後の
処理が実行されないことをテストします。

[CalculatorTest.java]
package junit.sample;
import static org.junit.jupiter.api.Assertions.*;
import org.junit.jupiter.api.Test;
class CalculatorTest {

@Test
void testDivException2() {
   Calculator cal = new Calculator();
   try {
     cal.div(1, 0);
     //cal.divが実行された後にfail()が実行されないことを確認
     fail();
   } catch (IllegalArgumentException e) {
   }
}
}

  • その他のアサーション
      • assertTrue: 実測値がTrueの場合にテスト成功

      • assertNotNull: 実測値がNullではない場合にテスト成功

      • assertAll: 複数のテストを実行でき、1つのテストに失敗しても残りのテストが実行される

知っておくと便利な機能

  • 構造化(@Nested)

役割やプロダクトコードのメソッドごとにテストメソッドをグループ化することができます。
これを用いると、テストケースが増加した際でも可読性が担保できます。
また、プロダクトコードのメソッドごとにテストを実行することも可能です。
[例]CalculatorのDivメソッドに対してのテストメソッド3つをグループ化する

@Nested
public class DivTest {
   @Test
   void testDiv() {
      Calculator cal = new Calculator();
      int expected = 2;
      int actual = (int) cal.div(6, 3);
      assertEquals(expected, actual);
   }

@Test
void testDivException() {
   Calculator cal = new Calculator();
   assertThrows(IllegalArgumentException.class, () -> cal.div(1, 0));
}

@Test
void testDivException2() {
   Calculator cal = new Calculator();
   try {
   cal.div(1, 0);
   fail();
} catch (IllegalArgumentException e) {
}
}
}

上記のコードのようにまとめたいテストメソッドを@Nestedアノテーションを付けたclassとして{}で囲みます。
そうすることでテストを実行した際に、下図のように入れ子構造になり可読性が上がります。
また、ネストしたclassだけをテスト実行することも可能です。

  • フィルタリング(@Tag)

@Nestedと役割は少し似ていて、グループ化したい時に付けます。これを用いると、テストを実行するときにフィルタリングをすることができます。
[例] 2つのExceptionに関するテストメソッドにタグ付けをする

@Test

@Tag("Exception")
void testDivException() {
   Calculator cal = new Calculator();
   assertThrows(IllegalArgumentException.class, () ->
   cal.div(1, 0));
}

@Test
   @Tag("Exception")
   void testDivException2() {
   Calculator cal = new Calculator();
   try {
   cal.div(1, 0);
   fail();
} catch (IllegalArgumentException e) {
}

上記のコードのようにタグ付けしたいテストメソッドの前に@Tagを付ければ良いです。
実行するには、右クリック > Debug Configurations > Testタブ > Include and exclude tags> configureを押して、Include Tagsに実行したいタグを、exclude Tagsには実行したくないタブを付けます。今回はInclude TagsにExceptionを入力してみます。入力できたら、OKを押して、Debugを押します。

Debugしてみると、Exceptionタグを付けたテストメソッドだけ実行されていることを確認できます。

  • 暗黙的セットアップ(@BeforeEach)

これを用いると、各テストメソッドで共通して行う処理をまとめて書くことができ、処理の高速化ができます。
[例]全てのクラスメソッドで行われていたCalculatorのオブジェクト生成をまとめる

(変更前)

package junit.sample;
import static org.junit.jupiter.api.Assertions.*;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Tag;
import org.junit.jupiter.api.Test;
class CalculatorTest {

@Test
   void testAdd() {
   //1.テスト対象のオブジェクト生成
   Calculator cal = new Calculator();
   //2.期待値の定義
   int expected = 7;
   //3.メソッドを呼び出して実測値を出す
   int actual = cal.add(2, 5);
   //4.期待値と実測値が等しいか確認
   assertEquals(expected, actual);
}

@Nested
public class DivTest {

@Test
void testDiv() {
   Calculator cal = new Calculator();
   int expected = 2;
   int actual = (int) cal.div(6, 3);
   assertEquals(expected, actual);
}

@Test

@Tag("Exception")
void testDivException() {
   Calculator cal = new Calculator();
   assertThrows(IllegalArgumentException.class, () -> cal.div(1, 0));
}

@Test

@Tag("Exception")
void testDivException2() {
   Calculator cal = new Calculator();
   try {
      cal.div(1, 0);
      //cal.divが実行された後にfail()が実行されないことを確認
      fail();
   } catch (IllegalArgumentException e) {
   }
}
}
}

(変更後)

package junit.sample;
import static org.junit.jupiter.api.Assertions.*;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Tag;
import org.junit.jupiter.api.Test;
class CalculatorTest {
   Calculator cal;
   @BeforeEach
   public void setUp() {
      //1.テスト対象のオブジェクト生成
      cal = new Calculator();
   }

@Test

void testAdd() {
   //2.期待値の定義
   int expected = 7;
   //3.メソッドを呼び出して実測値を出す
   int actual = cal.add(2, 5);
   //4.期待値と実測値が等しいか確認
   assertEquals(expected, actual);
}

@Nested
public class DivTest {

@Test
void testDiv() {
   int expected = 2;
   int actual = (int) cal.div(6, 3);
   assertEquals(expected, actual);
}

@Test

@Tag("Exception")
void testDivException() {
   assertThrows(IllegalArgumentException.class, () -> cal.div(1, 0));
}

@Test

@Tag("Exception")
void testDivException2() {
   try {
      cal.div(1, 0);
      //cal.divが実行された後にfail()が実行されないことを確認
      fail();
   } catch (IllegalArgumentException e) {
   }
   }
}
}

  • パラメータ化テスト(@ParameterizedTest)とカンマ区切りデータ(@CsvSource)

@ParameterziedTestを用いると、テストメソッドの引数にデータを渡せるようになります。@CsvSourceを用いると、カンマ区切りのデータを定義できます。 これらを組み合わせると、引数により条件が増えた場合でもテストメソッドの数を抑えることができます。

[例] 「20歳以上」かつ「東京都在住」かつ「利用回数が1回以上」の場合に優待を受けられるプロダクトコードのテスト

[Customer.java]
package junit.sample;
public class Customer {
   int id;
   int age;
   String address;
   int count;
   public Customer() {
   }
   public boolean checkBonus(int age, String address, int count) {
      return age >= 20 && address.contentEquals("東京都") && count >= 1;
   }
}

[CustomerTest.java]
package junit.sample;
import static org.junit.jupiter.api.Assertions.*;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.CsvSource;
class CustomerTest {

@ParameterizedTest

@CsvSource({ "20,'東京都',1,true",
"20,'東京都',0,false",
"20,'千葉県',1,false",
"19,'東京都',1,false"
})

public void testCheckBonus(int age, String address, int count, boolean expected) {
   Customer customer = new Customer();
   boolean actual = customer.checkBonus(age, address, count);
   assertEquals(actual, expected);
}
}

CSVデータ1行に対してテストが1回実行され、データが前から順番にテストメソッドの引数に入っていきます。

このように4回分のテストを1つのテストメソッドで実行できるようになります。
テストケースが多い場合はこのパラメータ化テストを使うと効率よくテストできるようになると思います。

最後に

ブログをご覧いただきありがとうございました。今回はJunitの基礎や便利な機能についてまとめてみました。テストコードは効率よくプロダクトの品質を保つために必要不可欠です。今後もJunitについて調査を進め、使いこなせるようにしたいと思います。次回は、今回分量の問題で書ききれなかったmockitoのspyやmockについてまとめたいと思います!