JenkinsでMSTestカバレッジ結果をxsltで変換して見やすくする方法



この記事はJAYANTHA SIRISENAさんが書かれた記事の翻訳です。
快く翻訳記事の掲載を快諾いただいたJAYANTHA SIRISENAさんにはこの場を借りて
御礼申しげます。
オリジナルはこちらを参照ください。→こちら

また翻訳はかなり意訳になっているのと、一部古いと思われる箇所には
注釈を入れています。

何か間違いにきずかれた方はお知らせくださればうれしいです。



※注意
VisualStudio2012ではStep 1 Step 2の方法ではなく、以下の方法でテスト自動化するようです。

VSTest.Console /Enablecodecoverage /InIsolation test.dll

※オリジナル記事はVisualStudio2010を対象としています。



Jenkinsで.NetのプロジェクトをCIしてるなら、そしてNCover(NUnit用のカバレッジ取得ツール)のようなツールを使わずにカバレッジ結果を取得閲覧するのは大変です。

そんなことをしなくてもいい方法があります。
以下がその方法。

テスト結果をXmlに変える必要があり、それをxslでHTMLに変換(xslt)します。
その後このHTMLをJenkinsで出力します。

Step 1
(テスト設定)
VisualStudioのソリューションのコンテキストで「Testの設定」を追加し
ターゲットファイルのDLL群を設定します。

テストの設定はこちらを参照してください

(注)VisualStudio2012ではMStest.exeよりもVSTest.Console.exeを使用する方法が推奨のようです。

これでVisualStudioの設定でテスト結果とカバレッジの設定ができます。

この設定がJenkins管理下のバージョン管理にコミットしてあることを確認します。
つまりJenkinsのワークスペースにあることを確認します。

Step 2
(テスト自動化)
JenkinsにMStestとテスト結果生成のWindowsバッチコマンドを(ビルド後に)追加します。
このバッチが動けば、reults.trxとdata.coverageができるはずです。

del results.trx 
mstest /testcontainer:Example\TestProject1\bin\debug\TestProject1.dll /resultsfile:results.trx /testsettings:Example\local.testsettings 

MSTestのパスは「\Microsoft Visual Studio 10.0\Common7\IDE」にあります

ここでできるカバレッジファイル(data.coverage)はバイナリ形式です。

Step 3
(xsl変換)
バイナリ形式からxml、さらにxsltでhtmlに書き換えるコンソールアプリをC#で記述します。
これをJenkinsのバッチコマンドに登録します。

これがサンプルコードです。

namespace CoverageConverter
{
    using System;
    using System.Xml;
    using System.Xml.Xsl;
    using Microsoft.VisualStudio.Coverage.Analysis;
    using System.Data;

    class Program
    {
        static void Main(string[] args)
        {
            string wspath = Environment.CurrentDirectory.ToString();
            CoverageInfo coverage = CoverageInfo.CreateFromFile(args[0]);


            DataSet data = coverage.BuildDataSet(null);

            data.WriteXml(wspath + @"\converted.coverage.xml");

            string xml = data.GetXml();

            XmlDocument xmlDocument = new XmlDocument();
            xmlDocument.LoadXml(xml);


            XslCompiledTransform myXslTransform = new XslCompiledTransform();
            XmlTextWriter writer = new XmlTextWriter(wspath + @"\converted.coverage.htm", null);

            myXslTransform.Load(wspath + @"\style.xslt");
            myXslTransform.Transform(xmlDocument, null, writer);

            writer.Flush();
            writer.Close();

        }
    }
}

「\Microsoft Visual Studio 10.0\Common7\IDE\PrivateAssemblies」フォルダにある
「Microsoft.VisualStudio.Coverage.Analysis.dll」をVisualStudioで参照設定しておきます。

「Microsoft.VisualStudio.Coverage.Symbols.dll」をbin出力フォルダにコピーしてください。

変換用のxsltはこれです。
style.xslt

