テストについて

Coldfusionのフレームワーク的な何かについてあれこれメモの続き。


そもそもフレームワークは、生産性やら品質やらに対してのアプローチ(厳密な数値化は難しいだろうけど)をしないと存在意義が無いです。生産性は品質を疎かにしては絶対に向上しないので、まずはテストのし易さ(テスタビリティ、で良いんでしょうか?)に対する検討をしませんと。。。

という事で、ちょっとテストについてめも。


Coldfusionは、前述のようにコマンドラインからの実行が出来ない為、UnitTestがニガテな感じです。UnitTestをやろうと思うと、「テストコードを含んだ画面」を作る→それを呼び出す→テストが実行される→結果がブラウザに表示される、という流れですね。(中には、「全てブラウザ操作でテスト」という手法もあるようですが、これだと「見落とし」「嘘」「操作ミス」が介入する可能性が高いと思うので僕はあまりオススメしません)

これだけではなく、Coldfusionの画面用ファイル(.CFM)では無く、コンポーネント用ファイル(.CFC)で機能をなるべく小分けにしておいた方が良い、とも思います。これは僕がJava好きだから、というのもあるかも知れませんが、コードを書く前後で行われる初期のテストでの組み合わせ爆発が起こるのが嫌だから、というのが一番の理由です。


ここで少しシミュレーションを。

仕様

CF電卓1号(calc1.cfm)

  • 入力項目について
    • Input左、Input中央、Input右の3つ。
    • Input右はreadonly。
    • 未入力は許可しない。
    • 整数以外は許可しない。
    • 入力範囲は0から10まで。
  • 計算処理
    • Input左、Input中央の入力血を足し算して右に表示。

多分、「Hello,World.」の次くらいにお約束な和算専用機ですね。

まず、これのコード。*1

<?xml version="1.0" encoding="${encoding}" ?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
  <!---// 入力欄用の領域を宣言する。 //--->
  <cfset Variables.t1="" />
  <cfset Variables.t2="" />
  <!---// メッセージ出力用の領域を宣言する。 //--->
  <cfset Variables.message="" />
  <!---// 回答表示用の領域を宣言する。 //--->
  <cfset Variables.answer="" />
  <!---// POSTによる当画面の再起呼び出し時に実行される処理 //--->
  <cfif IsDefined("Form.calc") and Form.calc neq "">
    <cftry>
      <!---// 再表示時用にローカル変数に格納 //--->
      <cfset Variables.t1=Form.t1 />
      <cfset Variables.t2=Form.t2 />
      <!---// 入力チェック //--->
      <cfif Variables.t1 is "">
        <cfthrow type="InputValidation" errorcode="ERR001" message="入力欄(左)が空ですぞ。" />
      </cfif>
      <cfif not IsNumeric(Variables.t1)>
        <cfthrow type="InputValidation" errorcode="ERR002" message="入力欄(左)に数値以外が入力されましたぞ。" />
      </cfif>
      <cfif not IsValid("range", Variables.t1, 0, 10)>
        <cfthrow type="InputValidation" errorcode="ERR003" message="入力欄(左)に11以上の数値が入っておりますぞ。" />
      </cfif>
      <cfif Variables.t2 is "">
        <cfthrow type="InputValidation" errorcode="ERR004" message="入力欄(右)が空ですぞ。" />
      </cfif>
      <cfif not IsNumeric(Variables.t2)>
        <cfthrow type="InputValidation" errorcode="ERR005" message="入力欄(右)に数値以外が入力されましたぞ。" />
      </cfif>
      <cfif not IsValid("range", Variables.t2, 0, 10)>
        <cfthrow type="InputValidation" errorcode="ERR006" message="入力欄(右)に11以上の数値または文字が入っておりますぞ。" />
      </cfif>
      <!---// 具体的な処理。 //--->
      <cfset Variables.answer=Form.t1 + Form.t2 />
      <cfcatch>
        <cfif cfcatch.Type is "InputValidation">
          <cfset Variables.message=cfcatch.Message />
        </cfif>
      </cfcatch>
    </cftry>
  </cfif>
  <head>
    <meta http-equiv="Content-Style-Type" content="text/css"/>
    <!---// イベント・ハンドリングの為にjQueryをインクルードする。 //--->
    <script charset="shift_jis" src="jquery-1.2.6.js"></script>
    <!---// アラート表示の為、SimpleModalをインクルードする。 //--->
    <script charset="shift_jis" src="jquery.simplemodal-1.1.1.js"></script>
      <script>
      <!---// DOMの構築までJavaScriptの実行を待つ制御@jQueryを使用。 //--->
      $(function() {
        <!---// Calcボタンが押された際のイベント・ハンドリング //--->
        $("#calc").click(function(e) {
          $("#form").attr("method", "post");
          $("#form").attr("action", "./calc1.cfm");
        });
        <!---// 入力チェックでメッセージが設定された場合、ダイアログ表示を行う。 //--->
        <cfif Variables.message neq "">
          $("#alert").modal();
        </cfif>
      });
    </script>
    <link rel="stylesheet" href="alert.css" type="text/css" />
    <title>Calc1</title>
  </head>
  <body>
    <h1>Calc1</h1>
    <cfoutput>
      <form id="form">
        <fieldset>
          <legend>和算電卓1</legend>
          <div class="notes"><a href="calc1.cfm">Reload</a></div>
          <p>それぞれに0〜10までの数値を入力して、イコールボタンを押して下さい。<br />っていうか、押せ。</p>
          <div class="console">
            <input type="text" id="t1" name="t1" value="#Variables.t1#" />+
            <input type="text" id="t2" name="t2" value="#Variables.t2#" />
            <input type="submit" id="calc" name="calc" value="=" />
            <input type="text" id="a" name="a" value="#Variables.answer#" readonly="readonly" />
          </div>
        </fieldset>
      </form>
      <!---// SimpleModalで表示するダイアログの内容をDivisionとして定義する。 //--->
      <div id="alert" style="display: none;">
        <h1>ALERT!!</h1>
        <p>#Variables.message#</p>
      </div>
    </cfoutput>
  </body>
