テスト、JUnitの概要

テスト、JUnit概要

テストとは

ソフトウェア開発の工程のうち、開発したプログラムが当初予定していた仕様、および顧客(システムの発注者)の要望通りに動作するかを確認する作業を「テスト」と言います。
テストの中にも段階があり、単体テスト(ユニットテスト)、結合テスト、システムテスト(総合テスト)、受入れテストといった順で4段階があります。
単体テスト~システムテストは開発者側で実施し、受入テストは顧客が実施します。
参考サイト:ソフトウェア開発におけるテスト工程とは?|お役立ちコラム

このうち、単体テストプログラムを構成する小さな単位(メソッドなど)が正しく機能しているか検証するテストです。
開発の初期段階で行われ、問題の早期発見と修正に繋がるので、全体の品質向上に貢献します。

テストの実施方法には、下図のように項目書を作成して、人力で実際の操作を行いその結果を記録していくようなパターンと、
テストコードを作成して実行し結果を得るパターンがあります。

JUnitとは

JUnitとは、Java言語で開発されたプログラムに対し単体テストを行なうためのソフトウェア・プログラムテスティングフレームワークです。
テストプログラムを実行することで、テストデータの入力と結果の確認を自動的に行え、効率化が図れます。
基本的に一度作ってれば、コードに修正が入ったときに影響がないことの確認を何度でも手軽に繰り返しテストを行うことが可能です。

単体テストのためのテストフレームワークは、各プログラミング言語毎に様々な団体から提供されています。
例えばPHPであればPHPUnitといったものがあり、一般に「xUnit」とも呼ばれます。

JUnitは、Javaのテストフレームワークの中でも広く使用されています。

JUnit Test サンプルの作成・実行

JUnit Testの導入

JUnitテストを実行するには、JUnitが作業環境に導入されている必要があります。
※今回は現時点で最新かつメジャーなJUnit5を使用します。

プロジェクト名で右クリック>ビルド・パス>ライブラリーの追加 をクリック


ライブラリーの追加 画面で「JUnit」を選択>次へ>完了


プロジェクト直下に「JUnit 5」ライブラリーが追加されます。以上でライブラリーの導入は完了です。

テストプログラムの作成

テストする対象のサンプルクラスを作成

今回テストする対象のクラスを作成します。
下記のクラス内のメソッドに対してテストを行っていきます。

【src/com/cmps/unittest/JunitPractice1.java】

package com.cmps.unittest;

public class JunitPractice1 {

	/**
	 * 整数が0~9の間にあるか判定する
	 * @param num
	 * @return
	 */
	public String inputNum(int num) {
		String val = null;

		if (num < 0 || num > 9) {
			val = "エラー!! 0 から 9 の数字ではありません。";
		} else {
			val = num + "が入力されました。";
		}

		return val;
	}
}

テストの作成

フォルダの作成

テストは一般に、テストファイル専用のソースフォルダに保存します。
詳細なフォルダ構成はプロジェクトにより異なりますが、今回は以下の通り作成します。

プロジェクトで右クリック>新規>ソースフォルダー をクリック


→「フォルダー名」に「src.test」と入力して完了
以下のようなフォルダ構成になります。

src.testに「com.cmps.unittest」パッケージを作成します。テストファイルを配置するためのパッケージです。

テストファイルの作成

テストクラスを作成します。
テスト対象の「JunitPractice1.java」を右クリック>新規>「JUnitテスト・ケース」を選択
※「JUnitテスト・ケース」が表示されていない場合は「その他」をクリック>JUnitで検索して選択します



ソースフォルダーを変更し、完了をクリックします。
☆テストクラスの名前は「テスト対象のクラス名 + Test」が基本ですので、デフォルトでそのように表示されています。



「次へ」をクリック>テストメソッドを作りたいメソッドにチェックを入れます。(基本的にクラスにチェックでOK)
今回はinputNumにチェックを入れます。


テストメソッド名は「test+メソッド名(キャメルケース)」が一般的であり、その名称で作成されます。

作成されたテストファイルの内容を、以下の通り書き換えます。
【src.test/com/cmps/unittest/JUnitPractice1Test.java】

