ja ja ja ja ja ja ja ja ja en ja ja ja ja ja ja ja ja ja ja ja en ja ja ja ja ja ja ja en ja ja ja ja ja ja ja en ja ja ja ja ja ja ja ja ja ja ja ja ja ja ja ja en ja ja ja ja ja ja ja ja ja ja ja ja ja ja en ja ja en my en ja it ja ja it it ja de ja ja en ja ja en ja zh en ur zh lrc ja eu de de vi en en ca zh zh en hu hu ja ja ja ja ja ja ja ja ja ja en ja en ja ja ja en ja ja en en ja ko ja fr ja ja ja hu jbo ja ja ja ja ja ja ja ja ja ja ja en ja ja en ja ja vi ja sv eu ja en en hu ja ja en en pl ja ja jbo hbs_lat en ja ja ja ja ja ja ja ja ja ja ja ja ja ja ja ja ja ja ja
function test_roman2kana() { assert.equals('あいうえお', transform.roman2kana('aiueo')); assert.equals('aiueo', transform.roman2kana('aiueo')); assert.equals('がぎぐげご', transform.roman2kana('gagigugego')); assert.equals('にほんご', transform.roman2kana('nihongo')); assert.equals('ぽーと', transform.roman2kana('po-to')); assert.equals('きゃっきゃ', transform.roman2kana('kyakkya')); assert.equals('うっうー', transform.roman2kana('uwwu-')); assert.equals('\\(\\)\\[\\]\\|', transform.roman2kana('\\(\\)\\[\\]\\|')); assert.equals('\\(\\[', transform.roman2kana('\\(\\[')); assert.equals('\\)\\]', transform.roman2kana('\\)\\]')); assert.equals('\\|', transform.roman2kana('\\|')); }
function test_roman2kana() { function oneTest(aExpected, aInput) { assert.equals(aExpected, transform.roman2kana(aInput)); } oneTest('あいうえお', 'aiueo'); oneTest('aiueo', 'aiueo'); oneTest('がぎぐげご', 'gagigugego'); oneTest('にほんご', 'nihongo'); oneTest('ぽーと', 'po-to'); oneTest('きゃっきゃ', 'kyakkya'); oneTest('うっうー', 'uwwu-'); oneTest('\\(\\)\\[\\]\\|', '\\(\\)\\[\\]\\|'); oneTest('\\(\\[', '\\(\\['); oneTest('\\)\\]', '\\)\\]'); oneTest('\\|', '\\|'); }
例えば以下のようなシナリオが考えられます。
2番目のパターンでfailした。
2番目のパターンでfailする原因となっていたバグを直した。
再度テストを実行した。
5番目のパターンでfailした。
5番目のパターンでfailする原因となっていたバグを直した。
再度テストを実行した。
6番目のパターンでfailした。
「3の段階で行った修正で5や8のバグが発生した」という可能性もありますが、最初から2や5や8のバグがあったのであれば、まとめて直せていたかもしれません。これでは、バグを直しても直してもきりがないという、モグラ叩きのような感覚に陥ってしまいます。
メンテナンス性を高める別の手法として、アサーションに使う期待値とメソッドに渡す引数だけを配列で別途定義しておくというやり方も考えられます。
function test_roman2kana() { var patterns = [ ['あいうえお', 'aiueo'], ['aiueo', 'aiueo'], ['がぎぐげご', 'gagigugego'], ['にほんご', 'nihongo'], ['ぽーと', 'po-to'], ['きゃっきゃ', 'kyakkya'], ['うっうー', 'uwwu-'], ['\\(\\)\\[\\]\\|', '\\(\\)\\[\\]\\|'], ['\\(\\[', '\\(\\['], ['\\)\\]', '\\)\\]'], ['\\|', '\\|'] ]; for each (var pattern in patterns) { assert.equals(pattern[0], transform.roman2kana(pattern[1])); } }
これにもやはり問題があります。
テストする入力パターンのどれか1つでアサーションに失敗したら、そこから後に記述された入力パターンに対するアサーションが行われない。(関数を使った書き方の場合と同じ問題)
配列の内容の順番を覚えておかないといけない。
failした時に、どの入力パターンでfailしたのかが分からない。
1、2、3の問題を回避するための対処を毎回しなければならないのが面倒。
1つ目の問題点については前述したとおりです。
2つ目の問題は、この例のように2次元配列を使った場合に現れます。上記の例では配列の0番目の要素が期待値、1番目の要素が入力となっていますが、これではどっちがどっちなのかを常に意識する必要があります。
3つ目は、ループに特有の問題です。UxUではテストにfailした時にスタックトレースが表示され、べた書きした場合や関数を使った書き方の場合であれば、スタックトレースを辿れば「どのパターンで失敗したのか」の情報に辿り着くことができます。しかし、ループを使っていると、スタックトレースの行き着く先はループの中になってしまうため、どのパターンに対して失敗したのかが一目では分からなくなってしまいます。
この対策として、UxUのアサーションでは最後の引数として任意のメッセージを渡せるので、以下のようにして「どのパターンで失敗したのか」を表示させることは可能です。
assert.equals(pattern.expected, transform.roman2kana(pattern.input), pattern.input+'に対するテスト');
しかし、実際にたくさんテストを書くようになってくると、これが地味に面倒です。これが4つ目の問題点です。
べた書きした場合や関数を使った書き方の場合であれば、このような配慮なしに淡々とテストを書いていても、テスト実行時にはスタックトレースを辿ればデバッグに必要な情報を得られます。それなのに、ループに対してはこのような配慮をしなければいけないわけです。この面倒さによって、テストを新しく書いたり過去に書いたテストをメンテナンスしたりする意欲がじわじわと削がれてしまう、というのが一番の問題点だと言えます。
UxU 0.7.0以降で導入されたデータ駆動テストの仕組みを使うと、上記のテストはこのように書くことができます。
test_roman2kana.parameters = utils.readParametersFromCSV('patterns.csv'); function test_roman2kana(aParameter) { assert.equals(aParameter.expected, transform.roman2kana(aParameter.input)); }
テスト用のコードにはロジックだけを書き、データは外部ファイルで定義します(後述しますが、テストケース内にデータを埋め込むこともできます)。データを定義しているファイルの形式はCSVです。
patterns.csvの内容
\\(\\)\\[\\]\\|
\\(\\)\\[\\]\\|
この時、UxUは「test_roman2kana」という1つのテストではなく、「test_roman2kana (半角英数)」「test_roman2kana (全角英数)」……という名前の11個のテストを実行するようになります。
コードの繰り返しが無く、簡潔に書ける。
すべてが独立したテストとして扱われるので、入力パターンのどれか1つでアサーションに失敗しても、他のテストはスキップされない。
(CSVの場合)カラム名が引数として渡ってくるハッシュのキーとなるので、引数の順番を覚えておかなくていい。
failした時は、実行されたテストの名前の中に実行時の入力パターンの名前が含まれるので、どのパターンで失敗したのかがすぐ分かる。
これらをフレームワークの機能として提供しているので、テストを記述する時に面倒なことを考えなくてもよい。
このように、データ駆動テストには多くのメリットがあります。単純な入出力のパターンを数多く検証しなければいけない場面で、データ駆動テストは威力を発揮します。
UxUでのデータ駆動テストの書き方
UxUでは、テスト関数のparametersプロパティに配列またはハッシュを代入すると、そのテストをデータ駆動テストとして実行するようになります。この時テスト関数には引数として、parametersプロパティの配列またはハッシュの要素が1つずつ渡されます。
以下は、配列を指定した場合の例です。
test_someFunc.parameters = [ { expected : '29', input : 'niku' }, { expected : '2929', input : 'nikuniku' }, { expected : '029', input : 'oniku' } ]; function test_someFunc(aParameter) { /* 1回目: aParameter = { expected : '29', input : 'niku' } 2回目: aParameter = { expected : '2929', input : 'nikuniku' } 3回目: aParameter = { expected : '029', input : 'oniku' } */ ... }
ハッシュを指定した場合は、以下のようになります。ハッシュのキーはテスト関数には渡されず、結果を表示する時のテスト名として表示されます。
test_someFunc.parameters = { simgle: { expected : '29', input : 'niku' }, double: { expected : '2929', input : 'nikuniku' }, o: { expected : '029', input : 'oniku' } }; function test_someFunc(aParameter) { /* 1回目: aParameter = { expected : '29', input : 'niku' } 2回目: aParameter = { expected : '2929', input : 'nikuniku' } 3回目: aParameter = { expected : '029', input : 'oniku' } */ ... }
データ駆動テスト用の便利な機能
データ駆動テストのサポートに併せて、いくつかの新しいヘルパーメソッドが追加されています。これらを使うことで、データ駆動テストをより簡単に作成・メンテナンスすることができます。
前述の例で使用しているutils.readParametersFromCSV()は、CSVファイルの内容を読み込み、最初の行のカラム名をキーとしたハッシュとして返します。例えば前述の例のCSVは、以下のようなハッシュとして解釈されます。
// test_roman2kana.parameters = utils.readParametersFromCSV('patterns.csv'); // これは、以下のように書くのと同じ test_roman2kana.parameters = { '半角英数': { input: 'aiueo', expected: 'あいうえお' }, '全角英数': { input: 'aiueo', expected: 'aiueo' }, '濁音のみ': { input: 'gagigugego', expected: 'がぎぐげご' }, '濁音混じり': { input: 'nihongo', expected: 'にほんご' }, '音引き': { input: 'po-to', expected: 'ぽーと' }, '拗音': { input: 'kyakkya', expected: 'きゃっきゃ' }, '撥音': { input: 'uwwu-', expected: 'うっうー' }, paren: { input: '\\(\\)\\[\\]\\|', expected: '\\(\\)\\[\\]\\|' }, parenOpen: { input: '\\(\\[', expected: '\\(\\[' }, parenClose: { input: '\\)\\]', expected: '\\)\\]' }, pipe: { input: '\\|', expected: '\\|' } };
CSVファイルはRFC4180準拠の形式の読み込みに対応しています。カンマ区切りではなくタブ区切りのファイルを使用したい場合は、utils.readParametersFromTSV()を使用して下さい。どちらも、読み込みたいファイルのパス(相対パスも利用できます)を第1引数に、ファイルのエンコーディングを第2引数に指定します。エンコーディング指定を省略した場合はUTF-8として読み込みます。
以下のリンク先に、各メソッドの詳しい説明があります。
utils.readParametersFromCSV()
utils.readParametersFromTSV()
また、JSON形式で保存した外部ファイルを読み込むためのutils.readJSON()というメソッドもあります。
test_roman2kana.parameters = utils.readJSON('patterns.json');
こちらも、読み込みたいファイルのパス(相対パスも利用できます)を第1引数に、ファイルのエンコーディングを第2引数に指定します。エンコーディング指定を省略した場合はUTF-8として読み込みます。詳しい説明は以下のリンク先をご覧下さい。
utils.readJSON()
データ駆動テストの仕組みを利用すると、テストのロジックとデータを分離できるため、メンテナンス性が高まることが期待できます。様々なパターンの入力を受け付ける機能を開発する時は、データ駆動テストをぜひ一度試してみて下さい。
現在クリアコードではソフトウェア開発者を募集しています! フリーソフトウェア開発での開発の仕方をベースに、成果や知識を公開し共有しながら業務に携われます。
1年間の長期サポート版のあるメールクライアントThunderbirdは企業様向けに、一定の支持をいただいています。
クリアコードではカスタマイズなどのサポートをご案内しています。
とちぎRuby会議02の資料公開
C++用xUnitでのテストの書き方
Apache Arrow
milter manager
ククログの記事はCC BY-SA 4.0とGFDLのデュアルライセンスで自由に利用できます。
クリアコードはプログラミングが楽しいソフトウェア開発者を1名募集しています。
クリアコードは「クリアコードをいい感じにする人」を1名募集しています。
クリアコードはフリーソフトウェア開発で培った技術力を提供しています。特にMozilla製品(Mozilla FirefoxとMozilla Thunderbird)とRubyとGroonga(全文検索)に関連した開発を得意としています。