</html>

jqueryjquery.simpledialogを使って、ダイアログ風に表示させるようにしてます。このファイル1つで、基本的に全ての機能が実装されてます。なんかVB6とか思い出しますねw


んでテストケースをざっくりと。

Case Input左 Input右
1
2 1 2
3 2
4 1
5 -1 1
6 0 1
7 10 1
8 11 1
9 1 -1
10 1 0
11 1 10
12 1 11
13 a 1
14 1 b
15 1
16 1

前述のようにColdFusionはコマンドラインから実行できないので、16回ブラウザを操作してテストする訳です。(本来であれば、これに加えて「和算が正しいか」とか「エラーの表示がされているか」とか「小数点はどこいった?」、「JavaScriptのテストとして、何種類かのブラウザで実行しる」とかが必要なのですが、縦長なエントリになってしまっているので割愛)


なので、分岐が一番集中している箇所を、コンポーネント用ファイル(.CFC)で切り出しました。

先ほどと同じように、画面用ファイルをCF電卓2号(calc2.cfm)として以下のように。*2

<?xml version="1.0" encoding="${encoding}" ?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
  <!---// 入力欄用の領域を宣言する。 //--->
  <cfset Variables.t1="" />
  <cfset Variables.t2="" />
  <!---// メッセージ出力用の領域を宣言する。 //--->
  <cfset Variables.message="" />
  <!---// 回答表示用の領域を宣言する。 //--->
  <cfset Variables.answer="" />
  <!---// POSTによる当画面の再起呼び出し時に実行される処理 //--->
  <cfif IsDefined("Form.calc") and Form.calc neq "">
    <cftry>
      <!---// 再表示時用にローカル変数に格納 //--->
      <cfset Variables.t1=Form.t1 />
      <cfset Variables.t2=Form.t2 />
      <!---// 入力チェック //--->
      <cfinvoke component="examples.test.calc2.Validator" method="validate">
        <cfinvokeargument name="t1" value="#Variables.t1#" />
        <cfinvokeargument name="t2" value="#Variables.t2#" />
      </cfinvoke>
      <!---// 具体的な処理。 //--->
      <cfset Variables.answer=Form.t1 + Form.t2 />
      <cfcatch>
        <cfif cfcatch.Type is "InputValidation">
          <cfset Variables.message=cfcatch.Message />
        </cfif>
      </cfcatch>
    </cftry>
  </cfif>
  <head>
    <meta http-equiv="Content-Style-Type" content="text/css"/>
    <!---// イベント・ハンドリングの為にjQueryをインクルードする。 //--->
    <script charset="shift_jis" src="jquery-1.2.6.js"></script>
    <!---// アラート表示の為、SimpleModalをインクルードする。 //--->
    <script charset="shift_jis" src="jquery.simplemodal-1.1.1.js"></script>
      <script>
      <!---// DOMの構築までJavaScriptの実行を待つ制御@jQueryを使用。 //--->
      $(function() {
        <!---// Calcボタンが押された際のイベント・ハンドリング //--->
        $("#calc").click(function(e) {
          $("#form").attr("method", "post");
          $("#form").attr("action", "./calc2.cfm");
        });
        <!---// 入力チェックでメッセージが設定された場合、ダイアログ表示を行う。 //--->
        <cfif Variables.message neq "">
          $("#alert").modal();
        </cfif>
      });
    </script>
    <link rel="stylesheet" href="alert.css" type="text/css" />
    <title>Calc2</title>
  </head>
  <body>
    <h1>Calc2</h1>
    <cfoutput>
      <form id="form">
        <fieldset>
          <legend>和算電卓2</legend>
          <div class="notes"><a href="calc2.cfm">Reload</a></div>
          <p>それぞれに0〜10までの数値を入力して、イコールボタンを押して下さい。<br />っていうか、押せ。</p>
          <div class="console">
            <input type="text" id="t1" name="t1" value="#Variables.t1#" />+
            <input type="text" id="t2" name="t2" value="#Variables.t2#" />
            <input type="submit" id="calc" name="calc" value="=" />
            <input type="text" id="a" name="a" value="#Variables.answer#" readonly="readonly" />
          </div>
        </fieldset>
      </form>
      <!---// SimpleModalで表示するダイアログの内容をDivisionとして定義する。 //--->
      <div id="alert" style="display: none;">
        <h1>ALERT!!</h1>
        <p>#Variables.message#</p>
      </div>
    </cfoutput>
  </body>
