TestNGを使ってみる

ここ数日はテスト支援ツールについて調べていたのですが、タイミングよく片貝さんNetBeans TestNG プラグインを紹介されていたので、試してみます。
TestNG は、Java 用のテスティングフレームワークです。ちなみに、@IT によると、 「Testing, the Next Generation」 の略だそうです。


環境

プラグインのインストール

NetBeans Wiki: TestNG からプラグインをダウンロードします。
Generic + Ant support: o.n.m.testng.nbm をダウンロードしました。


NetBeansプラグインメニューよりダウンロードファイルを指定してインストールします。


テストを生成する

テストのターゲットクラスを準備

あとで遊べるよう、バグも仕込んでおきましょうね。


package examples;
public class Example1ForTestNG {
public boolean isPassed(int point) {
// 本当は60点以上で合格だけど、イコール入れるの忘れた!
if (point > 60) {
return true;
} else {
return false;
}
}
}


テストを生成

プロジェクトペインでクラスを選んでコンテキストメニューを表示するか、ソース上でコンテキストメニューを表示するかして、CreateTest。


テストクラス名を指定するダイアログがでて、テストクラスが生成された!


生成されたテストクラス


package examples;

import org.testng.annotations.AfterClass;
import org.testng.annotations.BeforeClass;
import org.testng.annotations.Test;

public class Example1ForTestNGTest {

@BeforeClass
public void setUp() {
// code that will be invoked before this test starts
}

@Test
public void aTest() {
System.out.println("Test");
}

@AfterClass
public void cleanUp() {
// code that will be invoked after this test ends
}
}


ひとりごと:ひそかに裏切られた期待
  • なんとなく、以下を妄想して一人でワクワクしていたのだけど、そんな虫のいい話はありませんね。それに、後で試すデータ駆動の機能を考えると、もしできたとしても要らないかも。
  • テストクラスのコンテキストメニューでも、create test class が有効でした。テストクラスのテストクラスも生成できてしまうのですね。
  • ターゲットクラスにメソッドを追加してもう一度 create test class (テストクラス名は変更しなかった)してみたら、同じクラスがもうひとファイル生成されました。クラスと基本的なひな形の生成だから、確かにこの動作になりますね。それに、折角定義したクラスが上書きされると困るので、妥当な動作ですね。
というわけで、

テストクラスと、基本的なメソッドが用意されたので、テストを記述してみましょう。


テストを記述する

こんな感じでテストを書いてみました。本当はテストファーストだよね・・・というツッコミは気にせず突き進みます。
あ、テストファーストを考えると、カバレッジに応じてテストを生成するなんて、ありえないですね。
境界値分析して、テストケースは、引数に 59、60、61 を渡す 3 ケースを実施します。ちなみに、testIsPassed2 のケース(引数 60 )はバグを仕込んであります。


import static org.testng.Assert.assertEquals;
(略)
@Test
public void testIsPassed1() {
Example1ForTestNG target = new Example1ForTestNG();
assertEquals(target.isPassed(59), false);
}

@Test
public void testIsPassed2() {
Example1ForTestNG target = new Example1ForTestNG();
assertEquals(target.isPassed(60), true);
}

@Test
public void testIsPassed3() {
Example1ForTestNG target = new Example1ForTestNG();
assertEquals(target.isPassed(61), true);
}

テストを実行する

実行したら fail

テストクラスを選んでコンテキストメニューから「Run Test Class」
プロジェクトを選んで「テスト」でも OK


テスト結果が表示されました。testIsPassed2 は fail ですね。


スタックトレースを確認すると、アサーションに失敗していることがわかります。


修正してテスト失敗分だけ再実行する

ソースを修正して、再実行。コンテキストメニューをみると、"Re-run failed tests" が有効になってます。
左は、テストクラスのコンテキストメニューで、右はテストクラスのソースのコンテキストメニュー。ちなみに、プロジェクトのコンテキストメニューには、再実行はありません。


というわけで、さっき失敗したテストのみが実行され、無事 pass しました。







ここまでは、まあ、JUnit ととりたてて変わるところがないのですが、さてさて、ここからが TestNG の面白いところですよ!


例外を判定する

TestNG では、例外のテストの書き方がシンプルです。
準備として、テストのターゲットクラスを変え、引数がマイナスの場合には RuntimeException を発行するようにし、テストケースを追加しました。

例外が発生して、テストは成功するケース

テスト実行時に RuntimeException が発生したら成功、というテストを追加しました。
テストを追加し、RunTestMethod で対象のテストメソッドだけ実行します。


@Test (expectedExceptions = { java.lang.RuntimeException.class })
public void testIsPassed0() {
Example1ForTestNG target = new Example1ForTestNG();
target.isPassed(-1);
}


とてもシンプルですね。

例外が発生せず、テストが失敗するケース

