Hudson の NUnit Plugin を使うとテストケースの数が減る問題とその解決方法
仕事で Hudson を使い始めた頃 (去年の 10 月だか 11 月だか) から、NUnit のテストケースが実際よりも少なくなってしまう問題は認識していましたが、あまり気にしていませんでした。
しかし今回、かなりの数のテストケースが削られていたため、ちょっと調べて直してみました。
始める前に
ここで紹介する方法は、プラグインのクラスファイルの一部を入れ替える方法です。
あくまでその場しのぎの解決方法であることを理解したうえで、この方法を実行する場合は自己責任でお願いします。
調査
まず、どこでテストケースが減っているのかを調べていきました。
NUnit が出力する XML ファイルでは、当然ながらテストケースの数は正しいものが出力されていました。
どうやら NUnit Plugin が NUnit の出力する XML ファイルを JUnit の形式に XSL ファイルで変換しているらしいので、この XSL ファイルに NUnit から出力された XML ファイルを与え、変換してみました。しかし、ここではテストケースの数は正しいままでした。それに、XSL ファイルの内容を見ても、テストケースの数は減りそうにありません。
次に、ソース *1 をチェックアウトして、変換部分を探してみると、NUnitReportTransformer クラスの splitJUnitFile メソッドが怪しい感じでした。
このメソッドは XSL 変換した結果を複数ファイルに分割しているのですが、なぜ分割しているのかは分かりません。
しかし、ここで分割にミスってテストケースを減らしている可能性は大いにあり得ますので、実際にこのクラスを動かしてみると、やはりテストケースの数が減っていました。
原因
どのテストケースが減っているのかを調べたところ、NUnit のパラメタライズドテストを使い、テストメソッドをオーバーロードしていた部分が減っていました。
[TestCase(10, "hoge")] [TestCase(20, "piyo")] public void Hoge(int input, string expected) { ... } [TestCase(10, true, "hoge")] public void Hoge(int input, bool isXxx, string expected) { ... }
NUnit のパラメタライズドテストの詳細については、
を参照してください。
上記のテストコードが、HogePiyo 名前空間の FooBarTest クラスに記述されていた場合、全体としては 3 つのテストケースが出力されて欲しいにも関わらず、NUnit Plugin ではテストメソッド名しか考慮しておらず、1 つのテストケースしか出力されていませんでした (後のテストケースで上書きされる)。
さらに、NUnit が出力する XML で、パラメタライズドテストは test-suite 扱いだったので、splitJUnitFile の動作と合わさって、テストケースが削られていました。
例えば、上の例で出力される XML は
<test-suite name="HogePiyo" ...> <test-suite name="FooBarTest" ...> <test-suite name="Hoge" ...> <results> <test-case name="HogePiyo.FooBar.Hoge(10, "hoge")" .../> <test-case name="HogePiyo.FooBar.Hoge(20, "piyo")" .../> </results> </test-suite> <test-suite name="Hoge" ...> <results> <test-case name="HogePiyo.FooBar.Hoge(10, True, "hoge")" .../> </results> </test-suite> </test-suite> </test-suite>
のようなものになりますが、
[Test] public void HogeTest1() { ... } [Test] public void HogeTest2() { ... } [Test] public void HogeTest3() { ... }
の場合、
<test-suite name="HogePiyo" ...> <test-suite name="FooBarTest" ...> <results> <test-case name="HogeTest1" .../> <test-case name="HogeTest2" .../> <test-case name="HogeTest3" .../> </results> </test-suite> </test-suite>
のような XML になります。
XSL の修正
上の XML をそれぞれ XSL 変換すると、
<!-- パラメタライズドテストの場合 --> <testsuite name="HogePiyo.FooBarTest.Hoge" ...> <testcase classname="HogePiyo.FooBarTest.Hoge" name="" .../> <testcase classname="HogePiyo.FooBarTest.Hoge" name="" .../> </testsuite> <testsuite name="HogePiyo.FooBarTest.Hoge" ...> <testcase classname="HogePiyo.FooBarTest.Hoge" name="" .../> </testsuite>
<!-- 普通のテストの場合 --> <testsuite name="HogePiyo.FooBarTest" ...> <testcase classname="HogePiyo.FooBarTest" name="HogeTest1" .../> <testcase classname="HogePiyo.FooBarTest" name="HogeTest2" .../> <testcase classname="HogePiyo.FooBarTest" name="HogeTest3" .../> </testsuite>
となります。
パラメタライズドテストでは classname にメソッド名まで含んでしまっている上、name が空になってしまっています。
この問題を修正するために、main/resources/hudson/plugins/nunit/nunit-to-junit.xsl の 13 行目の XSL 変数 assembly と 18 行目の testcaseName で直接値を決定していた部分を、仮の testcaseName の内容を見てから決定するようにしました。
<!-- この下が13行目 --> <xsl:variable name="tmpAssembly" select="concat(substring-before($firstTestName, @name), @name)" /> <!-- assemblyからtmpAssemblyに変更 --> <!-- <redirect:write file="{$outputpath}/TEST-{$tmpAssembly}.xml">--> <testsuite name="{$tmpAssembly}" tests="{count(*/test-case)}" time="{@time}" failures="{count(*/test-case/failure)}" errors="0" skipped="{count(*/test-case[@executed='False'])}"> <xsl:for-each select="*/test-case[@time!='']"> <xsl:variable name="tmpTestcaseName"> <!-- testcaseNameからtmpTestcaseNameに変更 --> <xsl:choose> <xsl:when test="contains(./@name, $tmpAssembly)"> <xsl:value-of select="substring-after(./@name, concat($tmpAssembly,'.'))"/> </xsl:when> <xsl:otherwise> <xsl:value-of select="./@name"/> </xsl:otherwise> </xsl:choose> </xsl:variable> <!-- 以下新規追加するコード --> <xsl:variable name="assembly"> <!-- ここでassemblyを決定 --> <xsl:choose> <xsl:when test="string-length($tmpTestcaseName)=0"> <xsl:value-of select="substring-before($tmpAssembly, concat('.', ../../@name))"/> </xsl:when> <xsl:otherwise> <xsl:value-of select="$tmpAssembly"/> </xsl:otherwise> </xsl:choose> </xsl:variable> <xsl:variable name="testcaseName"> <!-- ここでtestcaseNameを決定 --> <xsl:choose> <xsl:when test="string-length($tmpTestcaseName)=0"> <xsl:value-of select="concat(../../@name, substring-after(./@name, ../../@name))"/> </xsl:when> <xsl:otherwise> <xsl:value-of select="$tmpTestcaseName"/> </xsl:otherwise> </xsl:choose> </xsl:variable>
仮の testcaseName (tmpTestcaseName) が空の場合、パラメタライズドテストとみなして、assembly と testcaseName を組み直し、空ではない場合、tmpAssembly と tmpTestcaseName を assembly と testcaseName として使用するようにしました。
しかし、これだけでは XSL 変数 assembly の名前がかぶった場合、つまりパラメタライズドテストでオーバーロードしていた場合に、テストケースが消えてしまいます。
なぜなら、main/java/hudson/plugins/nunit/NUnitReportTransformer.java の splitJUnitFile メソッドが
String filename = JUNIT_FILE_PREFIX + element.getAttribute("name").replaceAll(ILLEGAL_FILE_CHARS_REGEX, "_") + JUNIT_FILE_POSTFIX; File junitOutputFile = new File(junitOutputPath, filename); FileOutputStream fileOutputStream = new FileOutputStream(junitOutputFile);
となっているからです。
element は testsuite 要素が格納されているので、その name 属性、つまり XSL 変数 assembly の名前がかぶっていた場合、上記プログラムの 1 行目で生成されるファイル名は同一のものになります。
そして、上記プログラムの 3 行目で FileOutputStream のコンストラクタを呼び出していますが、ここで同じ名前のファイルが上書きされてしまい、その分のテストコードが消えることになります。
NUnitReportTransformer.java の修正
これを避けるために、NUnitReportTransformer.java にも修正が必要となります*2。
要は、作成しようとしているファイル名がすでに存在するなら、違うファイル名を使えばいいだけです。
そこで、ファイル名の末尾に連番を付与することにしました。
上記 3 行は、以下のように変更します。
File junitOutputFile = outputFile(element.getAttribute("name").replaceAll(ILLEGAL_FILE_CHARS_REGEX, "_"), junitOutputPath); FileOutputStream fileOutputStream = new FileOutputStream(junitOutputFile);
そして、フィールドとメソッドを追加します。
private static int seq = 2; private static File outputFile(String tmp, File parent) { File f = new File(parent, filename(tmp)); return f.exists() ? outputFileImpl(tmp, parent) : f; } private static File outputFileImpl(String tmp, File parent) { File f = new File(parent, filename(tmp, seq)); if (f.exists()) { seq++; return outputFileImpl(tmp, parent); } seq = 2; return f; } private static String filename(String name) { return JUNIT_FILE_PREFIX + name + JUNIT_FILE_POSTFIX; } private static String filename(String name, int n) { return JUNIT_FILE_PREFIX + name + "_" + n + JUNIT_FILE_POSTFIX; }
後はこれをコンパイルして、Hudson をインストールした場所にある plugins/nunit/WEB-INF/classes/hudson/plugins/nunit に .xsl ファイルと生成された .class ファイルをコピーします。
これで、ファイル名がかぶることがなくなり、テストケースの数が減ることがなくなりました。
さらに、今まで空だったテスト名や、メソッド名まで含んでしまっていたクラス名も直りました。