|▽ ̄)ノ なページ再帰 - UJS/UTF/JsUnit(Jorg Schaible)
JScript を使う >> UnitTest Framework >> JsUnit ( Jorg Schaible )

JsUnit ( Jorg Schaible )


JUnit の JavaScript port。 GPL ライセンスで配布されています。

prototype を駆使し Test Case を記述します。

配布元および使用したバージョンは次の通りです。
JsUnit ( Jorg Schaible )
http://jsunit.berlios.de/
- jsunit_1-2.tar.gz

目次

JsUnit のテスト


JsUnit の動作要件を満たしているかテストできます。テスト スクリプトは tests ディレクトリ下にあります。コマンドラインで試す場合は tests ディレクトリに移り実行してください。
AllTests.html
AllTests.js
AllTests.wsh

テスト結果の参考例。
D:\jsunit_1-2\tests>cscript AllTests.js
Microsoft (R) Windows Script Host Version 5.6
Copyright (C) Microsoft Corporation 1996-2001. All rights reserved.


JavaScript compatibility:
has exceptions: true
exceptions working: true

JavaScript engine detection:
isBrowser: false
isJScript: true
isWSH: true
isIIS: false
isNSServer: false
isRhino: false
isMozillaShell: false
isKJS: false
isShell: true
isObtree: false
hasCallStackSupport: true

JsUnit Test Suite:

........................................
........................................
........................................
........
Time: 0.47

OK (128 tests)

サンプル

添付サンプル


JsUnit のデモは samples にあります。コマンドラインで試す場合は samples ディレクトリに移り実行してください。

テスト結果の参考例。 failure は意図的に引き起こされたものです。
D:\jsunit_1-2\samples>cscript AllTests.js
Microsoft (R) Windows Script Host Version 5.6
Copyright (C) Microsoft Corporation 1996-2001. All rights reserved.

..........................F..
Time: 0.06
There was 1 failure:
1) Test SimpleTest.testAdd failed: AssertionFailedError: Expected:<6>, but was:<5>

FAILURES!!!
Tests run: 28, Failures: 1, Errors: 0

添付サンプルを1つのファイルにまとめた物


samples の AllTests.js と SimpleTest.js を参考に1つのファイルにまとめたサンプル。
/*
JsUnit - a JUnit port for JavaScript
Copyright (C) 1999,2000,2001,2002,2003 Joerg Schaible

This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation in version 2 of the License.

This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
GNU General Public License for more details.

You should have received a copy of the GNU General Public License
along with this program; if not, write to the Free Software
Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA

Test suites built with JsUnit are derivative works derived from tested 
classes and functions in their production; they are not affected by this 
license.
*/

if( !this.JsUtil )
{
    var pathJsUtil = "lib/JsUtil.js";
    if( this.WScript )
    {
        var fso = new ActiveXObject( "Scripting.FileSystemObject" );
        var file = fso.OpenTextFile( pathJsUtil, 1 );
        var all = file.ReadAll();
        file.Close();
        eval( all );
    }
    else
        load( pathJsUtil );

    eval( JsUtil.prototype.include( "lib/JsUnit.js" ));
}


/**
 * Some simple tests.
 */
function SimpleTest(name)
{
    TestCase.call( this, name );
}
SimpleTest.prototype = new TestCase();
SimpleTest.prototype.setUp = 
    function()
    {
        this.fValue1= 2;
        this.fValue2= 3;
    };
SimpleTest.prototype.testAdd = 
    function()
    {
        var result = this.fValue1 + this.fValue2;
        // forced failure result == 5
        this.assertEquals( 6, result );
    };
SimpleTest.prototype.testDivideByZero = 
    function()
    {
        var zero = 0;
        this.assertEquals( "Infinity", 8/zero );
    };
SimpleTest.prototype.testAsserts = 
    function()
    {
        this.assertTrue( true );
        this.assertFalse( false );
        this.assertEquals( 1, this.fValue2 - this.fValue1 );
        this.assertNull( null );
        this.assertNotNull( this.fValue1 );
        this.assertUndefined();
        this.assertNotUndefined( true );
        this.assertSame( this, this );
        this.assertNotSame( 
            new Number( this.fValue1 ), new Number( this.fValue1 ));
    };


