XQuery/XUnit Annotations

Motivation edit

You want an easy way to add unit tests to each function of a module without having to create separate test files for each function.

Method edit

We will use XQuery 3.0 annotations to add assertions to each of our functions. Note that the following examples are based on the eXist 2.0 release and are dependent on the XQuery 3.0 functions.

How it Works edit

Lets say you have a simple XQuery function that returns the string "Hello World!" like this:

declare function myfunct:hello() as xs:string {
    'Hello World!'
};

You want to add a single assertion to the function using an XQuery 3.0 annotation. The XQuery 3.0 Working Draft indicates that annotations always start with the "%" symbol and occur after the keyword "declare" and before the keyword "function" like this:

declare %test:assertEquals('Hello World!') function myfunct:hello() as xs:string {
    'Hello World!'
};

Note that the %test:assertEquals('Hello World!') has been added to the original function. The "assertion" is just a function that must return true for the test to pass. The output of the function is automatically compared with the input string and in this case the assertEquals function returns true if the strings match exactly.

Note that this syntax is different from other languages like Java that put assertions in comments before each method.

You now have enough information it the function to run a simple unit test.

How to Invoke the Tests edit

To invoke the tests you use the following test suite function:

   test:suite($function* as function) as node()*

This function will return an XUnit test results file.

If you want to test all the functions in a module you can use the util:list-functions($module-uri) function which returns a sequence of function objects for the module.

Sample Test Driver

xquery version "3.0";

(: the following line must be added to each of the modules that include unit tests :)
import module namespace test="http://exist-db.org/xquery/xqsuite" at "resource:org/exist/xquery/lib/xqsuite/xqsuite.xql";

(: import the module that contains your test-annotated functions :)
import module namespace mt="http://exist-db.org/xquery/test/my-module" at "mymodule.xqm";

(: the test:suite() function will run all the test-annotated functions in the module whose namespace URI you provide :)
test:suite(util:list-functions("http://exist-db.org/xquery/test/my-module"))

XQuery Annotations edit

Annotations are structures that are added to each function. They are not compiled by the XQuery compiler but can be used by other XQuery scripts to create a database of actual test files to be run. This places the tests directly in the context of each XQuery function. You can use the util:inspect-function() function to get a list of all the annotations in a function.

XUnit Test Result Format edit

XUnit is the family name given to a general class of testing frameworks that have become widely known amongst software developers. The name is a derivation of JUnit, the first of these to be widely known.

XUnit also includes a is an standardized XML test results output format that is used by many continious integration tools such as Jenkins, Hudson, CruiseControl, Team City and other tools.

The following is a sample XUnit result showing a passing test:

<testcase name="prefix:function-name Test #1" classname="my-module" time=".01"/

The test name is any string that describes the test. You can add the name of the function to help you know what function is being tested. You can also add a classname attribute to group the test results together in test results reports. The time element is the execution time of the unit test.

A failing test is indicated as follows:

<testcase name="prefix:function-name Test #2" classname="my-module" time=".01">
    <failure  message="prefix:function-name Test #2 has failed" type="failure-code-type-1"/>
</testcase>

See https://gist.github.com/959290 for a sample XML Schema of these files.

Example Annotation edit

The following is a module with a single function. This function takes no input parameters and always returns the string 'a'.

xquery version "3.0";

module namespace mymodule="http://exist-db.org/xquery/test/myfunctions";

import module namespace test="http://exist-db.org/xquery/xqsuite" at "resource:org/exist/xquery/lib/xqsuite/xqsuite.xql";

declare %test:assertEquals("123") function mymodule:a() as xs:string {
    '123'
};

We can then write a test driver that will test this function using the assertion:

xquery version "3.0";
 
(: the following line must be added to each of the modules that include unit tests :)
import module namespace test="http://exist-db.org/xquery/xqsuite" at "resource:org/exist/xquery/lib/xqsuite/xqsuite.xql";
 
