1
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

【新人向け】JUnitの紹介

Posted at

これは、前々から新人教育用にJUnitを紹介したいなぁと思っていたのですが、草稿を書いていたらちょっとしたボリュームになってしまったために断念した残念な記事です。このまま捨てるのももったいないので会社も夏休みですし、新人向けに。


#JUnitとは
JUnitは一般的にユニットテストと呼ばれているところで使われるツールです。
JUnitは使い方と検証を書いたメソッドを持ったクラスを作っておくことで、そのメソッドを好きなタイミングで実行出来るソフトです。
JUnitを実行する際はJUnitの流儀に従ったクラスやメソッドの書き方をします。

JUnitの流れ

JUnitの流れ(概論)

JUnitの基本的な流れは

  • 「その機能の使い方」を基に実行
  • 「その機能を使った結果」を検証

になります。

JUnitの流れ(簡単なサンプル)

例えば、買い物かごクラス(Cart)があったとします。この中に、買い物かごの中に入っている商品全ての価格と個数を反映した合計金額を取得するgetSum()というメソッドがあったとします。
使い方はこんな感じです。

Cart cart = new Cart();
cart.add(商品, 個数);

int result = cart.getSum(); // cartの中の現在の合計金額が取得できる
assertEquals(なっているはずの金額, result); // 取得した金額が想定した金額かどうか確認

assertEqualsが検証の部分になります。
ここで引数同士が違っていればテスト処理はそこで終了します。
テストもfailedとなります。assertEqualsをパスし、最後までいくとテストはsuccessfulになります。

なお、assertEqualsは複数書くことができます。例えば、コンストラクタのテストで複数のパラメータの設定をして正しく設定できたか確認などする場合は次のようになります。

Product apple = new Product("リンゴ", 400); // appleという変数にリンゴという商品名と400円を設定している

assertEquals("リンゴ", apple.getName());  // 名前が正しく設定出来たか確認
assertEquals(400, apple.getPrice()); // 価格が正しく設定できたか確認

みたいな感じです。2つのassertEqualsをパスすればテストは成功となります。

JUnitでのテスト

テスト対象のクラス

下記のクラスはテスト対象となるクラスです

Product.java
public class Product {
    private String productId; // 商品番号
    private String productName; // 商品名
    private Integer price; // 価格

    // getter
    public String getProductId() { return productId; }
    public String getProductName() { return productName; }
    public Integer getPrice() { return price; }
    
    // コンストラクタ
    public Product(String 商品番号, String 商品名, Integer 価格) { /* 上記のプロパティを設定 */ }

}
Cart.Java
public class Cart {
  // プロパティ
  private List<Product> list; // 入っている商品とその個数

  // メソッド
  /** 
   * 商品を追加する処理を行う
   */
  public void add(Product 商品) { /* 略 */ }

  /**
   * 入っている商品とその個数を返す処理
   */
  public List<Product> getList() { /* 略 */ }
  /**
   * 入っている商品の合計を返す処理
   */
  public int getSum() { /* 略 */ }
}
ちなみに全体像は次の通りになります。
Product.java
public class Product {
	private String productId; // 商品番号
	private String productName; // 商品名
	private Integer price; // 価格

	public Product(String productId, String productName, Integer price) {
		this.productId = productId;
		this.productName = productName;
		this.price = price;
	}

	public String getProductId() {
		return productId;
	}

	public String getProductName() {
		return productName;
	}

	public Integer getPrice() {
		return price;
	}
}
Cart.java
import java.util.List;
import java.util.ArrayList;

public class Cart {
	// プロパティ
	private List<Product> list; // 入っている商品とその個数

	public Cart() {
		this.list = new ArrayList<Product>();
	}
	// メソッド
	/**
	* 商品を追加する処理を行う
	*/
	public void add(Product product) {
		this.list.add(product);
	}

	/**
	 * 入っている商品とその個数を返す処理
	 */
	public List<Product> getList() {
		return this.list;
	}
	/**
	 * 入っている商品の合計を返す処理
	 */
	public int getSum() {
		int sum = 0;
		for (Product p: this.list) {
			sum += p.getPrice();
		}
		return sum;
	}
}

テストの方法(概要編)

ここではaddのテストをしたいと思います。その場合、次の流れを考えました

使い方
0. カートに入れる商品を作成する

  1. カートを作成する
  2. カートに商品を追加する

上記の使い方でaddが正しいかどうかの検証は次のように考えました( 注:これは一例です。必ずしも同じようなテストで下記のようなテストを行う必要はありません )
0. カートから一覧を取り出す(これは前準備でテストに入りません)

  1. カートに入っている一覧の個数が追加した個数と同じか確認する
  2. カートに入っている商品が正しいものか順に確認する