function SimpleTestSuite()
{
    TestSuite.call( this, "SimpleTestSuite" );
    this.addTestSuite( SimpleTest );
}
SimpleTestSuite.prototype = new TestSuite();
SimpleTestSuite.prototype.suite = function() { return new SimpleTestSuite(); };


function AllTests()
{
    TestSuite.call( this, "AllTests" );
}
AllTests.prototype = new TestSuite();
AllTests.prototype.suite = 
        function()
        {
            var suite = new AllTests();
            suite.addTest( SimpleTestSuite.prototype.suite());
            return suite;
        };


if( JsUtil.prototype.isShell )
{
var args;
    if( this.WScript )
    {
        args = new Array();
        for( var i = 0; i < WScript.Arguments.Count(); ++i )
            args[i] = WScript.Arguments( i );
    }
    else if( this.arguments )
        args = arguments;
    else
        args = new Array();

    var result = TextTestRunner.prototype.main( args );
    JsUtil.prototype.quit( result );
}

解説

上記サンプルは大まかに3つに分けられます。
  • ライブラリの読み込み。
  • テスト スイートの定義。
  • コマンドライン引数の取得。

順に見ていきます。

まずライブラリの読み込み。 JScript にはライブラリの読み込み関数は用意されていません。 JScript にはファイル操作関係の関数もないため回りくどくなります。 Scripting.FileSystemObject オブジェクトを介してファイルを読み込み、 eval 関数で実行しています。 else にある load 関数は SpiderMonkey に用意されたライブラリ読み込み関数です。 SpiderMonkey は Firefox の JavaScript エンジンです。 JsUtil.prototype.include メソッドは引数で指定したファイルの内容を読み込み、値を戻します。メソッド内で eval しないのは、関数内で実行された eval は関数内にとどまるためです。
if( !this.JsUtil )
{
    var pathJsUtil = "lib/JsUtil.js";
    if( this.WScript )
    {
        var fso = new ActiveXObject( "Scripting.FileSystemObject" );
        var file = fso.OpenTextFile( pathJsUtil, 1 );
        var all = file.ReadAll();
        file.Close();
        eval( all );
    }
    else
        load( pathJsUtil );

    eval( JsUtil.prototype.include( "lib/JsUnit.js" ));
}

テスト ケースは TestCase クラスを継承し定義します。テスト メソッドは prototype プロパティに定義していきます。名前が test で始まるメソッドがテスト メソッドになります。 setUp はテスト メソッド呼び出し前に毎回呼ばれるメソッド、 tearDown はテスト メソッド呼出し後に毎回呼ばれるメソッドです。

コンストラクタでは親クラスの初期化のため TestCase.call を実行します。
function SimpleTest(name)
{
    TestCase.call( this, name );
}
SimpleTest.prototype = new TestCase();
SimpleTest.prototype.setUp = 
// snip.
SimpleTest.prototype.testAdd = 

テスト スイートは TestSuite クラスを継承し定義します。コンストラクタで親クラスを初期化したあと this.addTestSuite メソッドでテスト ケースを登録します。
function SimpleTestSuite()
{
    TestSuite.call( this, "SimpleTestSuite" );
    this.addTestSuite( SimpleTest );
}
SimpleTestSuite.prototype = new TestSuite();
SimpleTestSuite.prototype.suite = function () { return new SimpleTestSuite(); };

テスト ランナーでテストを実行します。その前に、 AllTests を定義します。特に TestSuite を指定しなかった場合、テスト ランナーはこのクラスを呼び出します。 AllTests は TestSuite クラスを継承し定義します。 prototype.suite メソッド内で実行するテスト スイートを登録します。登録するテスト スイートは複数あってもかまいません。
function AllTests()
{
    TestSuite.call( this, "AllTests" );
}
AllTests.prototype = new TestSuite();
AllTests.prototype.suite = 
        function()
        {
            var suite = new AllTests();
            suite.addTest( SimpleTestSuite.prototype.suite());
            return suite;
        };

コンソールから実行した場合のテスト ランナー。コマンドライン引数を Array オブジェクトに乗せかえてテスト ランナーに渡しています。引数は TestSuite のクラス名を指定します。クラス名は大文字小文字を区別します。引数で TestSuite を指定しなかった場合は AllTests が呼ばれます。
if( JsUtil.prototype.isShell )
{
var args;
    if( this.WScript )
    {
        args = new Array();
        for( var i = 0; i < WScript.Arguments.Count(); ++i )
            args[i] = WScript.Arguments( i );
    }
    else if( this.arguments )
        args = arguments;
    else
        args = new Array();

    var result = TextTestRunner.prototype.main( args );
    JsUtil.prototype.quit( result );
}