package com.cmps.unittest;

import static org.junit.Assert.*;
import org.junit.jupiter.api.Test;

class JUnitPractice1Test {

	@Test
	void testInputNum() {

		//(1) テスト対象オブジェクトを生成
		JunitPractice1 practice = new JunitPractice1();

		//(2) 入力値、想定結果を設定
		int inputNum = 0;
		String expectesStr = "0が入力されました。";

		//(3) メソッドを呼び出して実測値を取得
		String outputStr = practice.inputNum(inputNum);

		//(4) 想定結果と実測値が等しいか確認
		assertEquals(expectesStr, outputStr);
	}
}

コードについて解説します。

@TestアノテーションはJUnitにテストメソッドであることを認識させます。テストメソッドには忘れず記載しましょう。

アノテーションとは、日本語にすると「注釈、注記」といった意味です。
Javaのコード上では”@(アットマーク)”から始まる形で記述したもので、コードでは表現しきれない情報を補足としてつけ加えられます。

・テストメソッドに記述する内容はコード内のコメント(1)~(4)の通りです。
(1) テストオブジェクトの生成
テスト対象クラスのメソッドを呼び出したいので、オブジェクト化します。
今回のようにパッケージが同じであれば、importは不要です。(同パッケージ内のクラスはimportなしで呼び出し可)

(2) 入力値、想定結果を設定
テストは具体的に結果を確かめたいので、入力値と想定結果を用意します。

(3) メソッドを呼び出して実測値を取得
入力値を用いて実測値(戻り値)を取得します。

(4) 想定結果と実測値が等しいか確認
ここではassertEqualsメソッドを使用しています。第1引数の期待値と、第2引数の実測値が等しいことを検証します。
第1引数、第2引数にはObjectが入れられるので数値でも文字列でも入れることが可能です。

assertEqualsメソッドを突然使用していますが、これは import static org.junit.Assert.*; の記述で静的インポート(staticインポート)していることによります。本来は Assert.assertEquals(expectesStr, outputStr); とクラスから書かなければならないところを省略できるので、コードを簡潔にして可読性を高めることができます。

テストの実行・結果の確認方法

テストの実行方法は以下
テストファイルを右クリック>実行>JUnitテスト (または Alt+Shift+X, T)

実行が完了すると、JUnitビューが表示され、結果が表示されます。
テスト結果がOKの場合は下記のように緑色で表示され、

結果がNGの場合は赤色で表示されます。
「障害トレース」にはどのようにNGなのかも記載されます。(下図はテストメソッドのinputNumだけを3にして実行した場合)

単体テスト技法

単体テスト手法には、「ホワイトボックステスト」と「ブラックボックステスト」があります。
ホワイトボックステストは、プログラムの内部構造を理解したうえで(=コードの中身を見ながら)、正しい構造で意図したとおりに動くかを確認するテストで、
ブラックボックステストは、プログラムの内部構造を考慮せず、要件定義・仕様を満たしているかを確認するテストです。

ホワイトボックステスト

ホワイトボックステストには以下の種類があります。

  • 命令網羅
  • 分岐網羅
  • 条件網羅
  • 複数条件網羅

先ほどのテスト対象クラスのinputNumメソッドを元に見ていきましょう。

public String inputNum(int num) {
    String val = null;

    if (num < 0 || num > 9) {
        val = "エラー!! 0 から 9 の数字ではありません。";
    } else {
        val = num + "が入力されました。";

    }
    return val;
}

命令網羅

すべての命令文を実行するようにテストケースを作成します。

今回の例であれば「”エラー!! 0 から 9 の数字ではありません。”」、「num + “が入力されました。”」の2つが確認できればOKです。
テストケース例:「5」「10」の2パターン(命令文が確認できればどの値でも良い)

分岐網羅

if文の分岐の各パターンを網羅する形式です。

今回の例であればif文が1つなので、trueになるかfalseになるかの2パターンを確認できればよいです。
テストケース例:「5」「10」の2パターン(if文のパターンが網羅できれば良い)

条件網羅

