Allow custom assertions in XQSuite
Description:
Add new functions test:fail#3 and test:fail#4 to XQSuite. Calling one of these inside a XQSuite test function will stop execution by throwing a special error test:failure. This error is caught and handled as a failure allowing to create arbitrary custom assertions and the ability to provide specific error messages.
(edit) After @adamretter's feedback the order of arguments was changed.
test:fail#3: fail with custom message, expected and actual valuetest:fail#4: fail with custom message, expected and actual value and a custom test type
Works in both XQuery and jUnit context. In jUnit you will only be presented with the serialized expected and actual values.
(edit) In order to provide this facility also to packages that target older versions of eXist-db, which might not have the test:fail function available both
- the QName of the error that is treated as a failure
$test:FAILURE - the default custom assertion type
$test:CUSTOM_ASSERTION_FAILURE_TYPE
are exposed. This allows to fall back to throwing the error directly in custom assertion code. In case of a failed assertion the test case will still fail. Though not as a failure but an error. Also expected and actual value might not be displayed nicely. But the provided custom message will still add value.
Example
test.xqm
xquery version "3.1";
module namespace t="http://exist-db.org/xquery/test/xqsuite";
import module namespace assert="http://line-o.de/xq/assert" at "./assert.xqm";
declare namespace test="http://exist-db.org/xquery/xqsuite";
declare variable $t:var := map {"a": 1, "b": 2};
declare %test:assertTrue function t:test-missing-key() as xs:boolean? {
assert:map($t:var, map {"a": 1, "c": 4})
};
declare %test:assertTrue function t:test-wrong-value() as xs:boolean? {
assert:map($t:var, map {"a": 1, "b": 3})
};
declare %test:assertTrue function t:test-wrong-type() as xs:boolean? {
assert:map($t:var, [1,2])
};
declare %test:assertTrue function t:test-passing() as xs:boolean? {
assert:map($t:var, $t:var)
};
assert.xqm
xquery version "3.1";
module namespace assert="http://line-o.de/xq/assert";
import module namespace test="http://exist-db.org/xquery/xqsuite"
at "resource:org/exist/xquery/lib/xqsuite/xqsuite.xql";
declare function assert:map ($e as map(*), $a as item()*) as xs:boolean? {
if (exists($a) and count($a) eq 1 and $a instance of map(*))
then (
for-each(map:keys($e), function ($key as xs:anyAtomicType) {
if (not(map:contains($a, $key)))
then test:fail("Key " || $key || " is missing", $e, $a, "map-assertion")
else if ($e($key) ne $a($key))
then test:fail("Value mismatch for key '" || $key || "'", $e, $a, "map-assertion")
else ()
})
,
true()
)
else test:fail("Type mismatch", $e, $a, "map-assertion")
};
Running the above will yield
<testsuites>
<testsuite package="http://exist-db.org/xquery/test/xqsuite" timestamp="2022-04-12T14:24:15.72+02:00"
tests="4" failures="3" errors="0" pending="0" time="PT0.008S">
<testcase name="test-missing-key" class="t:test-missing-key">
<failure message="Key 'b' is missing" type="map-assertion"/>
<output>map{"a":1,"c":4}</output>
</testcase>
<testcase name="test-passing" class="t:test-passing"/>
<testcase name="test-wrong-type" class="t:test-wrong-type">
<failure message="Type mismatch" type="map-assertion"/>
<output>[1,2]</output>
</testcase>
<testcase name="test-wrong-value" class="t:test-wrong-value">
<failure message="Value mismatch for key 'b'" type="map-assertion"/>
<output>map{"a":1,"b":3}</output>
</testcase>
</testsuite>
</testsuites>
Reference:
This was added due to the limitations of XQuery function annotations and will allow to compare complex values, loading fixtures and assertions from modules and provide human readable feedback via custom error messages.
Type of tests:
XQSuite tests in custom-assertion.xqm
I have a concern that your test:fail function is mixing concerns, it appears to do two things:
- check that expected and actual match
- log a failure message (conditionally if (1) is false)
The problem with mixing concerns is that we are then not creating composable building blocks.
I think it needs to be simplified to do one thing, i.e. fail with an optional code and message. For a good example, see the JUnit fail function, it does a single thing. fail is certainly not meant to be an assertion!
test:fail does not assert anything @adamretter
As you can clearly see in the provided example.
test:faildoes not assert anything @adamretter As you can clearly see in the provided example.
The example isn't clear at all as it uses an assert library which isn't part of this PR. Instead I was reading the commits themselves...
Ah I see... It doesn't assert, however including them as args to test:fail, still mixes concerns and by extension limits the usefulness of test:fail.
When I have a moment later I will try and propose some alternative function signatures, which still allow you to achieve the same goal but are more composable
In order to get the desired test output on failure it is crucial to pass both expected and actual to test:fail. Function annotations are not sufficient to handle these.
Thanks Juri. I understand and I am generally in favour of this, I think it just needs a little tweaking and discussion
@adamretter I am all for discussions. I would appreciate it, though, that you state what you think needs to change and why.
From reading through your comments, I understand that passing expected and actual to test:fail is what you dislike. I already pointed out why this needs to happen.
So the question remains how do you envision to pass those two values to the error that is thrown or more importantly the error handling code?
I think the full function signature for a test:fail function should look like:
test:fail($code as xs:QName?, $description as xs:string, $failure-object as item()*) as none
We can then also have:
test:fail()test:fail($code as xs:QName?)test:fail($code as xs:QName?, $description as xs:string)
I have chosen the above function signatures to mirror that of fn:error, as failure is a parallel concept to error, and @line-o's proposed implementation uses fn:error itself.
By not mixing concerns, i.e. not forcing the user to include the expected and actual values of an assertion, the test:fail function now does one thing and one thing only.
There are of course also many use case, where you may want to fail when there is no assertion to be made; JUnit's fail function demonstrates that! The user is still free to provide any additional values they need (e.g. expected and actual values) in the $failure-object sequence.
A new implementation of such a function might look like:
declare function test:fail($code as xs:QName?, $description as xs:string, $failure-object as item()*) as empty-sequence() {
fn:error(xs:QName("test:fail"), $description, ($code, $failure-object))
};
@adamretter I incorporated your feedback.
@line-o You still appear to have the old versions of test:fail which mix concerns (i.e. they still expect actual and expected values). Can you remove those please as they are now unnecessary.
@adamretter I cannot follow. Both expected and actual are necessary in every case.
@line-o The premise behind all of my concerns with this PR is that having actual and expected values as explicit arguments mixes concerns in a negative way!
Instead, with my proposal you have $failure-objects, and you can simply do this instead (if you wish), which is equivalent but doesn't mix concerns:
test:fail($code, $description, ($expected, $actual))
Your proposal, @adamretter, will not work. Here are the main issues:
- both the actual and expected values can be sequences and will be mashed together into a single sequence making it impossible for later code to distinguish them
- the later code needs to handle missing values (throwing an error in the error handler?)
- assertion developers need to keep in mind what and how is to be put there and no tooling will help them remember that
both the actual and expected values can be sequences and will be mashed together into a single sequence making it impossible for later code to distinguish them
That is solved simply by using a Map, e.g.:
test:fail($code, $description, map {"expected" : $expected, "actual": $actual})
the later code needs to handle missing values (throwing an error in the error handler?)
I am not sure what you mean by "the later code" - can you show me where to find this?
assertion developers need to keep in mind what and how is to be put there and no tooling will help them remember that
Again I am afraid, I am not sure what you mean by this. Can you show me where to find this? Also, keep in mind that not "all assertions" involve comparing actual and expected values, or even comparing values. The Hamcrest library can show you some good examples of other assertions that don't involve values.
@adamretter I am sure you noticed that test:fail#0, and test:fail#1 were added to this PR along with their tests. So, if an assertion does not need to expected and actual vaiue it is as easy as:
declare %test:assertTrue function t:random-is-odd() as xs:boolean? {
if (( random-number-generator()?number * 1e16 ) mod 2)
then test:fail()
else true()
};
Regarding the usage of a map. Yes, this would solve the problem of keeping sequences apart. On the flip side there is no tooling in place to help you keep in mind what keys need to propagated. Whereas having them as explicit arguments means that in code editors like eXide, VSCode and intelliJ you will be presented with useful hints what needs to be added. And if one does not use those the XQuery compiler of existdb will throw a static error with the line number and column number where the argument is missing.
The "later code" is here https://github.com/eXist-db/exist/blob/8464ae4ea69455c4ff0d5c61a591373593587ef5/exist-core/src/main/resources/org/exist/xquery/lib/xqsuite/xqsuite.xql#L425-L446
While one can leave the values empty, it will result in following output in jUnit

