Adaptable Code Reuse in XSpec, Part 2
XPath Matching
This topic builds on Adaptable Code Reuse in XSpec, Part 1, which shows how to use <x:like>
and <x:variable>
to bring variants of XSpec code from a scenario into multiple other XSpec scenarios. In that topic, we varied text content using text value templates, and we varied attribute values using attribute value templates. Not every reuse situation lends itself to such value templates. Here, we explore another way to create code variants, using the flexibility of XPath expressions. This way lets you reuse elements and other non-string data.
To go even further and see how to reuse functionality in the form of custom XPath functions, see Testing a Thin-Wrapper XSLT Template Without Excessive Repetition (Option 3).
Example Checking a Map
Consider an XSLT function that creates a map data type, where the value of the 'duplicates'
entry can vary according to the function's ignore-dup
input.
Figure 1. XSLT Code to Test
<xsl:function name="mf:options" as="map(*)">
<xsl:param name="ignore-dup" as="xs:boolean"/>
<xsl:map>
<xsl:map-entry
key="'duplicates'"
select="
if ($ignore-dup)
then 'use-last'
else 'reject'
"/>
<xsl:map-entry key="'escape'" select="false()"/>
</xsl:map>
</xsl:function>
(If the output map looks familiar, that’s because it’s an input to the parse-json
XPath function. If the map doesn't look familiar, that's fine; you don't need to know about parse-json
to understand this example.)
The function input can be true or false. Let’s test both cases. Here is a fragment that sets up the function calls. It has no <x:expect>
elements yet.
Figure 2. Partial Scenario to Check Output Map with True and False Inputs
<!-- This scenario is not complete yet -->
<x:scenario label="Tests for mf:options function">
<x:scenario label="Ignore duplicates">
<x:call function="mf:options">
<x:param select="true()"/>
</x:call>
<!-- x:expect elements go here -->
</x:scenario>
<x:scenario label="Error on duplicates">
<x:call function="mf:options">
<x:param select="false()"/>
</x:call>
<!-- x:expect elements go here -->
</x:scenario>
</x:scenario>
Matching the Entire Result
If we want to check the entire map’s data in one <x:expect>
element, we can use syntax like the following.
<x:expect label="Check entire map"
select="map{
'duplicates': 'use-last',
'escape': false()
}"/>
This syntax can use an XSpec variable instead of hard-coding 'use-last'
. Also, we can place <x:expect>
in a shared scenario to prepare for pulling code into the function-call sub-scenarios shown earlier. Here's the shared scenario with the variable usage.
Figure 3. Adaptable Way to Check Entire Map
<!-- Scenario containing <x:like> must have $xv:duplicates -->
<x:scenario shared="yes" label="Check map">
<x:expect label="Check entire map"
select="map{
'duplicates': $xv:duplicates,
'escape': false()
}"/>
</x:scenario>
Now we can augment the two sub-scenarios by defining xv:duplicates
and inserting <x:like label="Check map"/>
:
Figure 4. Full Scenario to Check Output Map with True and False Inputs
<x:scenario label="Tests for mf:options function">
<x:scenario label="Ignore duplicates">
<x:call function="mf:options">
<x:param select="true()"/>
</x:call>
<x:variable name="xv:duplicates" select="'use-last'"/>
<x:like label="Check map"/>
</x:scenario>
<x:scenario label="Error on duplicates">
<x:call function="mf:options">
<x:param select="false()"/>
</x:call>
<x:variable name="xv:duplicates" select="'reject'"/>
<x:like label="Check map"/>
</x:scenario>
</x:scenario>
Matching a Piece of the Result
We were lucky to find a syntax for expressing the entire result of the function with an embedded variable reference. Let’s stay with this example a bit longer, to find an alternative approach that generalizes to situations where we’re not that lucky.
The big idea: From the function call’s actual value (
$x:result
), extract a nugget of information and compare it against an XSpec variable.
We can extract information from a map by accessing an entry, such as the 'duplicates'
entry. The following code uses the test
attribute to focus on the function result's 'duplicates'
entry, while the select
attribute says that the thing we're focusing on should match $xv:duplicates
.
Figure 5. Adaptable Way to Check Individual Map Entry
<!-- Scenario containing <x:like> must have $xv:duplicates -->
<x:scenario shared="yes" label="Check map">
<x:expect label="Approach 2: Check 'duplicates' value"
test="$x:result('duplicates')"
select="$xv:duplicates"/>
<x:expect label="Check that 'escape' value is false"
test="not($x:result('escape'))"/>
</x:scenario>
This approach is worth learning, because the idea of extracting a nugget of information from $x:result
is applicable in many situations. That's because the test
attribute of <x:expect>
is an XPath expression and XPath excels at pulling information out of the data structures you meet in the XML world.
With that backdrop in mind, let’s look at a situation where the adaptability we need for XSpec code reuse involves a fragment of XML within a larger XML tree.
Example Matching XML Subtree
Suppose we need to write and test an XSLT function with the following characteristics:
- Input is a JSON string
- Output is an XML tree that is equivalent to the JSON except for having one extra property,
revision
, with value"1.0"
Suppose also that the project’s specification says the function must support a JSON property named isbn
whose value can be either a string or an array of strings.
For a hypothetical input { "year": "1900", "isbn": "978-0470192740" }
, the function output should be
<map>
<string key="revision">1.0</string>
<string key="year">1900</string>
<string key="isbn">978-0470192740</string>
</map>
For an input { "year": "1900", "isbn": ["0764547763","978-0764547768"] }
, the function output should be
<map>
<string key="revision">1.0</string>
<string key="year">1900</string>
<array key="isbn">
<string>0764547763</string>
<string>978-0764547768</string>
</array>
</map>
Building Up or Breaking Down?
The two outputs are similar, except the last child of <map>
. We can use an XSpec variable to store this last child in each of two test scenarios. How do we use the variable, though? We can't build up an expected value by hard-coding the common elements and embedding an XSpec variable; syntax like the following won't work in XSpec:
<x:expect label="This syntax does not work in XSpec!">
<map>
<string key="revision">1.0</string>
<string key="year">1900</string>
{ $xv:isbn-in-xml }
</map>
</x:expect>
The big idea: Instead of building up a complete expected value to compare with the actual value, try the opposite: break down the actual value to compare pieces with partial expected values.
As in the “Matching a Piece of the Result” section earlier, the test
attribute of <x:expect>
can use an XPath expression to extract information from the function result. Here's a shared scenario whose <x:expect>
elements check the function result's high-level structure followed by its smaller pieces.
Figure 6. Adaptable Way to Check ISBN Child Element and Rest of Map
<!-- Scenario containing <x:like> must have $xv:isbn-in-xml -->
<x:scenario shared="yes" label="Check 'map' element"
xmlns="http://www.w3.org/2005/xpath-functions">
<x:expect label="Result is a map element">
<map>...</map>
</x:expect>
<x:expect label="Check children other than isbn"
test="$x:result/*[not(@key='isbn')]">
<string key="revision">1.0</string>
<string key="year">1900</string>
</x:expect>
<x:expect label="Check isbn"
test="$x:result/*[@key='isbn']"
select="$xv:isbn-in-xml"/>
</x:scenario>
Reuse Across Sub-Scenarios
Here’s a scenario that uses the shared scenario twice, with different values of the xv:isbn-in-xml
variable.
Figure 7. Scenario to Check Output XML with String and Array ISBN Inputs
<x:scenario label="Tests for mf:json-processing function (some reuse)"
xmlns="http://www.w3.org/2005/xpath-functions">
<x:scenario label="String values for all properties">
<x:call function="mf:json-processing">
<x:param select="/string()">
{
"year": "1900",
"isbn": "978-0470192740"
}
</x:param>
</x:call>
<x:variable name="xv:isbn-in-xml">
<string key="isbn">978-0470192740</string>
</x:variable>
<x:like label="Check 'map' element"/>
</x:scenario>
<x:scenario label="Array in isbn">
<x:call function="mf:json-processing">
<x:param select="/string()">
{
"year": "1900",
"isbn": ["0764547763","978-0764547768"]
}
</x:param>
</x:call>
<x:variable name="xv:isbn-in-xml">
<array key="isbn">
<string>0764547763</string>
<string>978-0764547768</string>
</array>
</x:variable>
<x:like label="Check 'map' element"/>
</x:scenario>
</x:scenario>
Bonus: Reuse Within Each Sub-Scenario
We can go a step further by reducing the repetition of ISBN values between each sub-scenario’s function parameter and variable definition.
Figure 8. Revised Scenario to Check Output XML with String and Array ISBN Inputs
<x:scenario label="Tests for mf:json-processing function (with reuse)"
xmlns="http://www.w3.org/2005/xpath-functions">
<x:scenario label="String values for all properties">
<x:variable name="xv:isbn-value" select="'978-0470192740'"/>
<x:call function="mf:json-processing">
<x:param expand-text="1" select="/string()">
{{
"year": "1900",
"isbn": "{$xv:isbn-value}"
}}
</x:param>
</x:call>
<x:variable name="xv:isbn-in-xml" expand-text="1">
<string key="isbn">{$xv:isbn-value}</string>
</x:variable>
<x:like label="Check 'map' element"/>
</x:scenario>
<x:scenario label="Array in isbn">
<x:variable name="xv:isbn-value1" select="'0764547763'"/>
<x:variable name="xv:isbn-value2" select="'978-0764547768'"/>
<x:call function="mf:json-processing">
<x:param expand-text="1" select="/string()">
{{
"year": "1900",
"isbn": ["{$xv:isbn-value1}","{$xv:isbn-value2}"]
}}
</x:param>
</x:call>
<x:variable name="xv:isbn-in-xml" expand-text="1">
<array key="isbn">
<string>{$xv:isbn-value1}</string>
<string>{$xv:isbn-value2}</string>
</array>
</x:variable>
<x:like label="Check 'map' element"/>
</x:scenario>
</x:scenario>
Notice a few things about this revised code:
- This form of reuse takes advantage of text value templates (
expand-text="1"
). - When using
expand-text="1"
in the function parameters (<x:param>
), we must double the curly braces when we mean them literally, as in JSON syntax. Single curly braces indicate text value templates. - The variables named
xv:isbn-value
,xv:isbn-value1
, andxv:isbn-value2
have only local relevance. They do not appear in the shared scenario code that<x:like>
pulls in. That's different from the rolexv:isbn-in-xml
plays in the shared and non-shared scenarios.
For the Record, the XSLT Function
Having a viable test, we can write the XSLT function that makes the test pass.
Figure 9. XSLT Code to Test
<xsl:function name="mf:json-processing" as="element(fn:map)">
<xsl:param name="json-string" as="xs:string"/>
<map xmlns="http://www.w3.org/2005/xpath-functions">
<!-- revision property -->
<string key="revision">1.0</string>
<!-- properties from $json-string -->
<xsl:sequence select="json-to-xml($json-string)/fn:map/*"/>
</map>
</xsl:function>
Example with Boolean Test
This next example uses the test
attribute of <x:expect>
in a fundamentally different way: instead of indicating which part of the result to focus on so that children or a select
attribute can match against it, the test
attribute can contain a Boolean expression. In the Boolean case, <x:expect>
has neither children nor a select
attribute. If the test
condition is true for the actual result in $x:result
, the <x:expect>
element passes. What this example has in common with the earlier ones is the use of XSpec variables in <x:expect>
elements in shared scenarios, making them adaptably reusable.
The following shared scenario is from the xslt3-functions repository on GitHub. The scenario is for testing a function that generates a sequence of random UUIDs (universally unique identifiers). The exact UUID values are not especially meaningful, so the code doesn’t use XSpec variables to store specific UUID sequences as expected values.
Instead, the code checks that the sequence has certain required characteristics. Each <x:expect>
element checks a required characteristic, by expressing a true/false condition. To make the code adaptable for multiple scenarios, some of the true/false conditions rely on a sequence length or seed stored in XSpec variables. (By slight contrast, $ov:uuid-v4-regex
is a global XSpec variable and is unrelated to adaptability of this scenario.)
Figure 10. Shared Scenario with Boolean Tests Using XSpec Variables
<x:scenario shared="yes" label="SHARED: Check sequence of uuids">
<!-- This set of shared assertions expects the referencing
scenario or its ancestor to have defined
<x:variable name="ov:seq-length" .../> and
<x:variable name="ov:seed" .../> -->
<x:expect label="Correct number of strings"
test="$x:result instance of xs:string+ and
count($x:result) eq $ov:seq-length"/>
<x:expect label="Each string matches uuid regular expression"
test="every $uuid in $x:result satisfies
matches($uuid, $ov:uuid-v4-regex)"/>
<x:expect label="Check repeatability for same seed"
test="deep-equal(
$x:result,
x3f:make-uuid-sequence($ov:seed, $ov:seq-length)
)"/>
<x:expect label="Not typically equal to function output for a different seed"
test="not(deep-equal(
$x:result,
x3f:make-uuid-sequence($ov:seed || '1', $ov:seq-length)
))"/>
</x:scenario>
This example illustrates another part of a thought process about reusing <x:expect>
elements. If you're inclined to use <x:like>
to pull in content of a shared scenario and you need some variability across destinations, think about what form of variability is most helpful. Is storing pieces of expected values in XSpec variables not the best design for your situation, because you don't really want to check specific values? Think about checking characteristics or conditions, as this code does, and use the variables to store whatever helps you express the Boolean tests in an adaptable way.
Key Takeaways
- Adaptably reusing XSpec code is more flexible than just varying strings. We looked at cases where the variability occurs in a map entry, an XML subtree, and true/false XPath expressions.
- Use per-scenario XSpec variables to store nuggets of expected-value information — or other information. In some cases, you can build up the complete expected value using a syntax that permits XSpec variables. If not, break down the actual value to focus on pieces that either match your XSpec variables or satisfy Boolean tests that you express using XSpec variables.
- The roles of
<x:like>
,<x:variable>
, and shared scenarios are as in Part 1 of this series, so read that if you haven't already.
Code is downloadable from xspectacles on GitHub, in the src/code-reuse-adaptable-part2
folder.