The Curious Dev

Various programming sidetracks and shiny-object detours

Testing XSL - Part 2

In Part 1 of this Testing XSL series I introduced how XSL can be used and some basic ways to test their correctness.

In this post I’m going to go into further detail with some utility methods that greatly reduce the amount of code you need to test the XSL on a more fine-grained level. They also abstract away any need to worry about DocumentBuilderFactories or Transformers and let you deal with just strings.

One thing about the XML diffs is that they’re quite brittle, if you break one thing it tends to push all the elements out of place and you could end up with hundreds of errors that make it difficult to work out what you broke. You also have to provide a full expected sample of what you expect from the XSL, which might not always be something you want to do just yet.

By writing a simple unit test to execute the XSL with a given input XML you can confirm various pieces of the puzzle are correct, without having to do it all at once.

Utility Methods

I’ve written three new utility methods to make this easy and some accompanying tests to demonstrate and test them, continuing with my “countries” example from Part 1.

First up, countOccurrencesWithXpath which allows one to simply provide an XPATH to count the elements and you can then use this in your test to confirm correctness.

public static Integer countOccurrencesWithXpath(String xmlDocument, String xpath) {
    try {
        Document resultDoc = loadDocumentFromString(xmlDocument)

        // run XPATH on provided XML
        XPath theXpath = XPathFactory.newInstance().newXPath()
        String xpathExp = "count(" + xpath + ")"
        XPathExpression expr = theXpath.compile(xpathExp)
        Number count = (Number) expr.evaluate(resultDoc, XPathConstants.NUMBER)
        println(xpathExp + " = " + count.intValue())

        return count.intValue()
    } catch (Exception e) {
        println(e.getMessage())
        return -1
    }
}

Here we’re simply taking in the XML payload and a provided XPATH and executing it with the count function.

The test to demonstrate this is very simple and demonstrates how easily one could introduce this functionality to an existing project. The XPATH used here //Country/Capital is simply to find just the Capital elements that are under Country elements.

@Test
def void testCountryTransformWithCountOfOccurrences() {
    String result = XslUtils.transformXmlWithXsl(
            XslUtils.loadXmlFromFile("xml/countrylist.xml"),
            XslUtils.loadXslFromFile("xsl/TransformCountries.xsl"))
    assertNotNull(result)
    println result

    //check that the transform has produced the 2 expected Country elements
    assertEquals(2, XslResultUtils.countOccurrencesWithXpath(result, "//Country"))
    
    //check that the transform has produced the 2 expected Capital elements
    assertEquals(2, XslResultUtils.countOccurrencesWithXpath(result, "//Country/Capital"))
}

The next method I’ve written is checkElementValueExists which allows one to easily check for the existence of a particular element and it’s value via a simple XPATH. This could be done via a simple string check on the XML payload string, but tends to be more brittle if namespaces are being used or especially if you’ve got the same common element name at different levels in the document structure (such as ‘Name’ or ‘Value’).

public static boolean checkElementValueExists(String xmlDocument, String xpath, String expectedVal) {
    try {
        Document resultDoc = loadDocumentFromString(xmlDocument)
        boolean found = false

        // run XPATH on provided XML
        XPath theXpath = XPathFactory.newInstance().newXPath()
        String xpathExp = xpath.startsWith("//") ? "${xpath}" : "//${xpath}"
        XPathExpression expr = theXpath.compile(xpathExp)
        NodeList nodeSet = (NodeList)expr.evaluate(resultDoc, XPathConstants.NODESET)

        // loop through nodeSet and check that an expected value exists
        nodeSet.each { Node node ->
            if (expectedVal.equals(node.getTextContent())) {
                found = true
            }
        }

        return found
    } catch (Exception e) {
        println(e.getMessage())
        return false
    }
}

The method essentially executes the provided XPATH and then loops through the results trying to match the expectedVal parameter.

The test to go along with this is trivial also, with a suitable XPATH to find the element you’re checking and the expected value.

@Test
def void testCountryTransformWithElementValue() {
    String result = XslUtils.transformXmlWithXsl(
            XslUtils.loadXmlFromFile("xml/countrylist.xml"),
            XslUtils.loadXslFromFile("xsl/TransformCountries.xsl"))
    assertNotNull(result)
    println result

    //check that one of the expected Capital elements contains 'Canberra'
    assertTrue(result.contains("<Capital>Canberra</Capital>"))  // <-- old way
    assertTrue(XslResultUtils.checkElementValueExists(result, "//Country/Capital", "Canberra")) // <-- better way
    
    //check that we're getting a negative when we should be
    assertFalse(XslResultUtils.checkElementValueExists(result, "//Capital", "Sydney"))
}

Here, both the string check way and the XPATH way are demonstrated.

The third and final test utility method is validateXmlWithSchema which allows one to provide an XML Schema (or more accurately the path to one) against which the provided XML document will be validated.

public static List<String> validateXmlWithSchema(String xmlDocument, String schemaPath) {
    StreamSource sourceXml = new StreamSource(new StringReader(xmlDocument))
    SchemaFactory sf = SchemaFactory.newInstance(XMLConstants.W3C_XML_SCHEMA_NS_URI)
    Schema schema = sf.newSchema(XslUtils.loadXslFromFile(schemaPath))
    Validator validator = schema.newValidator()
    MyErrorHandler errorHandler = new MyErrorHandler()

    try {
        validator.setErrorHandler(errorHandler)
        validator.validate(sourceXml);
    } catch (SAXException e) {
        e.printStackTrace()
    }

    List<String> errorsList = errorHandler.getErrors()
    errorsList.each { println it }

    return errorsList
}

Here’s the “countries” schema I’ve created to use in this example:

<xs:schema elementFormDefault="qualified" xmlns:xs="http://www.w3.org/2001/XMLSchema">
    <xs:element name="Countries">
        <xs:complexType>
            <xs:sequence>
                <xs:element name="Country" type="countryType" minOccurs="1" maxOccurs="unbounded"/>
            </xs:sequence>
        </xs:complexType>
    </xs:element>
    <xs:complexType name="countryType">
        <xs:sequence>
            <xs:element name="Name" type="xs:string"/>
            <xs:element name="Capital" type="xs:string"/>
            <xs:element name="TLD" type="xs:string" minOccurs="0"/>
        </xs:sequence>
    </xs:complexType>
</xs:schema>

It is quite basic, but our XML payload can still be tested with the above method, as demonstrated here:

@Test
def void testCountryTransformValidatesToSchema() {
    String schemaLocation = "xsd/Countries.xsd"
    String result = XslUtils.transformXmlWithXsl(
            XslUtils.loadXmlFromFile("xml/countrylist.xml"),
            XslUtils.loadXslFromFile("xsl/TransformCountries.xsl")
    )

    assertNotNull(result)
    assertEquals(0, XslResultUtils.validateXmlWithSchema(result, schemaLocation).size())
}

The validateXmlWithSchema method simply returns a List of Strings for where there are validation errors, if the list is empty then there are no errors.

But if I were to use a different XSL which produces a different result, then the test will fail: Failed Validation Test

Running Tests With Gradle

As I’ve Gradlified this project I can now simply call the gradle wrapper to test with gradlew clean test and a clean looking test report is produced:

Test Report

Code

The update repository is here on Bitbucket. Included are the above demonstrated tests and over the next while I might add some more to test the methods more negatively.

Summary

Hopefully this post has helped to demonstrate how your XSL development task can be made easier with the addition of a very simple test library.

In the next post I intend to go into the process of adding test coverage to your XSLs to see how well your unit tests are exercising them.

Comments

Included file 'facebook_like.html' not found in _includes directory