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:messagewrites 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"| Symptom | Likely cause | Check |
|---|---|---|
| Output is the input unchanged | No template matched | Add the identity template and see what passes through |
| Text appears twice | A template both applies templates and copies text | Look for value-of and apply-templates on the same node |
| Attributes missing | An explicit copy template dropped @* | Include @* in the apply-templates select |
| Output not well formed | Unescaped markup or an unclosed literal element | Run xmllint on the result |
| Works on one file, fails on another | Namespace variation in the input | Compare 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.
Related
Conditionals, loops and variables Match patterns versus select expressions
Last refreshed 2026-09-18.