Unit Testing XSLT

Introduction

This is a package that makes it easy to write and run unit tests for XSLT code. All you need is to unzip the .zip file unit-testing.zip somewhere and install Saxon 8.4B (or later), with saxon8.jar in your CLASSPATH.

I've made various upgrades to the testing package since August. These changes are reflected in the text below.

My apologies to Linux and Mac users: I would supply shell files for running the unit tests, but I don't know how to write them. Any kindly soul who wishes to donate is more than welcome: it should all be portable, since it's just Java and XSLT. Actually, I'm no expert at writing Windows batch files either, so if anyone wants to smarten up test.bat they're more than welcome.

Writing the Tests

I've tried to design this package to make writing tests easy. That means avoiding having to write anything that could be created automatically, such as test IDs (although you can supply them), calls to the tested functions or templates or comparison code. You write the tests inside your stylesheet, just before the code that you want to test.

Simple Example

This is a really simple example of a single test that checks the result of calling the eg:square() function with the number parameter set to 2. The expected result is the number 4.

<test:tests>
  <test:test>
    <test:param name="number" select="2" />
    <test:expect select="4" />
  </test:test>
</test:tests>
<xsl:function name="eg:square" as="xs:double">
  <xsl:param name="number" as="xs:double" />
  <xsl:sequence select="$number * $number" />
</xsl:function>

All the testing elements are in the namespace http://www.jenitennison.com/xslt/unit-test, so you need to declare that in your stylesheet and you probably want to prevent namespace declarations for that namespace littering your code, so you need to use the exclude-result-prefixes attribute on <xsl:stylesheet> as follows:

<xsl:stylesheet version="..."
                xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
                ...
                xmlns:test="http://www.jenitennison.com/xslt/unit-test"
                exclude-result-prefixes="... test">
...
</xsl:stylesheet>

The tests that are applicable to a particular template or function are all wrapped in a <test:tests> element. This is primarily to make it easy to collapse them all out of the way (in editors that can do such a thing) so they don't become too distracting when writing the code. Individual tests are given in <test:test> elements.

Both <test:tests> and <test:test> elements can have an id attribute to specify a unique identifier and/or a <test:title> child that gives a human-readable title. Both are optional.

Within the <test:test> element, you need to specify the input to the template or function and the expected output. The input can consist of a context node (for templates only) specified with an optional <test:context> element and any number of parameters specified with <test:param> elements. The expected output must be specified with a <test:expect> element.

The values for the context, parameters and expected result are all defined in the same ways. For atomic values (strings, numbers and the like), use the select attribute, as in the example previously. If you want to specify nodes, you need to supply a document from which the nodes can be selected. You can do this either with the href attribute, which gives a URI for an external document, or by embedding the document (or document fragment) within the relevant element. The select attribute is then used to select nodes within the document; the path it holds is interpreted from the root/document node of the document. The default value for the select attribute is "/*", which selects (all) the document element(s).

Testing using external documents

If you want to test the stylesheet as a whole, the easiest thing to do is to create external input/output files and reference them. Note that the result must be an XML document, so you can't use this to test HTML output.

<test:tests>
  <test:test>
    <test:context href="input.xml" />
    <test:expect href="output.xml" />
  </test:test>
</test:tests>
<xsl:template match="/">
  ...
</xsl:template>
Supplying nodes as context and expected value

You can test with simplified documents by simply embedding the important part within the test itself. The embedded document should include any important ancestors of the relevant element, but doesn't need to include any of the irrelevant parts of the document. Use the select attribute to pick the nodes you want from the document. Remember that if your function or template returns something other than elements, you will need a select attribute on the <test:expect> element too.

<test:tests>
  <test:test>
    <test:title>Empty header cells</test:title>
    <test:context select="/table/tgroup/thead/row/entry">
      <table>
        <tgroup>
          <thead>
            <row>
              <entry />
            </row>
          </thead>
        </tgroup>
      </table>
    </test:context>
    <test:expect>
      <th>&#x160;</th>
    </test:expect>
  </test:test>
</test:tests>    
<xsl:template match="thead/row/entry">
  ...
  <xsl:when test="not(*) and not(normalize-space(.))">
    <th>&#x160;</th>
  </xsl:when>
  ...
</xsl:template>
Differences in whitespace-only text nodes between the expected and actual result are ignored in this version. If you want to generate tests that take whitespace-only text nodes into account, you'll need to configure the testing; see below.

Running the Tests

Quick version: change directory to the directory where you unzipped anything and run test.bat with the stylesheet you want to test as the only argument. The command line looks like:

test stylesheet.xsl
         