</html>

で、切り出した奴を[Validator.cfc]として以下のように。

<cfcomponent output="false">
  <cffunction name="validate" access="public" returntype="void">
    <cfargument name="t1" type="any" required="false" />
    <cfargument name="t2" type="any" required="false" />
    <!---// 入力チェック //--->
    <cfif Arguments.t1 is "">
      <cfthrow type="InputValidation" errorcode="ERR001" message="入力欄(左)が空ですぞ。" />
    </cfif>
    <cfif not IsNumeric(Arguments.t1)>
      <cfthrow type="InputValidation" errorcode="ERR002" message="入力欄(左)に数値以外が入力されましたぞ。" />
    </cfif>
    <cfif not IsValid("range", Arguments.t1, 0, 10)>
      <cfthrow type="InputValidation" errorcode="ERR003" message="入力欄(左)に11以上の数値が入っておりますぞ。" />
    </cfif>
    <cfif Arguments.t2 is "">
      <cfthrow type="InputValidation" errorcode="ERR004" message="入力欄(右)が空ですぞ。" />
    </cfif>
    <cfif not IsNumeric(Arguments.t2)>
      <cfthrow type="InputValidation" errorcode="ERR005" message="入力欄(右)に数値以外が入力されましたぞ。" />
    </cfif>
    <cfif not IsValid("range", Arguments.t2, 0, 10)>
      <cfthrow type="InputValidation" errorcode="ERR006" message="入力欄(右)に11以上の数値または文字が入っておりますぞ。" />
    </cfif>
  </cffunction>
</cfcomponent>

で、Validator.cfcのTestDriverを以下のように。