条件式に対して、「真」「偽」の両パターンを網羅します。

テストケース例:「5」「10」の2パターン

複数条件網羅

複数の条件に対し、各条件が「真」「偽」となる組み合わせ全てを網羅します。

今回の例であれば「num < 0」、「num > 9」のそれぞれに対して「真」「偽」となる組み合わせ(最大4つ)
テストケース例:「-1(真・偽)」「0(偽・偽)」「15(偽・真)」の3パターン(真・真の組み合わせはないので省略)

ブラックボックステスト

ブラックボックステストの種類として以下を紹介します。

  • 同値分割法
  • 境界値分析

先ほどのテスト対象クラスのinputNumメソッドにあたる仕様が以下であったとして、この仕様を元に考えます。
仕様1「0 より小さい値を入力、もしくは、 9 より大きい値を入力した場合、”エラ ー!! 0 から 9 の数字ではありません。”と表示する。」
仕様2「1桁の数値x(0~9)を入力した場合、”xが入力されました。”と表示する。」

同値分割法

「出力結果が同じと期待される入力の集合 」を 等価である(同値クラス)とみなし、テスト します。

今回の例であれば「0~9の数値」、「0~9以外」の数値という2つの同値クラスがあると考えます。
テストケース例:「-3」「0」「15」の3パターン(同値クラス内であればどの値でも良い)

境界値分析

境界となる値(同値クラスの端)とその前後に対してテストします。

今回の例であれば「0~9の数値」、「0~9以外」2つの同値クラスの境界値に対して設定します。
テストケース例:「-1」「0」「9」「10」の4パターン

テストメソッドの作成

上記で説明した通り、実際にはほとんどの場合で、1つのメソッドに対し複数のテストメソッドを用意します。
テストメソッドを複数作成する場合、一般に「test+メソッド名(キャメルケース)+_001」や「test+メソッド名(キャメルケース)+01」といった形で連番で命名します。
または、連番の代わりにテスト内容にあった命名をします。

このページの序盤で作成した「JUnitPractice1Test」を分岐網羅で作成した例が下記です。
【src.test/com/cmps/unittest/JUnitPractice1Test.java】

package com.cmps.unittest;

import static org.junit.Assert.*;
import org.junit.jupiter.api.Test;

class JunitPractice1Test {

	@Test
	void testInputNum_001() {////メソッド名を連番にする
		//(1) テスト対象オブジェクトを生成
		JunitPractice1 practice = new JunitPractice1();

		//(2) 入力値、想定結果を設定
		int inputNum = 0;
		String expectesStr = "0が入力されました。";

		//(3) メソッドを呼び出して実測値を取得
		String outputStr = practice.inputNum(inputNum);

		//(4) 想定結果と実測値が等しいか確認
		assertEquals(expectesStr, outputStr);
	}

	@Test
	void testInputNum_002() {////メソッド名を連番にする
		//(1) テスト対象オブジェクトを生成
		JunitPractice1 practice = new JunitPractice1();
		
		//(2) 入力値、想定結果を設定
		int inputNum = -1;
		String expectesStr = "エラー!! 0 から 9 の数字ではありません。";
		
		//(3) メソッドを呼び出して実測値を取得
		String outputStr = practice.inputNum(inputNum);
		
		//(4) 想定結果と実測値が等しいか確認
		assertEquals(expectesStr, outputStr);
	}
	
}

カバレッジ

カバレッジとは

カバレッジ(テストカバレッジ)とは、プログラムコードのうちどの程度の割合がテストで確認できたかを示す指標、テストによる網羅率を意味します。
「カバレッジが高い」ことはテストケースが多くのプログラムをカバーしていると言えるので、テストの妥当性を測る1つの指標になります。
品質の観点で、プロジェクトの品質目標として「カバレッジ○%以上」などのように指標にしたり、進捗のモニタリングとしても活用されます。リスク低減の手段として、未テスト箇所のあぶり出し、プロジェクト内の重要機能ほどカバレッジを重視するといった使用がなされたりします。