Assert

メソッド一覧


Assert クラスのメソッド一覧です。 TestCase は Assert クラスから派生しています。
assertTrue( String msg, String cond )Asserts that a condition is true.
assertFalse( String msg, String cond )Asserts that a condition is false.
assertEquals( String msg, Object expected, Object actual )Asserts that two values are equal.
assertFloatEquals( String msg, Object expected, Object actual, Object tolerance )Asserts that two floating point values are equal to within a given tolerence.
assertSame( String msg, Object expected, Object actual )Asserts that two values are the same.
assertNull( String msg, Object object )Asserts that an object is null.
assertUndefined( String msg, Object object )Asserts that an object is undefined.
assertNotSame( String msg, Object expected, Object actual )Asserts that two values are not the same.
assertNotNull( String msg, Object object )Asserts that an object is not null.
assertNotUndefined( String msg, Object object )Asserts that an object is not undefined.
fail( String msg, CallStack stack, String usermsg )Fails a test with a give message.

TestCase クラスのメソッド一覧です。抜粋です。メソッドはオーバーライドして使います。これらはテスト ケースを実行すると自動的に呼び出されます。
setUp()Set up the environment of the fixture.
tearDownClear up the environment of the fixture.
注意点

assertEquals は expected == actual で評価される。

assertEquals は配列同士の比較はできない。

assertEquals の expected が RegExp のインスタンスであり、 actual が String なら actual.match( expected ) で評価される。

assertEquals の振る舞いは奇妙に見えるかもしれない。
this.assertEquals( 1, 1.0 );
this.assertEquals( "1", 1 );
this.assertEquals( true, 1 );
this.assertEquals( false, 0 );
this.assertEquals( true, "1" );
this.assertEquals( false, "0" );
this.assertEquals( ["a"], "a" );
this.assertEquals( "a,b", ["a", "b"] );
this.assertEquals( "tostring", {toString:function(){return "tostring";}} );
this.assertEquals( [1], 1 );
this.assertEquals( function(){}, "function(){}" );
this.assertEquals( undefined, null );
this.assertEquals( /abc/, "abc" );

assertSame は expected === actual で評価される。

assertTrue は eval( cond ) で評価される。

思考する

JsUnit はどこにある?


JScript は include のようなライブラリ読み込みステートメントを持たないため、JsUnit のサンプルでは File System Object 経由でファイルを読み込んでいる。このためライブラリ パスをアプリケーション側で管理することになる。

ライブラリをアプリケージョンごとにコピーすると JsUnit にバグがあった場合面倒だ。

アプリケーションで cscript.exe のあるパスの ../lib や、JSUNIT_HOME 環境変数の値や、テスト スクリプトが置かれた場所など、柔軟に対応しようとすれば複雑になる。

複雑に考えるより、 Sysinternals の junction コマンドでジャンクション ポイントを作成するほうがよさそうだ。作成先が NTFS でフォーマットされたパーティションなら使える。

JsUnit が %JSUNIT_HOME% にあり、カレント ディレクトリ下の lib にジャンクション ポイントを作成する場合はコマンドプロンプトから次のように junction コマンドを実行する。
junction %JSUNIT_HOME%\lib lib

カレント ディレクトリに既に lib ディレクトリが存在する場合はジャンクション ポイントは作成されない。

ジャンクション ポイントを削除する場合は -d オプションで作成したジャンクション ポイントを指定する。
junction -d lib

js か wsf か


この問題のひとつは「 JsUnit はどこにある?」問題の別の側面だ。 js ファイルの場合はライブラリを File System Object 経由で読み込まなければならない。 wsf なら script タグの src 属性にパスを指定してやれば読み込んでくれる。カレント ディレクトリが異なっていてもスクリプトの置かれた場所から相対パスで検索してくれる。下は sample/AllTests.wsf から抜粋し手を加えたもの。
<?XML version="1.0" standalone="yes" ?>
<job id="JsUnitTest">
<script language="JScript" src="../lib/JsUtil.js" />
<script language="JScript" src="../lib/JsUnit.js" />
<script language="JScript"><![CDATA[
// JScript code
]]></script>
</job>

