Testing functions and error cases
A function makes a convenient starting point: pass in parameters, check what comes back. Our examples convert integers to hexadecimal strings and calculate powers. They make it easy to try normal cases, boundaries and expected errors.
Both tasks appear in the training material. The referenced hexadecimal stylesheet, d2t.hex.xsl, is missing from the supplied files, so this article provides a new implementation.
Start with clear inputs
f:hex accepts a signed integer and returns an uppercase hexadecimal string. Negative numbers get a minus sign. f:power raises a decimal value to a non-negative integer exponent. Fractional hexadecimal conversion is not part of this example, although fractional input was mentioned as a possible requirement in the original introduction.
examples/01-functions/numbers.xsl
<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet version="3.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns:xs="http://www.w3.org/2001/XMLSchema"
xmlns:f="urn:example:numbers" xmlns:e="urn:example:errors"
exclude-result-prefixes="#all">
<!-- Contract: signed integers, uppercase digits, no prefix. -->
<xsl:function name="f:hex" as="xs:string">
<xsl:param name="value" as="xs:integer"/>
<xsl:sequence select="
if ($value lt 0) then concat('-', f:unsigned-hex(-$value))
else f:unsigned-hex($value)"/>
</xsl:function>
<xsl:function name="f:unsigned-hex" as="xs:string">
<xsl:param name="value" as="xs:integer"/>
<xsl:sequence select="concat(
if ($value ge 16) then f:unsigned-hex($value idiv 16) else '',
substring('0123456789ABCDEF', ($value mod 16) + 1, 1))"/>
</xsl:function>
<!-- Non-negative integer exponents only; power(value, 0) is 1. -->
<xsl:function name="f:power" as="xs:decimal">
<xsl:param name="base" as="xs:decimal"/>
<xsl:param name="exponent" as="xs:integer"/>
<xsl:sequence select="
if ($exponent lt 0) then error(xs:QName('e:NEGATIVE_EXPONENT'),
'The exponent must be non-negative.')
else if ($exponent eq 0) then 1
else $base * f:power($base, $exponent - 1)"/>
</xsl:function>
</xsl:stylesheet>
For hexadecimal conversion, start with zero. Recursive conversions can accidentally return an empty string here. The expected result is 0:
examples/01-functions/numbers.xspec
<x:scenario xmlns:x="http://www.jenitennison.com/xslt/xspec"
xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:f="urn:example:numbers"
xmlns:e="urn:example:errors" label="Convert integer 0 to hexadecimal">
<x:call function="f:hex"><x:param select="0"/></x:call>
<x:expect label="Return 0 as a string" result-type="xs:string" select="'0'"/>
</x:scenario>
The number 300 exercises conversion across several digits:
examples/01-functions/numbers.xspec
<x:scenario xmlns:x="http://www.jenitennison.com/xslt/xspec"
xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:f="urn:example:numbers"
xmlns:e="urn:example:errors" label="Convert integer 300 to hexadecimal">
<x:call function="f:hex"><x:param select="300"/></x:call>
<x:expect label="Return 12C as a string" result-type="xs:string" select="'12C'"/>
</x:scenario>
The download adds tests for 50, 4654, 1234567890 and -16. The hexadecimal result is always an xs:string, even when it contains only digits. Negative numbers use a minus sign, not a two's-complement representation.
The power function returns an xs:decimal. Its tests include the square and cube of 5, a negative base and exponent zero. For exponent zero, this example always returns 1, including for 0^0. That is the programming rule chosen here. The simple recursion is not optimized for arbitrarily large exponents.
An error can be the right result
What should happen when a caller supplies a negative exponent? Our function should report e:NEGATIVE_EXPONENT, rather than returning an arbitrary value. That behavior can be tested too.
XSpec can capture a dynamic error with catch="yes". The error information is then available through $x:result?err.
examples/01-functions/numbers.xspec
<x:scenario xmlns:x="http://www.jenitennison.com/xslt/xspec" xmlns:xs="http://www.w3.org/2001/XMLSchema"
xmlns:f="urn:example:numbers" xmlns:e="urn:example:errors" label="Reject a negative exponent" catch="yes">
<x:call function="f:power"><x:param select="2"/><x:param select="-1"/></x:call>
<x:expect label="Raise the documented error" result-type="map(*)"
test="$x:result?err?code eq xs:QName('e:NEGATIVE_EXPONENT')"/>
</x:scenario>
The test compares the qualified error name, including its namespace. An unrelated error with similar wording is not enough. result-type="map(*)" checks the result type before the test accesses the captured error information.
Keep this separate from a problem in the test setup. A missing module or undeclared prefix may prevent the test from compiling at all. That does not show that the function correctly rejects an invalid input.
For your own library, the approach is simple: check the correct value for allowed inputs and the intended error for disallowed ones. Both sets of tests remain useful when the function is rewritten later. For production use, also decide on numeric precision and resource limits instead of assuming that a few passing examples settle those questions.

