Praxisbeispiel: Excel-Spaltennamen testen
Aus 1 wird A, aus 26 wird Z und aus 27 wird AA. Eine solche Funktion braucht man zum Beispiel, wenn eine Verarbeitung Zelladressen für eine Excel-Datei erzeugt. Die Umrechnung wirkt einfach, hat aber ein paar Stellen, an denen sich leicht Fehler einschleichen.
Das Beispiel stammt aus unserer XSpec-Schulung. Im Material liegen mehrere Lösungen, eine Tabelle mit 16.384 erwarteten Werten und ein Generator für Tests. Die Grundidee ist praktisch: Eine neue Funktion soll dieselben Spaltennamen liefern wie die vorhandene Referenz.
Besonders auf die Übergänge achten
Excel-Spaltennamen funktionieren nicht ganz wie eine normale Zahlendarstellung zur Basis 26. Es gibt keine Ziffer für null. Nach Z folgt AA, nach ZZ folgt AAA. Deshalb wird im folgenden Algorithmus vor Division und Restbildung jeweils eins abgezogen.
Die Funktion erlaubt Spaltennummern von 1 bis 16.384. Das entspricht der Excel-Arbeitsblattgrenze; die letzte Spalte heißt XFD. Für eine allgemeine Buchstabennummerierung könnte man einen größeren Bereich vorsehen, aber darum geht es in diesem Beispiel nicht.
examples/04-columns/column-name.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:columns" xmlns:e="urn:example:errors"
exclude-result-prefixes="#all">
<!-- This public function deliberately enforces the Excel column range. -->
<xsl:function name="f:column-name" as="xs:string">
<xsl:param name="number" as="xs:integer"/>
<xsl:sequence select="
if ($number lt 1 or $number gt 16384)
then error(xs:QName('e:OUT_OF_RANGE'),
'The column number must be between 1 and 16384.')
else f:letters($number)"/>
</xsl:function>
<xsl:function name="f:letters" as="xs:string">
<xsl:param name="number" as="xs:integer"/>
<xsl:sequence select="
if ($number eq 0) then ''
else concat(f:letters(($number - 1) idiv 26),
codepoints-to-string(65 + (($number - 1) mod 26)))"/>
</xsl:function>
</xsl:stylesheet>
Die öffentliche Funktion kontrolliert zuerst den Wertebereich. Danach berechnet die rekursive Hilfsfunktion den Namen. Null, negative Zahlen und zu große Werte sollen eine Fehlermeldung auslösen.
Zwei besonders nützliche Tests liegen direkt an der Grenze zwischen zwei und drei Buchstaben:
examples/04-columns/column-name.xspec
<x:scenario xmlns:x="http://www.jenitennison.com/xslt/xspec" xmlns:xs="http://www.w3.org/2001/XMLSchema"
xmlns:f="urn:example:columns" xmlns:e="urn:example:errors" label="Column 702 is ZZ">
<x:call function="f:column-name"><x:param select="702"/></x:call>
<x:expect label="Return ZZ" select="'ZZ'" result-type="xs:string"/>
</x:scenario>
examples/04-columns/column-name.xspec
<x:scenario xmlns:x="http://www.jenitennison.com/xslt/xspec" xmlns:xs="http://www.w3.org/2001/XMLSchema"
xmlns:f="urn:example:columns" xmlns:e="urn:example:errors" label="Column 703 is AAA">
<x:call function="f:column-name"><x:param select="703"/></x:call>
<x:expect label="Return AAA" select="'AAA'" result-type="xs:string"/>
</x:scenario>
Die kleine Testsuite enthält 15 gültige Eingaben und drei Fehlerfälle: 0, -1 und 16.385. Solche kurzen Tests sind im Alltag angenehm, weil sich bei einem Fehler sofort erkennen lässt, welcher Übergang betroffen ist.
Alle 16.384 Werte prüfen
Bei diesem Beispiel ist der Eingabebereich klein genug, um jeden erlaubten Wert zu testen. Die Erwartungswerte kommen aus der vorhandenen Datei colnames.xml. Sie werden nicht mit der neuen Funktion erzeugt. Sonst könnte derselbe Fehler auf beiden Seiten des Vergleichs stehen und unbemerkt bleiben.
Für das Paket wurden alle 16.384 Paare zusätzlich mit einer unabhängigen Python-Berechnung verglichen. Es gab keine Abweichung. Damit sind die Referenzdaten gegengeprüft. Der XSpec-Lauf der neuen XSLT-Funktion ist davon getrennt und steht noch aus.
Die kompakte Vollprüfung verarbeitet die gesamte Referenztabelle. Bei Fehlern gibt sie die betroffenen Nummern und Namen aus. Der zugehörige XSpec-Test bleibt dabei sehr kurz:
examples/04-columns/exhaustive.xspec
<?xml version="1.0" encoding="UTF-8"?>
<x:description xmlns:x="http://www.jenitennison.com/xslt/xspec"
stylesheet="exhaustive-check.xsl">
<x:scenario label="Compare all 16384 values with the independent fixture">
<x:context href="column-cases.xml"/>
<x:expect label="Check every value without a mismatch">
<verification checked="16384" mismatches="0"/>
</x:expect>
</x:scenario>
</x:description>
Hier prüft eine XSpec-Erwartung die Zusammenfassung: 16.384 Werte verarbeitet, keine Abweichung. Im Bericht steht deshalb nicht automatisch ein eigener Testeintrag für jede Spaltennummer.
Für jeden Wert einen eigenen Test erzeugen
Manchmal ist genau das gewünscht: ein Bericht mit einem Szenario pro Eingabe. Dafür enthält das Paket einen Generator:
examples/04-columns/generate-tests.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:x="http://www.jenitennison.com/xslt/xspec"
xmlns:f="urn:example:columns" exclude-result-prefixes="xs">
<xsl:output method="xml" indent="yes"/>
<xsl:param name="from" as="xs:integer" select="1"/>
<xsl:param name="to" as="xs:integer" select="16384"/>
<xsl:template match="/">
<!-- The output belongs in this example's generated/ directory. -->
<x:description stylesheet="../column-name.xsl">
<xsl:namespace name="f" select="'urn:example:columns'"/>
<xsl:for-each select="cases/case[xs:integer(@number) ge $from
and xs:integer(@number) le $to]">
<x:scenario label="Generated test for column {@number}">
<x:call function="f:column-name"><x:param select="{@number}"/></x:call>
<x:expect label="Return {@expected}" select="'{@expected}'"/>
</x:scenario>
</xsl:for-each>
</x:description>
</xsl:template>
</xsl:stylesheet>
So werden die Tests für den gesamten Bereich erzeugt:
python scripts/generate-individual-tests.py --start 1 --end 16384
Die neue Datei liegt unter examples/04-columns/generated/individual.xspec. Danach muss sie noch mit XSpec ausgeführt werden. Zum Ausprobieren kann der Bereich mit --start und --end verkleinert werden, beispielsweise auf die Zahlen rund um 702 und 703.
Der Generator wählt die tatsächlichen Spaltennummern aus. Das ist leichter nachzuvollziehen als mehrere aufeinanderfolgende Filter mit position(), bei denen sich die Position nach jedem Filter verschieben kann.
Viele einzelne Szenarien ergeben einen ausführlichen Bericht, brauchen aber mehr Aufwand für Generierung, Kompilierung und Ausgabe. Die kompakte Vollprüfung eignet sich für regelmäßige Durchläufe, die Einzelszenarien für detaillierte Berichte. Die rund 15 Minuten aus der Schulung von 2013 waren eine damalige Messung, keine Laufzeitangabe für die neue Umgebung.





