Funktionen und Fehlerfälle testen
Bei einer Funktion ist der Einstieg besonders einfach: Parameter hinein, Ergebnis heraus. Unsere Beispiele rechnen ganze Zahlen in Hexadezimalzahlen um und berechnen Potenzen. Daran lassen sich Normalfälle, Grenzwerte und erwartete Fehlermeldungen gut zeigen.
Beide Aufgaben kommen schon in den Schulungsunterlagen vor. Das dort referenzierte Hexadezimal-Stylesheet d2t.hex.xsl fehlt allerdings im Material. Für diesen Artikel gibt es deshalb eine neue Implementierung.
Mit klaren Eingaben anfangen
f:hex nimmt eine ganze Zahl entgegen und liefert einen Hexadezimal-String in Großbuchstaben. Negative Zahlen bekommen ein Minuszeichen. f:power berechnet die Potenz eines Dezimalwerts mit einem nichtnegativen ganzzahligen Exponenten.
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>
Bei der Hexadezimalfunktion lohnt sich zuerst ein Test für null. Rekursive Umrechnungen liefern hier leicht versehentlich einen leeren String. Erwartet wird aber 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>
Die Zahl 300 prüft eine Umrechnung mit mehreren Stellen:
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>
Im Download folgen Tests für 50, 4654, 1234567890 und -16. Das Ergebnis ist immer ein xs:string, auch wenn es nur aus Ziffern besteht. Negative Zahlen werden mit einem Minuszeichen dargestellt, nicht als Zweierkomplement. Hexadezimalzahlen mit Nachkommastellen sind in diesem Beispiel nicht vorgesehen.
Die Potenzfunktion liefert einen xs:decimal. Ihre Tests prüfen unter anderem das Quadrat und die dritte Potenz von 5, eine negative Basis und den Exponenten null. Bei Exponent null liefert diese Beispielbibliothek immer 1, auch für 0^0. Das ist die hier gewählte Programmregel. Die einfache Rekursion ist nicht auf beliebig große Exponenten optimiert.
Auch eine Fehlermeldung kann das richtige Ergebnis sein
Was soll passieren, wenn jemand einen negativen Exponenten übergibt? Unsere Funktion soll nicht irgendeinen Wert liefern, sondern den Fehler e:NEGATIVE_EXPONENT melden. Auch das lässt sich testen.
XSpec kann mit catch="yes" einen dynamischen Fehler abfangen. Die Fehlerdaten stehen dann über $x:result?err zur Verfügung.
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>
Der Test vergleicht den qualifizierten Fehlernamen, also Namensraum und lokalen Namen zusammen. So reicht nicht einfach irgendeine Fehlermeldung mit ähnlichem Text. result-type="map(*)" prüft vorher, ob der erwartete Ergebnistyp vorliegt.
Davon zu unterscheiden ist ein Fehler in der Testumgebung. Ein fehlendes Modul oder ein nicht deklariertes Präfix kann verhindern, dass der Test überhaupt startet. Das bestätigt nicht, dass die Funktion ungültige Eingaben richtig behandelt.
Für eigene Bibliotheken ergibt sich daraus ein einfaches Vorgehen: Für erlaubte Eingaben wird der richtige Wert getestet, für unerlaubte Eingaben die vorgesehene Fehlermeldung. Beides bleibt nützlich, wenn die Funktion später umgeschrieben wird.





