これは、前々から新人教育用に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でのテスト
テスト対象のクラス
下記のクラスはテスト対象となるクラスです。
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 価格) { /* 上記のプロパティを設定 */ }
}
public class Cart {
// プロパティ
private List<Product> list; // 入っている商品とその個数
// メソッド
/**
* 商品を追加する処理を行う
*/
public void add(Product 商品) { /* 略 */ }
/**
* 入っている商品とその個数を返す処理
*/
public List<Product> getList() { /* 略 */ }
/**
* 入っている商品の合計を返す処理
*/
public int getSum() { /* 略 */ }
}
ちなみに全体像は次の通りになります。
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;
}
}
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. カートに入れる商品を作成する
- カートを作成する
- カートに商品を追加する
上記の使い方でaddが正しいかどうかの検証は次のように考えました( 注:これは一例です。必ずしも同じようなテストで下記のようなテストを行う必要はありません )
0. カートから一覧を取り出す(これは前準備でテストに入りません)
- カートに入っている一覧の個数が追加した個数と同じか確認する
- カートに入っている商品が正しいものか順に確認する
テストの方法(擬似コード編)
上記のテストコードは次のようなものでしょう
// 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フォルダを右クリックし、「新規」→「その他」を選択します。
新規の画面が表示されますので「Java」→「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テストケースの実体は単なる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では何のテストをしているか分かりやすいように日本語が使われることも多いです1。testNo1
と書くよりもこちらの方が分かりやすいのではないでしょうか。
またassertEqualsの引数の1つ目に文字列が入っているのに気がついたと思いますが、これはコメントだと思ってください。これを残しておくとJUnitでテストしたときに、この第一引数がメッセージとして表示され、何を意図したテストで失敗したのかが分かります。
JUnit4のテストケースを作成する
JUnit4のテストケースを作成します
JUnit4のテストケースを作成します
作成されたJUnitテストケースの実体は単なる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では何のテストをしているか分かりやすいように日本語が使われることも多いです1。testNo1
と書くよりもこちらの方が分かりやすいのではないでしょうか。
またassertEqualsの引数の1つ目に文字列が入っているのに気がついたと思いますが、これはコメントだと思ってください。これを残しておくとJUnitでテストしたときに、この第一引数がメッセージとして表示され、何を意図したテストで失敗したのかが分かります。
蛇足ですがJUnit 3とほぼ同様のコードも書けます
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"であると検証する)って感じです。
他にもこんな感じで書けます。is
やnot
はMatcherと呼ばれるものです。
assertThat( target, is( not( "not target") ) )
JUnitの実行
作ったテストケースを実行します。「実行」メニュー→「実行」→「JUnitテスト」を選択します。
すると画面のどこかに下記のような画面が出てきます。
ここでmyStore.CartTestの左の三角形をクリックしてみましょう。下記のように何のテストをしたのかが分かります。なお、成功したテストは緑(Green)で表示され、失敗したテストは赤(Red)で表示されます。
ちなみに、ここで、テストプログラムの28行目の"A001"を"A003"に変更して再度実行してみましょう。先ほど同じように右クリックで実行しても良いですが、JUnitビューがある場合は「テストの再実行」(赤線部)を押すと再実行可能です。
なお、"A001"を"A003"にしたことでファイルが変更されましたので保存するか確認するダイアログが出てきますので問題なければ「OK」を押します。ファイル名の横のチェックを外すとそのファイルが保存前状態でのプログラムが実行されます。
すると、今度はアイコンの左下に群青色のバツが付きます。これが失敗の印です。
そして、失敗したテストをクリックすると、テストケースのどこで失敗したか表示されます。これをダブルクリックすると対象の行が表示されます。
また、障害トレースのjunit.framework.ComparisionFailure:
の後ろ側を見るとエラーの詳細に「カートの1つめの商品IDがA001であるかの確認があること」の記述が確認できると思います。
また「Expected: is」に正しい値、「but: was」に実際の値(実行の結果)が表示されます。
さて、ながったらしい話になりましたが以上になります。
コラム(蛇足) テストとは?品質とは?何をどう検査するか?
単なるポエムです
JUnitは実行と検証の二つが大事だと書きました。このうち、検証について触れておこうと思います。
ここは超ややこしいので読み飛ばしてもOKです。
検証は実行の結果の値が期待する結果と同じかどうかを確認することですが、「何を」検証するかについては意見の分かれるところです。明確な正解があるのかも知れませんが、私は知りません(@t_wada さん辺りは知ってそうですが・・・)。
例えば、下記のようなクラスがあったとします。
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
を使うテストも書くことになります。
このように直接的ではなく、間接的にテストすることもあり得るのではないかと思います。
こう言った「何を」「どのように」「誰が」と言った視点を元にプロジェクトで統一した視点を持ち、テストすることがチーム開発では必要ですので配属先などでどのようにテストするか聞きましょう。決まっていなければ開発の最初にチームのメンバーで統一した視点を共有できるようにしましょう。
-
リフレクションについては次などを参照 :Samurai blog - 【Java入門】リフレクションでメソッドの実行、フィールドの変更、ひしだま's 技術メモページ - リフレクション、 ↩