テストの方法(擬似コード編)

上記のテストコードは次のようなものでしょう

// 0. カートに入れる商品を作成する
Product apple = new Product("A001", "リンゴ", 158);
Product orange = new Product("A002", "オレンジ", 258);

// 1. カートを作成する
Cart cart = new Cart();
// カートに商品(リンゴとオレンジ)を追加する
cart.add(apple);
cart.add(orange);

// ここから検証
// 0. カートから一覧を取り出す
List<Product> list = cart.getList(); // カートに入っているリストを取得する

// 1. カートに入っている一覧の個数が追加した個数(2個)と同じか確認する
assertEquals(2, list.size());

// 2. カートに入っている商品が正しいものか順に確認する
Product item;
// 2.1 1つめの商品がappleで定義したものと同じか確認
item= list.get(0);  // カートの1つ目の商品を取り出し
assertEquals("A001", item.getProductId()); // 取り出した商品の商品IDの検証
assertEquals("リンゴ", item.getProductName()); // 取り出した商品の商品名の検証
assertEquals((Integer)158, item.getPrice()); // 取り出した商品の商品名の検証

// 2.1 2つめの商品がorangeで定義したものと同じか確認
item = list.get(1); // カートの2つ目の商品を取り出し検証を開始する
assertEquals("A002", item.getProductId()); // 取り出した商品の商品IDの検証
assertEquals("オレンジ", item.getProductName()); // 取り出した商品の商品名の検証
assertEquals((Integer)258, item.getPrice()); // 取り出した商品の商品名の検証

ちなみに、 "A001" などで書いた部分は apple.getProductName() としても問題ないです。

テストの方法(擬似コード補足編)

最初に書きました通り、JUnitの流れは

  • 「その機能の使い方」を基に実行
  • 「その機能を使った結果」を検証

が基本です。その検証は assertEquals などの assert で始まるメソットで検証されます。
そして、**assertEqualsは両方の引数が同じでない場合、そこでテスト失敗(failure)**となり、以降の処理は行われません

例えば
assertEquals("リンゴ", item.getProductName());
"リンゴ""りんご" になっていれば、failureとなり、以降の
assertEquals((Integer)158, item.getPrice());
などは実施されません。

そしてassertEqualsを全て抜けて最後まで処理が進むと成功(success)となります。

なお、assertXXX(expect, actual) は正しい値、テストする値の順 に書きましょう。

JUnitでのテストの方法(Eclipse編)

Eclipse編としていますが、書いている元はpleiadesです。
ちなみにこれだけでで力尽きました・・・。もし気力があればいつかVSCodeなどは追加します。
EclipseでJUnitを行う場合、次のようにします。

プロジェクトフォルダの下にtestフォルダがなければ作成します。

「ファイル」メニュー→「新規」→「ソースフォルダー」を選択します。

「ファイル」メニュー→「新規」→「ソースフォルダー」を選択します

次に表示される画面の「フォルダー名:」に「test」と入力し「完了」をクリックします。

表示される画面の「フォルダー名:」に「test」と入力し「完了」をクリックします

先ほど確認または作成したtestフォルダを右クリックし、「新規」→「その他」を選択します。

testフォルダを右クリックし、「新規」→「その他」を選択

新規の画面が表示されますので「Java」→「JUnit」→「JUnit テスト・ケース」を選択し、「次へ」を選択します。
もしくは新規の画面の上部のウィザードフィールドに「junit」と入力します。残ったものの中から「JUnit テスト・ケース」を選択し、「次へ」を選択します。

ウィザード画面が表示されますので「junit」と入力。JUnit テスト・ケースを選択する

次はJUnit3か、4か5によって変わります。新人教育でなんのしがらみもない場合は4か5を使いましょう。配属先が3という残念な状況ならば3を選択してください。なお、残念ながら5については4で包括させていただきます。ちょっと、時間が足りなかったので・・・

JUnit3のテストケースを作成する

JUnit3のテストケースを作成します

1.「新規 JUnit 3テスト」を選択します
2. 名前にテストケースを書くクラスの名前を入力します(赤線部)。「テスト対象Test」が良いと思われます。
3. パッケージ名(青線部)はテストのクラスの対象と同じパッケージ(テスト対象Javaプログラムの最初の方に書いてある「package XXX;」の部分)を指定します。
4. 「完了」をクリックします。

パッケージ名と名前を書くクラスの名前を入力します。入力したら「完了」をクリックします