とはいえ、実際にはカバレッジは必ず100%を目指さなければならないというわけではありません。現実的には、膨大なコード量を持つプロジェクトであるほど、テストプログラムの作成に傾倒しすぎるとコスト・工数がかさんでしまいます。そのため、カバレッジの扱いや運用ルールは、プロジェクトや現場ごとに異なります。
参考サイト:【保存版】JUnitカバレッジ100%達成への完全ガイド:効率的な測定と改善の7つの手順

カバレッジにはいくつかの種類があります。一部を紹介します。

  • ラインカバレッジ (Line Coverage): テストで実行されたコード行の割合。プログラムの各行について少なくとも1回実行されたかどうかを測定する。最も基本的で理解しやすいカバレッジ指標
  • ブランチカバレッジ (Branch Coverage): if文やwhile文などの分岐(ブランチ)のうち、実行された割合。
  • ステートメントカバレッジ (Statement Coverage): 命令網羅ともいい、条件分岐に応じて実行された文の数、その網羅率。
  • ファンクションカバレッジ (Function Coverage): テストで呼び出されたメソッドの割合。

実用的な使い方として、不要行の発見(コード品質の向上)、条件分岐に対しての網羅率の確認(不足しているテストケースの確認)に役立ちます。

カバレッジの確認方法

Eclipseでの カバレッジ取得・確認方法は以下
テストファイルを右クリック>カバレッジ>JUnitテスト

実行が完了すると、カバレッジビューが表示され、またテスト対象のファイルにマーカーが追加されます。

カバレッジビュー

プロジェクト名が表示されたら、ディレクトリ階層を開いていき、対象のファイルを探してください。(ここではsrc/com/cmps/unittest/JunitPractice1.java)
画像の下線部のように結果が確認できます。
また、下図はステイトメントカバレッジとして表示されている状態ですが、右上のメニューアイコンからカバレッジの種類を変更することでそれぞれの結果を確認することもできます。

テスト対象ファイルに追加されるマーカーの見方

マーカーの色の意味は下記の通り
・緑:実行された行
・黄:分岐に関して、一部完了しているが未実施のケースもある場合
・赤:実行されていない行

黄色に関しては、対象の行か左端のアイコンにカーソルを合わせると詳細を確認できます。
今回の場合 “1 of 4 branches missed.”と書いてあるので、4分岐のうち1つ実施できていない、と言った意味になります。
ちなみにここで実施していないと指摘されているのは、テストで確認していない「numが9より大きいケース」です。これをテストクラスに実装して再度カバレッジを取得すると緑色になります。

赤色の例として、例えば適当な分岐を足して実行すると(テスト未実施行があると)、下図のように表示されます。

このように網羅できていない分岐や命令文、メソッド等を確認することができます。

カバレッジのレポート出力

カバレッジはレポートとして保存して繰り返し見返すこともできます。

カバレッジビュー内で右クリック>セッションのエクスポート
出力したいセッション(実行したカバレッジ)を選択し、下図のようにHTMLレポートの書式で任意のローカルフォルダに保存します。

保存したファイルをChromeなどのブラウザで開くと、Eclipse上で取得した際と同等の内容をいつでも確認することができます。

確認が終了し、マーカーを消したい場合

カバレッジビューの「すべてのセッションを除去」を実行すると、マーカーを除去できます。

JUnit その他の機能

JUnitの基礎的な機能を抜粋して紹介します。

アノテーション

テストクラスで使用できるアノテーションを紹介します。
Assert (JUnit API)

@Test実行されるテストメソッド
@BeforeEach各テストメソッドが実行される前に毎回実行されるメソッド
@AfterEach各テストメソッドが実行された後に毎回実行されるメソッド
@BeforeAllテストクラス内の全てのテストメソッドが実行される前に実行される
@AfterAllテストクラス内の全てのテストメソッドが実行された後に実行される
@DisplayNameJUnitテストクラス・メソッドの表示名を変更できる
@Disabledテストメソッドを無効化する

上記のアノテーションのいくつかを使用したサンプルを以下に示します。

テスト対象
【JunitPractice2.java】

package com.cmps.unittest;

public class JunitPractice2 {