例外が発生せず、失敗するテストを定義してみました。
マイナスの場合にはターゲットクラスは RuntimeException を発行しますが、0 の場合は例外は発生しません。
なので、以下のテストは失敗します。


@Test (expectedExceptions = { java.lang.RuntimeException.class })
public void testIsPassed01() {
Example1ForTestNG target = new Example1ForTestNG();
target.isPassed(0);
}


失敗するとこんな感じにスタックトレースが表示されます。失敗の理由は、Exception が発生しなかったから、ということがわかりますね。


データ駆動する

テストの入力値のバリエーションを増やしたいためにテストをじゃかじゃか追加する必要がありません。
データと期待値といった値のセットを定義し、テストに対して次々とパラメータを変えて実行できます。
これはすごく便利ですね。このためだけでも、TestNG を選ぶ理由があります。

例外が発生しないテスト

入力値と期待結果を3セット定義しました。上のほうでやった境界値の3ケースです。
DataProvider につけた名前 "firstProvider" をテストメソッドに指定します。テストの引数は、DataProvider で定義した {} に対応します。


import org.testng.annotations.DataProvider;

(略)

@DataProvider(name = "firstProvider")
public Object[][] data() {
return new Object[][] {
{ 59, false }, { 60, true }, { 61, true } };
}

@Test (dataProvider = "firstProvider")
public void testIsPassedDataProvider(int in, boolean expected) {
Example1ForTestNG target = new Example1ForTestNG();
assertEquals(target.isPassed(in), expected);
}


結果はこうです。


失敗したテストがある場合の表示はこうです。{60, false} のケースの Assertion が失敗したことが分かりますね。


PASSED: testIsPassedDataProvider(59, false)
PASSED: testIsPassedDataProvider(61, true)
FAILED: testIsPassedDataProvider(60, false)
java.lang.AssertionError: expected: but was:
at examples.Example1ForTestNGTest.testIsPassedDataProvider(Example1ForTestNGTest.java:34)
... Removed 27 stack frames

===============================================
TestNGTest
Tests run: 3, Failures: 1, Skips: 0
===============================================


例外が発生する場合のテスト

データプロバイダと、期待する例外クラスの両方を指定してみました。


@DataProvider(name = "secondProvider")
public Object[][] data2() {
return new Object[][] {
{ -1, false }, { -1000, false } };
}

@Test (dataProvider = "secondProvider", expectedExceptions = { java.lang.RuntimeException.class })
public void testIsPassedDataProvider2(int in, boolean expected) {
Example1ForTestNG target = new Example1ForTestNG();
target.isPassed(in);
}



結果です。


Creating D:\aaa\NetBeans\projects6.7.1\TestNGTest\build\test\results\Custom suite\TestNGTest.html
PASSED: testIsPassedDataProvider2(-1, false)
PASSED: testIsPassedDataProvider2(-1000, false)

===============================================
TestNGTest
Tests run: 2, Failures: 0, Skips: 0
===============================================


なんだか、嬉しいですね。テストが楽になりますね。


テストのタイムアウトを設定する

無限ループなど、リソースの無駄遣いは困りますね。タイムアウトを設定してみましょう。


@Test (timeOut=1)
public void testTimeout() {
int cnt = 0;
while(true){
System.out.println(cnt++);
}
}


無限ループしてしまいましたが、テストは無事タイムアウトしてくれました。


ここの説明みるとミリ秒単位のようですが、実際には 1 と指定したのに、94 ミリ秒実行しています。タイムアウトしたかどうかをチェックする間隔が広いのかもしれません。よくわかりません。


ほかにも

単純に便利だと思う機能

気になっていた機能だけ試してみましたが、ほかにもいろいろできるようですね。

  • パラメータを渡せる
  • 依存関係を定義できる
    • あるテストが失敗した場合に、他のテストを実行しても仕方ない場合があります。そういった依存関係が定義できるようです
  • テストスイートを XML で指定できる
  • テストの前後に実行する処理のタイミングをいろいろ指定できるようです。いろいろなアノテーションがありますね
    • @BeforeSuite
    • @AfterSuite
    • @BeforeTest
    • @AfterTest
    • @BeforeGroups
    • @AfterGroups
    • @BeforeClass
    • @AfterClass
    • @BeforeMethod
    • @AfterMethod
  • ロギングやレポートのバリエーションがあるようです
使いこなしに注意が必要だと思う機能
  • テストごとに別のインスタンスを作らない?
    • インスタンスが使いまわされるのであれば、それを考慮したテストの実装が必要ですね
    • ん??これは私の勘違いかもしれません
  • テストの並列実行が可能
    • 並行実行の単位を適切に指定する必要がありますね

蛇足

ネーミングは2重の意味でナンセンスで面白いですね。
1)正しさを確認するツールなのに NG とか、どうよ?
2)5年くらい後に、「過去の "the Next Generation"」と呼ばれることになるのはどうよ?