JUnit3用のライブラリが最初はありませんので、追加するかどうか聞かれますので「次のアクションを実行」を選択し、「JUnit 3 ライブラリーをビルド・パスに追加」を選択し、OKを押します。

「次のアクションを実行」を選択し、「JUnit 3 ライブラリーをビルド・パスに追加」を選択し、OKを押します
作成されたJUnitテストケースの実体は単なるJavaのプログラムファイルです。今回は次のように記述します。
CartTest.java
package myStore;

import junit.framework.TestCase;

import java.util.List;

public class CartTest extends TestCase {
	public void testカートに追加できたか確認する() {
				// テストに必要な投入する商品は先に作っておく
		Product apple = new Product("A001", "リンゴ", 158);
		Product orange = new Product("A002", "オレンジ", 258);

		//カートを作成
		Cart cart = new Cart();
		// リンゴとオレンジを追加する
		cart.add(apple);
		cart.add(orange);
		
		// ここから検証
		List<Product> list = cart.getList(); // カートに入っているリストを取得する
		
		// カートの中身が2個か検証する
		assertEquals(2, list.size());
		
		// カートの中身が正しいか確認する
		Product item;
		item= list.get(0);  // カートの1つ目の商品を取り出し検証を開始する
		assertEquals("カートの1つ目の商品IDがA001であるかの検証", "A001", item.getProductId());
		assertEquals("カートの1つ目の商品名がリンゴであるかの検証", "リンゴ", item.getProductName());
		assertEquals("カートの1つ目の価格が158であるかの検証", (Integer)158, item.getPrice());
		
		item = list.get(1); // カートの2つ目の商品を取り出し検証を開始する
		assertEquals("カートの1つ目の商品IDがA002であるかの検証", "A002", item.getProductId());
		assertEquals("カートの1つ目の商品名がオレンジであるかの検証", "オレンジ", item.getProductName());
		assertEquals("カートの1つ目の価格が258であるかの検証", (Integer)258, item.getPrice());
	}
}

JUnit3のお約束は次の通りです。

  • import junit.framework.TestCase; を書く
  • テスト用のクラスはextends TestCaseでTestCaseクラスを継承する
  • テスト用のメソッドの修飾子は public void で名前は test で始めること

以上のルールを守って作成します。テスト用のメソッドはtestから始めれば何個でも作ることができますので、あとはいろんなパターンのテストを作っていってください。
また、メソッド名に日本語が入っていますが、これはJavaにとって問題はありません(仕様的に問題なし)。JUnitでは何のテストをしているか分かりやすいように日本語が使われることも多いです1testNo1と書くよりもこちらの方が分かりやすいのではないでしょうか。

またassertEqualsの引数の1つ目に文字列が入っているのに気がついたと思いますが、これはコメントだと思ってください。これを残しておくとJUnitでテストしたときに、この第一引数がメッセージとして表示され、何を意図したテストで失敗したのかが分かります。

JUnit4のテストケースを作成する

JUnit4のテストケースを作成します
JUnit4のテストケースを作成します
1.「新規 JUnit 4 テスト」を選択します 2. 名前にテストケースを書くクラスの名前を入力します(赤線部)。「テスト対象Test」が良いと思われます。 3. パッケージ名(青線部)はテストのクラスの対象と同じパッケージ(テスト対象Javaプログラムの最初の方に書いてある「package XXX;」の部分)を指定します。 4. 「完了」をクリックします。 パッケージ名と名前を書くクラスの名前を入力します。入力したら「完了」をクリックします

JUnit4用のライブラリが最初はありませんので、追加するかどうか聞かれますので「次のアクションを実行」を選択し、「JUnit 4 ライブラリーをビルド・パスに追加」を選択し、OKを押します。

「次のアクションを実行」を選択し、「JUnit 4 ライブラリーをビルド・パスに追加」を選択し、OKを押します
作成されたJUnitテストケースの実体は単なるJavaのプログラムファイルです。今回は次のように記述します。
CartTest2.java
package myStore;

import static org.hamcrest.CoreMatchers.*;
import static org.junit.Assert.*;
import java.util.List;
import org.junit.Test;