(: import the module that contains your test-annotated functions :)
import module namespace myfunct="http://exist-db.org/xquery/test/myfunctions" at "mymodule.xqm";
 
(: the test:suite() function will run all the test-annotated functions in the module whose namespace URI you provide :)
test:suite(util:list-functions("http://exist-db.org/xquery/test/myfunctions"))

The following annotation example shows how you would add an assertion to test a simple function that adds two decimal numbers. Note that you must place the percent sign between the declare and function.

xquery version "3.0";

import module namespace test="http://exist-db.org/xquery/xqsuite" at "resource:org/exist/xquery/lib/xqsuite/xqsuite.xql";

declare %test:assertEquals(70) function test:a1() {
    20 + 50
};

This example would return a successful test.

declare %test:assertEquals(70) function test:a2() {
    21 + 50
};

This example would return a test failure record.

Assertions edit

An assertion is any statement that returns true if a test has passed and false if a test fails. Assertions form the basis of XQuery annotations.

Simple Assertions edit

Many XQuery tests can be run based on a single line of code without the need for pre-conditions or post-conditions to the test.

For example %test:assertEquals("Heinz Roland Uschi Verona")

Checks the result returned from the function for equality with the provided data. If the function returned a string sequence with more than one item, a space is added between the items (as above).

An assertion can take any sequence of literals, including strings and numbers, but not XML fragments. However, assertEquals does inspect the return type of the function. If it returned an XML fragment, it will be normalized (ignorable whitespace stripped). The annotation string is parsed into XML as well and the two values are compared using deep-equals(). Thus, if your function returns XML, you provide a string to %test:assertEquals and it is parsed into XML as well.

%test:assertEmpty - returns true if the result is an empty string.

%test:assertExists - returns true if the result exists.

%test:assertTrue - returns true if the result is true

%test:assertFalse - returns true if the result is false

%test:assertError("error code") - Excepts the function to fail with an error. If an error code is given (optional), it should be contained in the error message or the test will fail.

%test:assertXPath("count($result) = 8") - This is the most powerful assertion. It checks the result against an arbitrary XPath expression. The assertion is true if

  1. the XPath results to a boolean true or
  2. the XPath returns a non-empty sequence

The output of the called function is always contained in variable $result.

Passing Parameters to Assertions edit

The following example uses the test:args annotation to pass to input parameters to the unit test.

declare
   %test:args('"fenny snake"', "all")
   %test:assertXPath("count($result[@class='scene']) = 2")
   %test:assertXPath("$result/table/tr/td[@class='hi'] = 'fenny'")
function t:show-hits($queryStr as xs:string?, $mode as xs:string) {
   let $result := shakes:do-query($queryStr, $mode)
   return
       shakes:show-hits((), (), $result)
};

In this example the first parameter is the $queryStr="fenny snake" and the second parameter is the $mode="all". This example then has two separate assertions. The first must have at least two results with the class of scene and the section assertion show that the result must contain a table with a row and a table data element with a class of "hi".

In this way many assertions may be created with these two parameters.

Using Multiple Arguments edit

You can use the %test:args() function as many times as you want to create multiple tests for a single function. You just repeat the %test:args() function over and over with a separate set of assert statements after each %test:args(). Here is the pattern:

declare
   (: test 1 :)
   %test:args('a')
   %test:assert()
   %test:assert()

   (: test 2 :)
   %test:args('b')
   %test:assert()
   %test:assert()

   (: test 3 :)
   %test:args('b')
   %test:assert()
   %test:assert()

   function prefix:myfunction($in as xs:string) as xs:string {
   ...
   };

Helper Annotations edit

In addition to the basic assertions there are a few additional annotations that can be used. You can describe a test using the following:

%test:name("description")

Can be used to supply an additional description for the test. This will be used for the @name attribute in the xUnit <test> element.

There are also special functions which will be called before and after any other test in the module is evaluated. Use this to load documents, configure indexing and the like.

%test:setUp

%test:tearDown

References edit