Debugging and testing stylesheets

The two hard parts of XSLT are knowing which template ran and knowing which one should have. Both have cheap, systematic answers.

Tracing without a debugger

<xsl:template match="item">
  <xsl:message>item id=<xsl:value-of select="@id"/> price=<xsl:value-of select="price"/></xsl:message>

  <!-- Fail loudly when the input breaks an assumption -->
  <xsl:if test="not(@id)">
    <xsl:message terminate="yes">item without an id: <xsl:value-of select="."/></xsl:message>
  </xsl:if>

  <li><xsl:value-of select="title"/></li>
</xsl:template>
<!-- Identity template: the starting point for any debugging fixture.
     Anything that survives to the output was not handled by a real template. -->
<xsl:template match="@*|node()">
  <xsl:copy>
    <xsl:apply-templates select="@*|node()"/>
  </xsl:copy>
</xsl:template>
  • xsl:message writes to stderr, so it never corrupts the result document.
  • terminate="yes" turns a wrong assumption into a failed build rather than subtly wrong output.
  • Comment out a template to see whether it was the one producing the output — with an identity template in place the node passes straight through.
  • Long pipelines are easier to debug as two transforms with an intermediate result you can read.

Fixture-based testing in CI

#!/bin/sh
set -e
# One fixture per behaviour; the expected output is checked in next to it
for input in tests/fixtures/*.xml; do
  name=$(basename "$input" .xml)
  xsltproc transform.xsl "$input" > "build/$name.html"
  diff -u "tests/expected/$name.html" "build/$name.html"
done
# Then validate the output so a well-formedness bug fails the build
xmllint --noout --html build/*.html
echo "stylesheets ok"
SymptomLikely causeCheck
Output is the input unchangedNo template matchedAdd the identity template and see what passes through
Text appears twiceA template both applies templates and copies textLook for value-of and apply-templates on the same node
Attributes missingAn explicit copy template dropped @*Include @* in the apply-templates select
Output not well formedUnescaped markup or an unclosed literal elementRun xmllint on the result
Works on one file, fails on anotherNamespace variation in the inputCompare the root element declarations
💡
Keep one fixture per behaviour rather than one huge document. When a test fails, the file name then tells you which rule broke, and a reviewer can see the intent from the input alone.

FAQ

Is there an interactive XSLT debugger?
Yes: Oxygen XML, Visual Studio's XSLT debugger and several Saxon tooling products step through templates. They are worth it for a complex stylesheet, but xsl:message plus fixtures covers most day-to-day work.
How do I check my output is valid HTML?
Run xmllint --html --noout over the generated files in the build. It catches unclosed literal result elements, which are the most common authoring mistake.

Conditionals, loops and variables Match patterns versus select expressions

Last refreshed 2026-09-18.