public class CartTest {
	@Test
	public void カートに追加できたか確認する() {
		// テストに必要な投入する商品は先に作っておく
		Product apple = new Product("A001", "リンゴ", 158);
		Product orange = new Product("A002", "オレンジ", 258);

		//カートを作成
		Cart cart = new Cart();
		// リンゴとオレンジを追加する
		cart.add(apple);
		cart.add(orange);

		// ここから検証
		List<Product> list = cart.getList(); // カートに入っているリストを取得する

		// カートの中身が2個か検証する
		assertEquals(2, list.size());

		// カートの中身が正しいか確認する
		Product item;
		item= list.get(0);  // カートの1つ目の商品を取り出し
		assertThat(item.getProductId(), is("A001")); // 取り出した商品の商品IDの検証
		assertThat(item.getProductName(), is("リンゴ")); // 取り出した商品の商品名の検証
		assertThat(item.getPrice(), is(158)); // 取り出した商品の商品名の検証

		item = list.get(1); // カートの2つ目の商品を取り出し検証を開始する
		assertThat(item.getProductId(), is("A002")); // 取り出した商品の商品IDの検証
		assertThat(item.getProductName(), is("オレンジ")); // 取り出した商品の商品名の検証
		assertThat(item.getPrice(), is(258)); // 取り出した商品の商品名の検証
	}
}

JUnit4のお約束は次の通りです。

  • 次を最初に書く
    • import static org.hamcrest.CoreMatchers.*;
    • import static org.junit.Assert.*;
    • import org.junit.Test;
  • テスト用のメソッドの修飾子は public void
  • テスト用のメソッドの前に@Testを書くこと
  • 検証はassertThat( 検証対象, is( 望む値 ) )のように書くこと

以上のルールを守って作成します。テスト用のメソッドは@Testを付ければ始めれば何個でも作ることができますので、あとはいろんなパターンのテストを作っていってください。
また、メソッド名に日本語が入っていますが、これはJavaにとって問題はありません(仕様的に問題なし)。JUnitでは何のテストをしているか分かりやすいように日本語が使われることも多いです1testNo1と書くよりもこちらの方が分かりやすいのではないでしょうか。

またassertEqualsの引数の1つ目に文字列が入っているのに気がついたと思いますが、これはコメントだと思ってください。これを残しておくとJUnitでテストしたときに、この第一引数がメッセージとして表示され、何を意図したテストで失敗したのかが分かります。

蛇足ですがJUnit 3とほぼ同様のコードも書けます
CartTest.java
package myStore;

import static org.junit.Assert.*;
import org.junit.Test;
import java.util.List;

public class CartTest {
	@Test
	public void カートに追加できたか確認する() {
		// テストに必要な投入する商品は先に作っておく
		Product apple = new Product("A001", "リンゴ", 158);
		Product orange = new Product("A002", "オレンジ", 258);

		//カートを作成
		Cart cart = new Cart();
		// リンゴとオレンジを追加する
		cart.add(apple);
		cart.add(orange);

		// ここから検証
		List<Product> list = cart.getList(); // カートに入っているリストを取得する

		// カートの中身が2個か検証する
		assertEquals(2, list.size());

		// カートの中身が正しいか確認する
		Product item;
		item= list.get(0);  // カートの1つ目の商品を取り出し検証を開始する
		assertEquals("カートの1つ目の商品IDがA001であるかの検証", "A001", item.getProductId());
		assertEquals("カートの1つ目の商品名がリンゴであるかの検証", "リンゴ", item.getProductName());
		assertEquals("カートの1つ目の価格が158であるかの検証", (Integer)158, item.getPrice());

		item = list.get(1); // カートの2つ目の商品を取り出し検証を開始する
		assertEquals("カートの2つ目の商品IDがA002であるかの検証", "A002", item.getProductId());
		assertEquals("カートの2つ目の商品名がオレンジであるかの検証", "オレンジ", item.getProductName());
		assertEquals("カートの2つ目の価格が 258であるかの検証", (Integer)258, item.getPrice());
	}
}

JUnit4の書き方について

序盤でassertEqualsを使ってしまったため、ここで補講です。
JUnit4では次のような書き方ができるようになりました。これからはこれがメインになります。

assertThat( target, is("target") );

assertEqualsとほぼやっていることが変わらないのですが、左から右に読めるようになったという利点が大きいです。assert that target is "target"(targetは"target"であると検証する)って感じです。
他にもこんな感じで書けます。isnotはMatcherと呼ばれるものです。

assertThat( target, is( not( "not target") ) )

参考:
Java Master - JUnit 入門基本的な使い方
JUnitの使い方(初級)

JUnitの実行

作ったテストケースを実行します。「実行」メニュー→「実行」→「JUnitテスト」を選択します。

スクリーンショット 2020-08-10 11.27.02.png

すると画面のどこかに下記のような画面が出てきます。

