cfneoのテストコードの命令網羅率100%達成のために頑張ってみた

id:imai78氏からコミット権に加えリリース権を付与されてもらって、もう3ヶ月くらいか。
SeaserConference2008Automnでのcfneoの紹介から数えるともう、早4ヶ月がたっていますが、ここまでver0.01のリリースが遅れたのは、テスト実装を託されたにもかかわらず放置をかましてしまった自分の責任です。もうしわけないことです。

テストのプロダクトだし、せめてテストの命令網羅率を100%にしてリリースしよう!という思いのもと、カバレッジ100%を目指してテストコードの実装を行っていたのですが、ちょっとした壁にぶち当たって長いこと停滞してしまっていました。
重ねて言います。申し訳ないことです。

さて、その壁です。

ステートメントカバレッジ100%を目指すと、どうしてもテスト実装困難な部分がありました。try/catchのcatchブロック内の処理、switch/caseのdefault、if/elseのelseの部分です。

こういうところのテストが行えないために、網羅できない。
<cfif cfcatch.Type is "AssertEquals">
	<cfset assertResult="failed" />
<cfelseif cfcatch.Type is "EqualsCaution">
	<cfset assertResult="caution" />
<cfelse><!--- ここのコードパスが実行しづらい!! --->
	<cfset assertResult="error" />
</cfif>

いわゆる「想定外の状況に実行されるコード」がテストしにくくてしょうがないのです。

こういった箇所のテストを実装する場合、テスト時にスタブを用意してその処理を呼び出すようにし、そのなかでExceptoinを発生させるなり、通常の処理では絶対帰ってこないような値を返したり、ということをしないとテストが難しくてしょうがないのです。
DIが出来ればそれが簡単にできる上に、ソース上からその定義を切り離せるのですが、CFにはDIを簡単に実現する機能がありません。

が、「cfneo」にはその機能を実装するための糸口が実はすでにありました。cfneo.ComponentUtilで提供される簡易DI機能です。
この簡易DI機能、現在提供されている機能はアプリケーションのルートからフォルダ名、コンポーネント名をパッケージ名.クラス名風に指定してコンポーネント(cfc)を取得する、という機能です。

cfneoの簡易DI機能
<!--- cfコンポーネントのアプリ内でのIDを文字列で指定してGet! --->
<cfset var hoge=getComponentManager()
	.getComponent("approot.karano.pasuwo.kakuyo.Korehaobjectnonamae") />

ここをいじればいいじゃん!簡易じゃなく、外部ファイルからコンポーネント切り替えを指定してちゃんとDIできるようにすればいいじゃん!
というわけで、簡易DIから、ちょっとしたDIまでの進化を目指して実装。
結果として命令網羅率100%が達成されそうな感じです!

やったことといえば、cfneoで作ったアプリケーションのルートにdi.xmlなるXMLファイルを配置し、その中に定義があればそのコンポーネントを取得するようにした・・・っていうそれだけ。

di.xmlの内容
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE Mapping SYSTEM "di.dtd">
<Mapping>
	<!-- コンポーネント定義 -->
	<Component id="appcore.cfneounit.CfneoTestRunner"
	           implement="appcore.cfneounit.CfneoTestRunner">
	   <!-- コンポーネントのプロパティを指定可能とする -->
	   <Properties>
		   <!-- オブジェクトのプロパティを指定 -->
		   <ComponentProperty name="CompareUnit" implementId="appcore.cfneounit.CompareUnit" />
		   <!-- リテラルのプロパティを指定 -->
		   <LiteralProperty name="test" value="thisIsTest" />
	   </Properties>
	</Component>
	<Component id="appcore.cfneounit.CfneoTestRunnerForTest"
	           implement="appcore.cfneounit.CfneoTestRunner">
	   <!-- コンポーネントのプロパティを指定可能とする -->
	   <Properties>
		   <!-- オブジェクトのプロパティを指定 -->
		   <ComponentProperty name="CompareUnit" implementId="dev.test.appcore.cfneounit.CfneoTestRunnerTest.ErrorCompareUnit" />
	   </Properties>
	</Component>
</Mapping>

コメント文のとおりな感じで、di.xmlの定義に従ってプロパティを指定しつつコンポーネントを生成して返してくれるようにgetComponentファンクションをいじりました。すみません、説明するのが面倒になって「コメントどおりです」メソッドを使いました。

ちゃんとDTDも書いたよ!DTDの書き方とかさっぱり知らなかったんだけど「入門XML」見ながら書いた!俺がんばった!

di.dtdの内容
<?xml version="1.0" encoding="UTF-8"?>
<!ELEMENT   Mapping    (Component+) >
<!-- 
	Component要素の定義
	DIするコンポーネントを表す。
	属性idはこのファイル上でユニークでなければならない。
	implementは、コンポーネントを定義するcfcファイルへのアクセスパスを記述する。 
-->
<!ELEMENT   Component  (Properties) >
<!ATTLIST   Component
  id        ID         #REQUIRED
  implement CDATA      #REQUIRED>
<!-- 
	Propertiesタグの定義
	コンポーネントが所有するプロパティを定義する領域。
	ComponentProperty、LiteralPropertyをそれぞれ複数定義できる。
-->
<!ELEMENT   Properties (ComponentProperty*, LiteralProperty*) >

<!-- 
	ComponentPropertyの定義
	ComponentPropertyは、Componentでできたプロパティを定義する。
 	属性nameはプロパティ名を表し、implementIdはその実装Componentを表す。
	implementIDにはcfcファイルへのアクセスパスとしてのIDが指定できるが、
	di.xmlで定義されているComponentを再帰的に指定することもできる。
-->
<!ELEMENT   ComponentProperty  EMPTY>
<!ATTLIST   ComponentProperty
  name        CDATA      #REQUIRED
  implementId CDATA      #REQUIRED>

<!-- 
	LiteralPropertyの定義
	LiteralPropertyは、文字列リテラルでプロパティの値を定義する
	属性nameはプロパティ名を表し、valueはその値を表す。
-->
<!ELEMENT   LiteralProperty    EMPTY>
<!ATTLIST   LiteralProperty
  name      CDATA      #REQUIRED
  value     CDATA      #REQUIRED>

テストコードをきちんと実装したら、ver0.01のリリースとさせていただこうと思ってます。
チーフコミッタたるid:imai78氏から「びみょー・・・な感じだったら全部書き直すから好きにいじっていいよ」とのお墨付きももらっているので、引き続きがりがり書き散らしてみようっと。