#confused
I believe two key purposes of a testing framework got confused here:
- assertions
- reporting
While the former has to have the expected and actual value the latter needs to know these only if the reporter wants to display these values. A dot-reporter for example only needs to know if the test passed or failed.
But all the reporters of XQSuite I am aware of - XML, HTML, jUnit - do display at least one of them.
That is why I chose to require them in the design of test:fail instead of making them optional. It is also impossible for the test function to know which reporter is currently used.
I have a hard time thinking of a reason why this is a bad decision and have not read a good one in the comments, yet.
The only point I can think of is that it would be one parameter less, three instead of four.
Developer experience and help from tooling outweighs this argument by a ton in my opinion.
I think there are some things here that still need to be discussed and developed further, I will try and find some time later in the week to take a look
As I read the discussion, the feature in itself is not in question but rather points related to usage? I agree that this needs to be well-discussed as we should not release a feature people will rely on with the “threat” of major changes in the near future – especially not as this would require people to rewrite their tests.
Can we compile a list of concerns and possible pitfalls that should be discussed? That may be a point for an upcoming call’s agenda.
@adamretter can you elaborate on why you are against merging this PR? It's been several months.
@dariok merging into the develop branch is not the same as releasing a feature. The develop branch allows developers and users to test features and provide feedback together with everything else that's happening in exist. The risk of changes to features currently on the develop branch is well documented and communicated.
If further changes are necessary before a release, then that's a separate question. The question at hand is, if the PR is ready to be merged into develop. As it is not a breaking change, I d say yes. Further development of this or other features before or after the next release should happen in a new pr.
+1 for me pulling in; maybe not perfect but good enough
@adamretter can you elaborate on why you are against merging this PR? It's been several months.
Sure, I have a long train journey today, and so I have found the time to re-evaluate and reframe the issues I raised above. As it seems some people did not understand the concerns I previously raised, I have also added examples to try and make this clearer. Hope it helps :-)
Previously XQSuite only made assertions via annotations. A test could only either Pass or Fail based on the annotations, or raise an Error.
XQSuite was originally designed so that the test assertions could be placed upon the actual code in question, evidence and examples of that can be seen here: https://exist-db.org/exist/apps/doc/xqsuite
The XQsuite test framework allows tests to be added to functions using function annotations. Since annotations are ignored during normal execution, this does not interfere.
In practice it seems, especially within the eXist-db database code base, XQSuite is not used in this manner as originally intended. Instead tests are written as standalone Test Suite modules which stand beside the code under test, this is also supported by XQSuite as stated:
More complex integration tests can be combined into separate modules.
The nature of XQSuite is somewhat vague, at no point does it descibe itself as suitable for Unit Tests, instead it mentions that it can be used for Integration Tests. However, arguably depending on the nature of the function under test, it can just as readily be used for writing Unit Tests.
This PR introduces Unit Test like functions for making assertions, thus turning XQSuite into quite a different tool. Previously, all assertions could only be made via annotations in a purely functional manner.
It should be noted that such additional functions are incompatible with XQSuite's original goal of testing existing code inline, i.e. without writing a separate Test Suite Module.
In principle I think that making XQSuite a more flexible framework is a positive gain. Whilst such additional assertion functions can only be used from Test Suite modules, as a great deal of tests are written in this manner, these functions can offer new and additional capabilities. The addition of such functions to XQSuite should remain an optional choice for the user, allowing the annotation approach to be used by default.
I would assert that adding such functions actually changes XQSuite into a framework that now has two personas:
- The existing functional persona which is restricted to purely functional programming.
- A new procedural presona which is more familiar to programmers coming from other languages. This persona very much adopts concepts from Unit Test frameworks such as: JUnit, NUnit, and xUnit.js.
In practice it may be possible for the user to use aspects of both personas when writing tests, however it should be clearly documented that each of these peronas fit different programming/testing styles, and how they interact with each other when combined.
To facilitate this I would strongly suggest that all such new assertion functions are separated into their own namespace; this can complement the existing test namespace.
Whilst I am happy in principle with adding such functions. I think the current implementation in this PR has issues, and it is these issues which I have tried to highlight previously.
Issues with this PR
In my opinion the signatures for the test:fail functions have several issues:
-
These functions should be moved into their own namespace to indicate the difference in persona, i.e.
test:fail#*->assert:fail#*. -
test:fail#0is not helpful for the user. When triggered it provides no contextual information to the user about what the failure is or why it occured. As the user has to write the code that includes this function call, they can certainly provide at least a description/label of/for the failure. I think this function enables a bad style of programming, and should be excluded to preclude users from doing things that will harm them. -
test:fail#1should not introduce a new concept called$type. This$typeseems to be used in the examples to indicate a string name of the user defined function that the user has called. I would argue that this is just another aspect of the description of the failure. It should not be a first class citizen, instead this can be handled in a cleaner way; see my suggestion below for anassert:assertfunction. -
test:fail#2should remove the$typeconcept. This function should simply becomeassert:fail($message), or better yetassert:fail($error-code as xs:QName, $message as xs:string?). -
test:fail#3andtest:fail#4conflates the purpose of the function with the fomatting of the falure description. These functions are unnecessary and make assumptions about how the user wishes to have their failured messages formated. They take a step too far, and are not required overtest:fail#2. It is better to provide building blocks that can be composed.
As I understand it the purpose of this PR is two fold:
- To introduce Unit Test like functions so that users may write a more procedural style of test.
- To allow users to write custom assertions.
I like the idea, but I would propose an alternative set of function signatures that preserve a separation of concerns between such custom assertions, and the handling and formatting of failures and their descriptions.
By a process of deduction, we can find that we only need implement a single function to support any Unit Test style procedural assertion. This function can then be utilised to both:
- Allow users to write their own custom assertions.
- Provide a utility library of pre-defined common assertions that may be useful to the user.
The signature for such a function could be:
assert:assert(function() as xs:anyAtomicType+) as empty-sequence()
Proposal
I propose the following module of assertions:
xquery version "3.1";
(:~
: A module which provides xUnit like assertions.
:
: These assertions may be used with XQSuite.
:
: These assertions may also be used in other contexts by simply
: catching the error $assert:assert-failure, i.e.: "assert:assert-failure".
:
: @author <a href="mailto:[email protected]">Adam Retter</a>
:)
module namespace assert = "http://exist-db.org/xquery/xqassert";
declare namespace test = "http://exist-db.org/xquery/test";
declare variable $assert:assert-failure := xs:QName("assert:assert-failure");
(:~
: This function is the root function for all assert functions.
:
: @param predicate a predicate which determines if the assertion passes or fails.
: Return type of $predicate is (xs:boolean, xs:QName?, xs:string?) i.e. ($result, $error-code, $message).
: Iff xs:boolean is fn:false() then xs:QName should be present, and xs:string is optional,
: Iff xs:boolean is fn:true() then xs:QName, and xs:string can be omitted.
:
:)
declare function assert:assert($predicate as function() as xs:anyAtomicType+) as empty-sequence() {
let $result := $predicate()
return
if ($result[1] eq fn:false())
then
if (fn:count($result) lt 2)
then
fn:error(xs:QName("assert:invalid-predicate-fn"), "The predicate passed to assert:assert#1 only returned 1 value, but at least 2 values are required")
else
let $description as xs:string := ($result[3], $result[2])[1]
return
(: NOTE: When the assert functions are used from within XQSuite, XQSuite will catch this $assert:assert-failure (i.e.: "assert:assert-failure") error and process it as a failure instead of an error :)
fn:error($assert:assert-failure, $description, ($result[2], fn:function-name($predicate)))
else ()
};
(: A library of assert Utility functions follows below for the convenience of users :)
(: TODO(AR) add XQDoc to below functions if we decide to progress this module :)
declare function assert:fail($error-code as xs:QName, $message as xs:string?) as empty-sequence() {
assert:assert(function() {
fn:false(),
$error-code,
$message
})
};
declare function assert:fail($error-code as xs:QName) as empty-sequence() {
assert:fail($error-code, ())
};
declare %private function assert:equal($error-code as xs:QName, $message as xs:string?, $message-formatter as function(xs:string?, item()+, item()+) as xs:string, $expected as item()*, $actual as item()*, $predicate as function() as xs:boolean) as empty-sequence() {
assert:assert(function() {
let $result := $predicate()
return
(
$result,
if ($result)
then
()
else
(
$error-code,
$message-formatter($message, $expected, $actual)
)
)
})
};
declare function assert:equal($error-code as xs:QName, $message as xs:string?, $message-formatter as function(xs:string?, item()+, item()+) as xs:string, $expected as item()*, $actual as item()*) as empty-sequence() {
assert:equal($error-code, $message, $message-formatter, $expected, $actual, function() { $expected = $actual })
};
declare variable $assert:default-equality-message-formatter := function($message as xs:string?, $expected as item()*, $actual as item()*) as xs:string {
fn:string-join(
(
$message,
"Expected: " || fn:serialize($expected, map { "method": "adaptive" }) || ".",
"Actual: " || fn:serialize($actual, map { "method": "adaptive" }) || "."
),
" "
)
};
declare function assert:equal($error-code as xs:QName, $message as xs:string?, $expected as item()*, $actual as item()*) as empty-sequence() {
assert:equal($error-code, $message, $assert:default-equality-message-formatter, $expected, $actual)
};
declare function assert:equal($error-code as xs:QName, $expected as item()*, $actual as item()*) as empty-sequence() {
assert:equal($error-code, (), $expected, $actual)
};
declare function assert:deep-equal($error-code as xs:QName, $message as xs:string?, $message-formatter as function(xs:string?, item()+, item()+) as xs:string, $expected as item()*, $actual as item()*) as empty-sequence() {
assert:equal($error-code, $message, $message-formatter, $expected, $actual, function() { fn:deep-equal($expected, $actual) })
};
declare function assert:deep-equal($error-code as xs:QName, $message as xs:string?, $expected as item()*, $actual as item()*) as empty-sequence() {
assert:deep-equal($error-code, $message, $assert:default-equality-message-formatter, $expected, $actual)
};
declare function assert:deep-equal($error-code as xs:QName, $expected as item()*, $actual as item()*) as empty-sequence() {
assert:deep-equal($error-code, (), $expected, $actual)
};
Examples of using such assertions are:
import module namespace assert = "http://exist-db.org/xquery/xqassert" at "xmldb:exist:///db/xqassert.xqm";
(:~ This is an example of a custom assertion that fails :)
(:
assert:assert(
function() { fn:false(), xs:QName("local:CUSTOM-LITTLE-FAIL-1"), "This is an example of a custom assertion that fails" }
)
:)
(:~ This is an example of a custom assertion that passes :)
(:
assert:assert(
function() { fn:true() }
)
:)
(:~ This is an example of calling the utility function assert:fail#1 :)
(:
assert:fail(xs:QName("local:CUSTOM-LITTLE-FAIL-2"))
:)
(:~ This is an example of calling the utility function assert:fail#2 :)
(:
assert:fail(xs:QName("local:CUSTOM-LITTLE-FAIL-3"), "This is an example of calling the utility function assert:fail#2")
:)
(:~ This is an example of calling the utility function assert:equal#3 :)
(:
assert:equal(xs:QName("local:CUSTOM-LITTLE-FAIL-4"), "a", "b")
:)
(:~ This is an example of calling the utility function assert:equal#4 :)
(:
assert:equal(xs:QName("local:CUSTOM-LITTLE-FAIL-5"), "friend, your things are not equal", "a", "b")
:)
(:~ This is an example of calling the utility function assert:equal#5 which allows you to format your own failure message :)
(:
let $my-formatter := function($message, $expected, $actual) { "I say whatever I want!" }
return
assert:equal(xs:QName("local:CUSTOM-LITTLE-FAIL-6"), "friend, your things are not equal", $my-formatter, "a", "b")
:)
(:~ This is an example of calling the utility function assert:deep-equal#3 :)
(:
assert:deep-equal(xs:QName("local:CUSTOM-LITTLE-FAIL-7"), "a", "b")
:)
(:~ This is an example of calling the utility function assert:deep-equal#4 :)
(:
assert:deep-equal(xs:QName("local:CUSTOM-LITTLE-FAIL-8"), "friend, your things are not deep-equal", "a", "b")
:)
(:~ This is an example of calling the utility function assert:deep-equal#5 which allows you to format your own failure message :)
let $my-formatter := function($message, $expected, $actual) { "I deeply think, before I say what I mean!" }
return
assert:deep-equal(xs:QName("local:CUSTOM-LITTLE-FAIL-9"), "friend, your things are not deep-equal", $my-formatter, "a", "b")
Furthermore, we can refactor @line-o approach so that it fits this more composable approach:
declare function local:assert-map($expected as map(*), $actual as item()*) as empty-sequence() {
if (fn:count($actual) eq 1 and $actual instance of map(*))
then
for $expected-key in map:keys($expected)
return
if (fn:not(map:contains($actual, $expected-key)))
then
assert:fail(xs:QName("local:map-assertion"), $assert:default-equality-message-formatter("Key " || $expected-key || " is missing", $expected, $actual))
else if ($expected($expected-key) ne $actual($expected-key))
then
assert:fail(xs:QName("map-assertion"), $assert:default-equality-message-formatter("Value mismatch for key '" || $expected-key || "'", $expected, $actual))
else ()
else
assert:fail(xs:QName("local:map-assertion"), $assert:default-equality-message-formatter("Type mismatch", $expected, $actual))
};
declare variable $local:expected := map {"a": 1, "b": 2};
(:~ This is an example of calling the custom assertion local:assert-map#2 for a map with a missing key :)
local:assert-map($local:expected, map {"a": 1, "c": 4})
(:~ This is an example of calling the custom assertion local:assert-map#2 for a map with a wrong value :)
(:
local:assert-map($local:expected, map {"a": 1, "b": 3})
:)
(:~ This is an example of calling the custom assertion local:assert-map#2 for a map with a wrong type :)
(:
local:assert-map($local:expected, [1,2])
:)
(:~ This is an example of calling the custom assertion local:assert-map#2 for a map that passes :)
(:
local:assert-map($local:expected, $local:expected)
:)
We can even go a step further and improve on the local:assert-map function to (a) introduce a bounds-check optimisation, and (b) reduce the amount of boiler-plate needed:
declare function local:improved-assert-map($expected as map(*), $actual as item()*) as empty-sequence() {
let $actual-count := fn:count($actual)
return
if (fn:count($actual) eq 1 and $actual instance of map(*))
then
let $expected-count := fn:count($expected)
return
if ($expected-count eq $actual-count)
then
map:for-each($expected, function($expected-key, $expected-value) {
(: First, compare the keys :)
if (fn:not(map:contains($actual, $expected-key)))
then
assert:fail(xs:QName("local:map-assertion"), $assert:default-equality-message-formatter("Key " || $expected-key || " is missing", $expected, $actual))
else ()
,
(: Second, compare the values :)
assert:equal(xs:QName("local:map-assertion"), "Value mismatch for key", $expected-value, $actual($expected-key))
})
else
assert:fail(xs:QName("local:map-assertion"), $assert:default-equality-message-formatter("Maps are of different sizes", $expected-count, $actual-count))
else
assert:fail(xs:QName("local:map-assertion"), $assert:default-equality-message-formatter("Type mismatch", $expected, $actual))
};
Oustanding questions:
- It is not clear how one can best use such assertions functions without any assert annotation at present, i.e. there is no generic
%test:testassertion. Regardless, they can be used succesfully with a simple%test:assertEmptyannotation which acts as well as a generic%test:testassertion. We should perhaps add%test:testas a synonym for%test:assertEmpty.
Interesting comments. I would like to point out that the introduction of types and the additional test:fail signatures where requested by you @adamretter . Now to my surprise you are advocating that these are the changes you dislike about this PR. What made you change your mind?
see https://github.com/eXist-db/exist/pull/4332#issuecomment-1098942507
I think the full function signature for a
test:failfunction should look like:test:fail($code as xs:QName?, $description as xs:string, $failure-object as item()*) as noneWe can then also have:
test:fail()test:fail($code as xs:QName?)test:fail($code as xs:QName?, $description as xs:string)I have chosen the above function signatures to mirror that of
fn:error, as failure is a parallel concept to error, and @line-o's proposed implementation usesfn:erroritself.By not mixing concerns, i.e. not forcing the user to include the
expectedandactualvalues of an assertion, thetest:failfunction now does one thing and one thing only. There are of course also many use case, where you may want to fail when there is no assertion to be made; JUnit's fail function demonstrates that! The user is still free to provide any additional values they need (e.g. expected and actual values) in the$failure-objectsequence.A new implementation of such a function might look like:
declare function test:fail($code as xs:QName?, $description as xs:string, $failure-object as item()*) as empty-sequence() { fn:error(xs:QName("test:fail"), $description, ($code, $failure-object)) };
Also important to point out: test:fail does raise an error of a specific type.
Wich in turn means it is fully compatible with the current design of XQSuite, with the exception of annotating function otherwise used in an application when this special error is not caught and handled inside the application code itself.
But as @adamretter already points out this feature is de facto unused. Existing code will work as before even if it is used in that way.
I a have a hard time understanding where the introduction of custom assertions changes the coding paradigm to procedural.