出てこない場合は「ウィンドウ」メニュー→「ビューの表示」→「その他」を選択し「Java」→「JUnit」(赤線部)を選択してください。
「ウィンドウ」メニュー→「ビューの表示」→「その他」を選択 「Java」→「JUnit」(赤線部)を選択
JUnitの実行画面

ここでmyStore.CartTestの左の三角形をクリックしてみましょう。下記のように何のテストをしたのかが分かります。なお、成功したテストは緑(Green)で表示され、失敗したテストは赤(Red)で表示されます。
クラス内で実行したテストを表示

ちなみに、ここで、テストプログラムの28行目の"A001"を"A003"に変更して再度実行してみましょう。先ほど同じように右クリックで実行しても良いですが、JUnitビューがある場合は「テストの再実行」(赤線部)を押すと再実行可能です。
「テストの再実行」(赤線部)を押すと再実行可能

なお、"A001"を"A003"にしたことでファイルが変更されましたので保存するか確認するダイアログが出てきますので問題なければ「OK」を押します。ファイル名の横のチェックを外すとそのファイルが保存前状態でのプログラムが実行されます。
保存して起動ダイアログが表示されたら「OK」を押す

すると、今度はアイコンの左下に群青色のバツが付きます。これが失敗の印です。
そして、失敗したテストをクリックすると、テストケースのどこで失敗したか表示されます。これをダブルクリックすると対象の行が表示されます。
失敗したテストをクリックすると、テストケースのどこで失敗したか表示されます。これをダブルクリックすると対象の行が表示されます。

また、障害トレースのjunit.framework.ComparisionFailure:の後ろ側を見るとエラーの詳細に「カートの1つめの商品IDがA001であるかの確認があること」の記述が確認できると思います。
また「Expected: is」に正しい値、「but: was」に実際の値(実行の結果)が表示されます。
エラーの詳細にカートの1つめの商品IDがA001であるかの確認があることが確認できる

さて、ながったらしい話になりましたが以上になります。


コラム(蛇足) テストとは?品質とは?何をどう検査するか?

単なるポエムです

JUnitは実行と検証の二つが大事だと書きました。このうち、検証について触れておこうと思います。
ここは超ややこしいので読み飛ばしてもOKです。
検証は実行の結果の値が期待する結果と同じかどうかを確認することですが、「何を」検証するかについては意見の分かれるところです。明確な正解があるのかも知れませんが、私は知りません(@t_wada さん辺りは知ってそうですが・・・)。

例えば、下記のようなクラスがあったとします。

SomeClass.java
class SomeClass {
  private String somethingvalue = "";
  public void setSomethingValue(String somethingvalue) {
    this.somethingvalue = somethingvalue;
  }
}

このクラスは将来の拡張のため、データを保持する機能だけしかありません。保持するデータsomethingvalueを設定するsetSomethingValueがあります。ただし、 valueの値を取得するgetSomethingValueはありません。このsetSomethingValueのテストはどうするべきでしょう?
私的な回答としては「プロジェクトの指針や仕様書に拠る」です。
先に言っておくとprivateな変数(プロパティ)であろうと、Javaのリフレクションという機能を使えばvalueの値は分かります。 2
その上でプロジェクトや仕様書について

  • プロジェクトや仕様書に「valueに設定する」と書かれるとテスト対象になる可能性が高いと思います
  • 「privateな変数は確認しない」「クラスが公開しているプロパティやメソッドで確認する」という方針ならばテスト対象外となります(valueにアクセスする方法がリフレクション以外にないのですから)

私的には後者の視点を採用したいところですが、この場合、いささか厄介な問題があります。下記のような値を指定された回数分繰り返し表示するメソッドを追加した場合

public String repeatSpeak(int times) {
  String result = "";
  for (int i=0; i < times; i++) {
    result += this.value;
  }
  return result;
}

このrepeatSpeakは公開された(=誰かが使う前提の)メソッドですからテスト対象になります。このとき、value によって値が変わります。なので、valueを変更するためにsetValueを使うテストも書くことになります。
このように直接的ではなく、間接的にテストすることもあり得るのではないかと思います。

こう言った「何を」「どのように」「誰が」と言った視点を元にプロジェクトで統一した視点を持ち、テストすることがチーム開発では必要ですので配属先などでどのようにテストするか聞きましょう。決まっていなければ開発の最初にチームのメンバーで統一した視点を共有できるようにしましょう。

  1. と言っても、半角スペースが入っていればエラーになりますので、一言でテストが表せるような言葉を選びましょう 2

  2. リフレクションについては次などを参照 :Samurai blog - 【Java入門】リフレクションでメソッドの実行、フィールドの変更ひしだま's 技術メモページ - リフレクション

1
4
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?