The new .Net Core CLI has the ability to use xunit or nunit. Which is great. For running tests locally.

I use TeamCity, so I like to see test results showing up, track testing count over time etc. There is a built in way to import xml reports which supports junit (which is xunit-compatible) and nunit. But choosing nunit means support for the nunit2 xml format. And guess what? The new .Net Core test runner for nunit only outputs in nunit3 format.

Now, the decision makes perfect sense to me. The new format is more expressive, gives more information and is the future. And vendors need to start using it and move away from the old format. But as you can imagine, JetBrains just hasn’t gotten around to getting it right yet. (There is some support in TC 10.0.2 but it seems there might be some teething issues and many of us are still on TC 9.X anyway.)

So what do I do about this? Solve it with code of course! And the solution is xslt (Hey! it gets the job done ok?)

Thanks to some notes on this related Jenkins issue from Paul Hicks, I have this xslt page (with a small tweak to fix a value casing issue):

<?xml version="1.0" encoding="utf-8"?>
<xsl:stylesheet version="2.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
  <xsl:output method="xml" indent="yes"/>

  <xsl:template match="/test-run">
    <testsuites tests="{@testcasecount}" failures="{@failed}" disabled="{@skipped}" time="{@duration}">
      <xsl:apply-templates/>
    </testsuites>
  </xsl:template>

  <xsl:template match="test-suite">
    <xsl:if test="test-case">
      <testsuite tests="{@testcasecount}" time="{@duration}" errors="{@testcasecount - @passed - @skipped - @failed}" failures="{@failed}" skipped="{@skipped}" timestamp="{@start-time}">
        <xsl:attribute name="name">
          <xsl:for-each select="ancestor-or-self::test-suite/@name">
            <xsl:value-of select="concat(., '.')"/>
          </xsl:for-each>
        </xsl:attribute>
        <xsl:apply-templates select="test-case"/>
      </testsuite>
      <xsl:apply-templates select="test-suite"/>
    </xsl:if>
    <xsl:if test="not(test-case)">
      <xsl:apply-templates/>
    </xsl:if>
  </xsl:template>

  <xsl:template match="test-case">
    <xsl:variable name="lowerResult" select="translate(@result,'ABCDEFGHIJKLMNOPQRSTUVWXYZ', 'abcdefghijklmnopqrstuvwxyz')"/>
    <testcase name="{@name}" assertions="{@asserts}" time="{@duration}" status="{$lowerResult}" classname="{@classname}">
      <xsl:if test="@runstate = 'Skipped' or @runstate = 'Ignored'">
        <skipped/>
      </xsl:if>
      
      <xsl:apply-templates/>
    </testcase>
  </xsl:template>

  <xsl:template match="command-line"/>
  <xsl:template match="settings"/>

  <xsl:template match="output">
    <system-out>
      <xsl:value-of select="output"/>
    </system-out>
  </xsl:template>

  <xsl:template match="stack-trace">
  </xsl:template>

  <xsl:template match="test-case/failure">
    <failure message="{./message}">
      <xsl:value-of select="./stack-trace"/>
    </failure>
  </xsl:template>

  <xsl:template match="test-suite/failure"/>

  <xsl:template match="test-case/reason">
    <skipped message="{./message}"/>
  </xsl:template>

  <xsl:template match="test-suite/reason"/>

  <xsl:template match="properties"/>
</xsl:stylesheet>

And we can use this in a TeamCity runner:

$xslt = new-object system.xml.xsl.xslcompiledtransform
$xslt.load('%system.teamcity.build.workingDir%\nunit3-junit.xslt')
$xslt.Transform('%system.teamcity.build.workingDir%\TestResult.xml', '%system.teamcity.build.workingDir%\TransformedTestResult.xml')
Write-Host "##teamcity[importData type='junit' path='TransformedTestResult.xml']"

Which is PowerShell, and so this works well under windows.

But our latest thing is .Net Core in Docker containers on Linux, right? (right?) So we don’t have PowerShell available. And it also seems that there is currently no xslt support in .Net Core, so that option is out. But there is another way:

apt-get install libsaxon-java
saxon-xslt -o TransformedTestResult.xml TestResult.xml nunit3-junit.xslt
echo "##teamcity[importData type='junit' path='TransformedTestResult.xml']"

You may want to just run the apt-get command on all your agents, rather than putting it into your build step.

And with that, we can publish our test results to TeamCity on Windows and on Linux build agents.