JScript に固定したアプリケーションであるなら wsf が楽だ。 Spider Monkey など他の JavaScript/ECMA Script 処理系で使用する場合は使えない。

ECMA の最新仕様には include ステートメントが追加されたそうだから(うろおぼえ)、そのうち JScript にも追加されるかもしれない。 include が使えるようになれば悩みは解消される。

TestCase と TestSuite と TestRunner


todo

簡略化する


prototype プロパティを記述するのが煩雑なので、プロパティを使ってテスト ケースを定義するようにしてみます。足りないメソッドも追加します。

スクリプトのライセンスは JsUnit's です。インデックスの Download から取得してください。

JsUnitExtend.js


ノーマルの Assert には充分なメソッドが定義されていない( assertNotEquals さえない!)ので欲しいメソッドを足すライブラリを書きました。
vardump( iObject )iObject を文字列に変換して返す。型などの情報を追加している。
assertNotEquals( msg, expected, actual )assertNotEquals の否定版。 expected と actual が同じであるときログに記録する。
assertDeepEquals( msg, expected, actual )assertEquals より深く比較する。 expected に equals メソッドが定義されている場合はこれを利用する。
assertDeepNotEquals( msg, expected, actual )assertDeepEquals の否定版。 expected と actual が同じであるときログに記録する。
Assert_assertNaN( msg, object )object が NaN でないときログに記録する。
assertInstanceof( msg, klass, object )object が klass 一族ではないときログに記録する。
assertIn( msg, method, object )object に method が定義されていない場合にログに記録する。
assertMatch( msg, pattern, string )string が正規表現 pattern にマッチしないときログに記録する。
assertNoMatch( msg, pattern, string )assertMatch の否定版。 string が正規表現 pattern にマッチするときログに記録する。
assertThrows( msg, expectedSymbol, block )block を呼び出し、例外 expectedSymbol が送出されなかった場合にログに記録する。
assertNothingThrown( msg, block )block を呼び出し、例外が送出された場合にログに記録する。
assertBlock( msg, block )block が偽を返した場合にログに記録する。

TestSuiteCollection


テストケースを追加するたび prototype を記述するのが面倒+視認性が悪いため、プロパティでテストケースを定義できるようにしました。
TestSuiteCollection.declareTestCase( iName, iTestCase )iName の名前でテストケースを作成します。 iTestCase はプロパティで渡します。テストケースの作成と同時にテストスイートも作成します。テストスイートの名前は iName の最後に Suite を付加した名前になります。

テスト ケース名は Test で終わる名前にします。各テストは test で始まるメソッド名にします。

以下、サンプル。
<?XML version="1.0" standalone="yes" ?>
<job id="JsUnitTest">
  <script language="JScript" src="lib/JsUtil.js" />
  <script language="JScript" src="lib/JsUnit.js" />
  <script language="JScript" src="lib/JsUnitExtend.js" />
  <script language="JScript" src="lib/TestSuiteCollection.js" />
  <script language="JScript"><![CDATA[

TestSuiteCollection.declareTestCase( 
    "SimpleTest", 
    {
        test : function()
            {
                this.assertEquals( 1, 1 );
            }
    }
);

if( JsUtil.prototype.isShell )
{
var args;
    if( this.WScript )
    {
        args = new Array();
        for( var i = 0; i < WScript.Arguments.Count(); ++i )
            args[i] = WScript.Arguments( i );
    }
    else if( this.arguments )
        args = arguments;
    else
        args = new Array();

    var result = TextTestRunner.prototype.main( args );
    JsUtil.prototype.quit( result );
}

  ]]></script>
</job>

リンク

Junction ( http://www.microsoft.com/technet/sysinternals/File... )
Windows でコマンドラインからシンボリック リンクを作成するためのコマンドライン ツール。
Microsoft TechNet: Windows Sysinternals ( http://www.microsoft.com/technet/sysinternals/defa... )
Sysinternals のサイト。上記 junction のほか有用なツールを数多く公開されています。

更新履歴


2007/6/24sun 「js か wsf か」を修正。「簡略化する」に追記。
2007/4/28sat Assert/メソッド一覧/注意点を追記。リンク追記。
2007/4/22sun ページ作成。