	/**
	 * 2つの整数の和を返す
	 * @param num1
	 * @param num2
	 * @return
	 */
	public int getSum(int num1, int num2) {

		return num1 + num2;
	}

	/**
	 * 整数の2乗の値を返す
	 * @param num
	 * @return
	 */
	public int getSquared(int num) {

		return num * num;
	}
}


テストクラス
【JunitPractice2Test.java】

package com.cmps.unittest;

import static org.junit.Assert.*;

import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;

@DisplayName("Junitサンプル②")
class JunitPractice2Test {

	static JunitPractice2 practice;
	static int cnt = 0;

	@BeforeEach
	void beforeEach() {
		//(1) テスト対象オブジェクトを生成
		//※BeforeEachはメソッドの前に都度実行されるので、共通処理であるインスタンス化を記載
		practice = new JunitPractice2();

		cnt++;
		System.out.println("テスト" + cnt + "を開始します。");
	}

	@AfterEach
	void afterEach() {
		System.out.println("テスト" + cnt + "を終了します。");
	}

	@Test
	@DisplayName("テスト1_和を求めるgetSum()")
	void testGetSum() {
		//(2) 入力値、想定結果を設定
		int inputNum1 = 1;
		int inputNum2 = 2;
		int expectesNum = 3;

		//(3) メソッドを呼び出して実測値を取得
		int actualNum = practice.getSum(inputNum1, inputNum2);

		//(4) 想定結果と実測値が等しいか確認
		assertEquals(expectesNum, actualNum);
	}

	@Test
	@DisplayName("テスト2_2乗を求めるgetSquared()")
	void testGetSquared() {
		//(2) 入力値、想定結果を設定
		int inputNum = 5;
		int expectesNum = 25;

		//(3) メソッドを呼び出して実測値を取得
		int actualNum = practice.getSquared(inputNum);

		//(4) 想定結果と実測値が等しいか確認
		assertEquals(expectesNum, actualNum);
	}

}


実行結果
@DisplayNameに書いた文字列が、JUnitの結果に表示名として使用されます。

コンソールには以下のように表示されます。
@BeforeEachと@AfterEachはテストメソッドの前後に毎回実行されるので、テストの回数分だけ出力されています。
(@BeforeEachと@AfterEachの実行確認のための出力なので、通常のテストケース作成時にはもちろん不要です。)

他にも、内部クラスを作成してネストしてまとめる@Nestアノテーションや、テストの実行順序を制御する@Orderアノテーションなどもあります。
参考サイト:JUnit5でネストしたテストと実行順序を指定する(@Nestedと@Order)

アサーションメソッド

サンプルの例で使用したassertEqualsメソッドは、アサーションメソッドです。
アサーション=「表明」、「断言」などの意味。プログラミングでは、あるコードが実行される時に満たされるべき条件を記述して実行時にチェックする仕組みのこと

assertEquals(Object expected, Object actual)expected と actual が等しいことを検証する
assertTrue(boolean condition)
assertFalse(boolean condition)
condition が true であることを検証する
condition が false であることを検証する
assertNull(Object object)
assertNotNull(Object object)
Object が null であることを検証する
object が null でないことを検証する
assertThrows(Class<T> expectedThrowable, ThrowingRunnable runnable)処理runnableが例外expectedThrowableを投げることを検証する

参考サイト:JUnitで例外の発生を検証する – Qiita

練習問題

問1:以下の「JUnitQuestion」クラスに対し、「分岐網羅」でテストを作成してください。
テスト対象の下記クラスは【src/com/cmps/unittest】に作成し、テストクラスは【src.test/com/cmps/unittest】に作成してください。

package com.cmps.unittest;

public class JUnitQuestion {

	/**
	 * 年齢に応じた入場料金に関するメッセージを返す
	 * @param age String 年齢
	 * @return message String
	 */
	public String getEntranceFee(int age) {
		String message = null;
		
		if(age < 0) {
			message = "無効な値です。";
		} else if (age <= 5) {
			message = "無料!";
		} else {
			message = "1000円です。";	
		}
		
		return message;
	}
}
タイトルとURLをコピーしました