<?xml version="1.0" encoding="utf-8"?>
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
   <xsl:output method="html" indent="yes"/> 
   <xsl:template match="/" >
      <html>
         <head>
	    <script type="text/javascript" src="http://ajax.googleapis.com/ajax/libs/jquery/1.7.2/jquery.min.js">/* */</script>
            <style type="text/css">
               th {
               background-color:#dcdcdc;
               border:solid 1px #a9a9a9;
               text-indent:2pt;
               font-weight:bolder;
               }
               #data {
               text-align: center;
               }
            </style>

            <script language="JavaScript" type="text/javascript"  >

		  function CreateJavescript(){

		  var fileref=document.createElement('script');
                  fileref.setAttribute("type","text/javascript");
                  //fileref.setAttribute("src", "script1.js");
		  document.getElementsByTagName("head")[0].appendChild(fileref);
		  }

		function toggleDetail(control)
		{
		var ctrlId = $(control).attr('Id');
		$("tr[id='"+ctrlId +"']").toggle();
		}
            </script>
		
            <title>Code Coverage Report</title>
         </head>
         <body onload='CreateJavescript()' >
            <h1>Code Coverage Report</h1>
            <table border="1">
               <tr>
                  <th colspan="3"></th>
                  <th>Name</th>
                  <th>Blocks Covered</th>
                  <th>Blocks Not Covered</th>
                  <th>Coverage</th>
               </tr>
               <xsl:apply-templates select="//CoverageDSPriv/Module" />
            </table>
         </body>
      </html>
   </xsl:template>
 
   <xsl:template match="Module">
      <xsl:variable name="parentId" select="generate-id(./..)" />
      <xsl:variable name="currentId" select="generate-id(.)" />
      <tr id="{$parentId}">
         <td id="{$currentId}"      colspan="3"               onClick="toggleDetail(this)"        onMouseOver="this.style.cursor= 'pointer' ">[+]</td>
         <td><xsl:value-of select="ModuleName" /></td>
         <td id="data"><xsl:value-of select="BlocksCovered" /></td>
         <td id="data"><xsl:value-of select="BlocksNotCovered" /></td>
         <xsl:call-template name="CoverageColumn">
            <xsl:with-param name="covered" select="BlocksCovered" />
            <xsl:with-param name="uncovered" select="BlocksNotCovered" />
         </xsl:call-template>
      </tr>
      <xsl:apply-templates select="NamespaceTable" />
      <tr id="{$currentId}-end" style="display: none;">
         <td colspan="5"></td>
      </tr>
   </xsl:template>
 
   <xsl:template match="NamespaceTable">
      <xsl:variable name="parentId" select="generate-id(./..)" />
      <xsl:variable name="currentId" select="generate-id(.)" />
      <tr id="{$parentId}" style="display: none;">
         <td> - </td>
         <td id="{$currentId}"
               colspan="2"
               onClick="toggleDetail(this)"
               onMouseOver="this.style.cursor= 'pointer' ">[+]</td>
         <td><xsl:value-of select="NamespaceName" /></td>
         <td id="data"><xsl:value-of select="BlocksCovered" /></td>
         <td id="data"><xsl:value-of select="BlocksNotCovered" /></td>
         <xsl:call-template name="CoverageColumn">
            <xsl:with-param name="covered" select="BlocksCovered" />
            <xsl:with-param name="uncovered" select="BlocksNotCovered" />
         </xsl:call-template>
      </tr>
      <xsl:apply-templates select="Class" />
      <tr id="{$currentId}-end" style="display: none;">
         <td colspan="5"></td>
      </tr>
   </xsl:template>
 
   <xsl:template match="Class">
      <xsl:variable name="parentId" select="generate-id(./..)" />
      <xsl:variable name="currentId" select="generate-id(.)" />
      <tr id="{$parentId}" style="display: none;">
         <td> - </td>
         <td> - </td>
         <td id="{$currentId}"
               onClick="toggleDetail(this)"
               onMouseOver="this.style.cursor='pointer' ">[+]</td>
         <td><xsl:value-of select="ClassName" /></td>
         <td id="data"><xsl:value-of select="BlocksCovered" /></td>
         <td id="data"><xsl:value-of select="BlocksNotCovered" /></td>
         <xsl:call-template name="CoverageColumn">
            <xsl:with-param name="covered" select="BlocksCovered" />
            <xsl:with-param name="uncovered" select="BlocksNotCovered" />
         </xsl:call-template>
      </tr>
      <xsl:apply-templates select="Method" />
      <tr id="{$currentId}-end" style="display: none;">
         <td colspan="5"></td>
      </tr>
   </xsl:template>
 
   <xsl:template match="Method">
      <xsl:variable name="parentId" select="generate-id(./..)" />
      <tr id="{$parentId}" style="display: none;">
         <td> -</td>
         <td> - </td>
         <td> - </td>
         <td><xsl:value-of select="MethodName" /></td>
         <td id="data"><xsl:value-of select="BlocksCovered" /></td>
         <td id="data"><xsl:value-of select="BlocksNotCovered" /></td>
         <xsl:call-template name="CoverageColumn">
            <xsl:with-param name="covered" select="BlocksCovered" />
            <xsl:with-param name="uncovered" select="BlocksNotCovered" />
         </xsl:call-template>
      </tr>
   </xsl:template>
 
   <xsl:template name="CoverageColumn">
      <xsl:param name="covered" select="0" />
      <xsl:param name="uncovered" select="0" />
      <td id="data">
         <xsl:variable name="percent"
                              select="($covered div ($covered + $uncovered)) * 100" />
         <xsl:attribute name="style">
            background-color:
            <xsl:choose>
               <xsl:when test="number($percent >= 90)">#86ed60;</xsl:when>
               <xsl:when test="number($percent >= 70)">#ffff99;</xsl:when>
               <xsl:otherwise>#FF7979;</xsl:otherwise>
            </xsl:choose>
         </xsl:attribute>
         <xsl:if test="$percent > 0">
            <xsl:value-of select="format-number($percent, '###.##' )" />%
         </xsl:if>
         <xsl:if test="$percent = 0">
            <xsl:text>0.00%</xsl:text>
         </xsl:if>
      </td>
   </xsl:template>
</xsl:stylesheet>