Long version: there are three steps, the last one optional, and of course you can reuse the results of any one of them.

  1. Process your stylesheet with generate-tests.xsl. The result is a stylesheet that contains the code for the tests. You can put this wherever you want; test.bat puts it in the same directory as the original stylesheet, and calls it test-stylesheet.xsl. Generating the stylesheet shouldn't produce any errors, unless you've written a test wrong or generate-tests.xsl still contains bugs. I'm afraid that there's not much error-checking code in generate-tests.xsl at the moment, so either could result in errors.

  2. Run the generated stylesheet using anything you like as the source document, or by invoking the main template directly. This runs the unit tests themselves. The result is an XML document holding a report on what tests were run and their results; test.bat puts it in the same directory again as the original stylesheet, and calls it test-stylesheet-result.xml.

    You may get errors at this stage, associated with supplying invalid values for parameters for example. If the stylesheet fails any of the tests, you'll get a message telling you the ID and title of the test. The ID may be gibberish if you haven't supplied any IDs or titles, so this is a good reason to include them. If you don't get any errors or messages, then you know everything's gone smoothly.

  3. Finally, particularly if there were failed tests that you can't trace, you can format the XML report using format-report.xsl. This produces a nice HTML page (well, an HTML page) from the XML report; test.bat puts it in the same directory as the original stylesheet, and calls it test-stylesheet-result.html. You shouldn't get any errors at this stage, but it's always possible. Open up the HTML document and there you have it.

If you want to customize the HTML report that you get, you can edit base.css or (of course), format-report.xsl to your heart's content.

Standalone Test Suites

Wouldn't you know it, while I prefer to have tests embedded in my stylesheet, it turns out other people don't! Well, you can have the best of both worlds.

A standalone test suite has a <test:suite> document element. The <test:suite> element has two attributes: stylesheet, which is a URL (relative to the test suite document) that points to the stylesheet tested by the suite; and date, which is a xs:dateTime that gives the date/time for the suite, useful for versioning.

The <test:suite> element contains one or more <test:tests> elements, which are the same as described above except that they also contain, immediately after the <test:title> element if there is one, a <test:xslt> element. The <test:xslt> element contains either an <xsl:template> or a <xsl:function> element, with name, match and/or mode attributes to identify the template/function being tested but no content.

Standalone Test Suite

Here's what a standalone test suite for a utils.xsl stylesheet, last modified at 12:44 on September 20th 2005, looks like. The only test shown is for the eg:square function.

<test:suite stylesheet="utils.xsl"
            date="2005-09-20T12:44:00">
        
<test:tests>
  <test:xslt>
    <xsl:function name="eg:square" />
  </test:xslt>
  <test:test>
    <test:param name="number" select="2" />
    <test:expect select="4" />
  </test:test>
  ...
</test:tests>
...
</test:suite>

You can use a standalone test suite as the argument to test.bat (or input to generate-tests.xsl if you're doing things by hand), and the tests will be run on the referenced stylesheet.

The extract-tests.xsl stylesheet transforms a stylesheet that has tests embedded in it into a standalone test suite. I haven't done the reverse of that as yet...

Configuring the Testing

You can configure the details of how the testing is carried out, and in particular how sequences/items/nodes are compared, by creating your own implementations of the various functions in generate-tests-utils.xsl. A test:config attribute on the <xsl:stylesheet> element, or a config attribute on the <test:suite> element in a standalone test suite, can point to a stylesheet which contains these implementations in order to override the default behaviour.

Ignoring Insignificant Ordering Differences

By default, this testing package compares the expected and actual results on an item-by-item basis. So if you expect

<xs:all>
  <xs:element ref="foo" />
  <xs:element ref="bar" />
</xs:all>

but you get

<xs:all>
  <xs:element ref="bar" />
  <xs:element ref="foo" />
</xs:all>

then the test fails.

In some cases, it might be that you really don't care what order particular elements appear in, just as long as they're all generated. In this example, the <xs:element> elements appearing in the content of the <xs:all> element can appear in any order with the same meaning.

To configure the testing package to ignore these ordering differences, you can create xsd-config.xsl, which overrides the test:sorted-children function from generate-test-utils.xsl. This ensures that the sequence of <xs:element> elements are sorted by name before being compared, effectively ignoring the original order in which they appeared.

To use xsd-config.xsl, include a reference to it from the test:config attribute on the <xsl:styelsheet> document element.

<xsl:stylesheet version="2.0"
                xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
                xmlns:xs="http://www.w3.org/2001/XMLSchema"
                xmlns:test="http://www.jenitennison.com/xslt/unit-test"
                extension-element-prefixes="test"
                test:config="xsd-config.xsl">
...  
</xsl:stylesheet>

Background

I've heard a lot about XP over the last few years, but never really got into the whole unit testing thing: I think that like a lot of programmers, I viewed writing tests as too much hard work. But during this summer (2005), I'm working on a project that's using an XP development methodology, plus Tim Bray talked glowingly about XP and unit testing during his talk about the XML Summer School. I decided that if I was going to do unit testing, I needed it to be really easy to write and run the tests.

So during the remaining couple of days at the XML Summer School, I hacked up the solution you see here. I tried to remain true to the rest of the XP philosophy: this does the minimum of what I need it to do at the moment, and if I find I need it to do more, then I'll add the code to do it.

The way I use it is that I've rigged up <oXygen/> with an "External Tool" so that I can hit a button and have the tests run and the results come up in the browser. If you use <oXygen/>, it's easy enough to do: go to Tools > External Tools > Preferences, click New and fill in the form as follows (I have the unit testing files in D:\library\unit testing):

Name

Test Stylesheet

Description

Runs the unit tests embedded in the current stylesheet

Working Directory

D:\library\unit testing

Command

"D:\library\unit testing\test.bat" ${cf}