<?xml version="1.0" encoding="${encoding}" ?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
  <head>
    <meta http-equiv="Content-Style-Type" content="text/css"/>
    <title>Test driver[Validator.cfc]</title>
  </head>
  <body>
    <h1>Test driver[Validator.cfc]</h1>
    <table border="1">
      <tr>
        <th rowspan="2">Case</th>
        <th rowspan="2">Note</th>
        <th colspan="2">Input</th>
        <th rowspan="2">Expected</th>
        <th rowspan="2">Actual</th>
        <th rowspan="2">Result</th>
      </tr>
      <tr>
        <th>Input左</th>
        <th>Input中央</th>
      </tr>
      <cfset doTest("1", "異常系:どちらも空欄",    "", "", "ERR001") />
      <cfset doTest("2", "正常系:",          "1", "2", "") />
      <cfset doTest("3", "異常系:Input左が空欄",    "", "2", "ERR001") />
      <cfset doTest("4", "異常系:Input中央が空欄",  "1", "", "ERR004") />
      <cfset doTest("5", "閾値:Input左が範囲外",    "-1", "1", "ERR003") />
      <cfset doTest("6", "閾値:正常系",        "0", "1", "") />
      <cfset doTest("7", "閾値:正常系",        "10", "1", "") />
      <cfset doTest("8", "閾値:Input左が範囲外",    "11", "1", "ERR003") />
      <cfset doTest("9", "閾値:Input右が範囲外",    "1", "-1", "ERR006") />
      <cfset doTest("10", "閾値:正常系",      "1", "0", "") />
      <cfset doTest("11", "閾値:正常系",      "1", "10", "") />
      <cfset doTest("12", "閾値:Input右が範囲外",  "1", "11", "ERR006") />
      <cfset doTest("13", "同値クラス:Input左が文字種エラー", "a", "1", "ERR002") />
      <cfset doTest("14", "同値クラス:Input右が文字種エラー", "1", "b", "ERR005") />
      <cfset doTest("15", "同値クラス:Input左が文字種エラー", "1", "1", "ERR002") />
      <cfset doTest("16", "同値クラス:Input右が文字種エラー", "1", "1", "ERR005") />
    </table>
  </body>
</html>
<cffunction name="doTest" access="private" returntype="void">
  <cfargument name="case" type="string" required="true" />
  <cfargument name="note" type="string" required="true" />
  <cfargument name="t1" type="any" required="false" />
  <cfargument name="t2" type="any" required="false" />
  <cfargument name="expected" type="string" required="false" />
  <cfset var res="" />
  <cftry>
    <cfinvoke component="examples.test.calc2.Validator" method="validate">
      <cfinvokeargument name="t1" value="#Arguments.t1#" />
      <cfinvokeargument name="t2" value="#Arguments.t2#" />
    </cfinvoke>
    <cfcatch>
      <cfset res=cfcatch.ErrorCode />
    </cfcatch>
  </cftry>
  <cfoutput>
    <tr>
      <th>#Arguments.case#</th>
      <td>#Arguments.note#</td>
      <td>#Arguments.t1#</td>
      <td>#Arguments.t2#</td>
      <td>#Arguments.expected#</td>
      <td>[#res#]</td>
      <cfif Arguments.expected is res>
        <td style="background-color: lime;">成功</td>
      <cfelse>
        <td style="background-color: red;">失敗</td>
      </cfif>
    </tr>
  </cfoutput>
</cffunction>

これで前述の16パターンは、一発のGETメソッドで実行完了です。ブラウザの操作は1回で済みましたし、この16パターンに限っては複数種類のブラウザでテストする必要は無いとしちゃえるかな、と。JavaScriptCSSによる見た目の検証であれば、ブラウザによって「想定していたものが動かない/動いちゃう」危険性はありますが、サーバサイドの処理なので基本的にブラウザの影響は受けない(トリガーを引かれるくらい)だと思います。

これによって、「いくつかのブラウザ×(16パターン+α)=画面の操作回数」という事態は避けられそうですね。(後は、CFMLが吐いた出力によって挙動が変わるJavaScriptの検証と見た目の検証、イコールボタン押下時の検証などを済ませばよいかと。)


という訳で、ブラウザを操作するのではなく、機械的にテストの実行をしてくれるような仕組みがまずはないといけないな、と。JavaにおけるJUnitのようなものが無いかなぁ、等と以前は思っておりました。
実はColdFusionのUnitTestingFrameworkにCFUnitっていうものがあるのですが、何故だか上手く使えないです><
もし上手く使えたらここに書くかも知れません。


という訳で、CFMLに慣れていない方、ColdFusionを知らなかった方に、このキモサをお送りいたしました。

*1:

*2: