The workshop is run as an exchange between a customer (played by Tommie Usdin) and a schema expert (played by Eric van der Vlist).
The customer, who needed a schema for her to-do list XML application, is puzzled by the "test first programming" technique imposed by the schema expert.
At the end of the day (or workshop), will she be converted to this well-known agile or extreme programming technique adapted to the development of XML schemas?
Step 1: Getting started
Hi Eric, can you help me to write a schema?
— Customer
Hi Tommie, yes, sure, what will the schema be about?
— Expert
I need a vocabulary for my to-do lists, with to-do item...
— Customer
OK, you've told me enough, let's get started.
— Expert (interrupting his customer)
Get started? but I haven't told you anything about it!
— Customer
Right, but it's never too soon to write tests when you do test first programming!
— Expert
Note
In test first programming (also called test driven development), developers create test case (usually unit test cases) before implementing a function. The test suite is run, code is written based on the result of these tests and the test suite and code are updated until all the tests pass.
Test suite (suite.xml):
<tf:suite xmlns:tf="http://xmlschemata.org/test-first/" xmlns:todo="http://balisage.net/todo#" title="Basic tests"> <tf:case title="Root element" expected="valid" id="root"> <todo:list/> </tf:case> </tf:suite>
Note
The vocabulary used to define these test cases has been inspired by the SUT (XML Schema Unit Test) project. It's a simple vocabulary (composed of only three different elements) allowing to pack several XML instances together with the outcome validation result. It uses conventions that you'll discover during the course of this workshop.
Note
The test suite is run using a simple Orbeon Forms application. The rendering relies on Orbeon Forms XForms' implementation while the test suite is run using an Orbeon Forms' XPL pipeline.
Step 2: Adding a schema
You see, you can already write to-do lists!
— Expert
Hold on, we don't have any schema!
— Customer
That's true, but you don't have to write a schema to write XML documents.
— Expert
I know, but you're here to write a schema! Furthermore, right now we accept anything. I don't want to have XML documents with anything as a root element!
— Customer
That's a good reason to write a schema, but before that we need to add a test in our suite first.
— Expert
Test suite (suite.xml):
<?xml version="1.0" encoding="UTF-8"?> <tf:suite xmlns:tf="http://xmlschemata.org/test-first/" xmlns:todo="http://balisage.net/todo#" title="Basic tests"> <tf:case title="TODO list toot element" expected="valid" id="root"> <todo:list/> </tf:case> <tf:case title="Other root element" expected="error" id="other-root"> <todo:title>A title</todo:title> </tf:case> </tf:suite>
Now that we've updated the test suite, we run it again.
— Expert
This result was expected, and we can now proceed to create a schema and attach it to the test suite.
— Expert
<?xml version="1.0" encoding="UTF-8"?> <xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema" elementFormDefault="qualified" targetNamespace="http://balisage.net/todo#" xmlns="http://balisage.net/todo#"> <xs:element name="list"/> </xs:schema>todo.xsd
<?xml version="1.0" encoding="UTF-8"?> <tf:suite xmlns:tf="http://xmlschemata.org/test-first/" xmlns:todo="http://balisage.net/todo#" title="Basic tests"> <tf:validation href="todo.xsd" type="xsd"/> <tf:case title="TODO list toot element" expected="valid" id="root"> <todo:list/> </tf:case> <tf:case title="Other root element" expected="error" id="other-root"> <todo:title>A title</todo:title> </tf:case> </tf:suite>suite.xml
It's time to test again what we've done.
— Expert
Step 3: Adding list title elements
I am happy to see some progress, at last, but I don't want to accept any content in the to-do list element. Can you add list title elements?
— Customer
Sure, back to the test suite...
— Expert
Test suite (suite.xml):
<?xml version="1.0" encoding="UTF-8"?> <tf:suite xmlns:tf="http://xmlschemata.org/test-first/" xmlns:todo="http://balisage.net/todo#" title="Basic tests"> <tf:validation href="todo.xsd" type="xsd"/> <tf:case title="TODO list root element" expected="valid" id="root"> <todo:list/> </tf:case> <tf:case title="TODO list with a title" expected="valid" id="list-title"> <todo:list> <todo:title/> </todo:list> </tf:case> <tf:case title="Other root element" expected="error" id="other-root"> <todo:title>A title</todo:title> </tf:case> </tf:suite>
Now that we've updated the test suite, we run it again.
— Expert
You see? We do already support list title elements!
— Expert
Sure, but I don't want to accept any content in my to-do list. And the title element should be mandatory. And it should not be empty but have at least one character!
— Customer
Back to the test suite, then...
— Expert
Test suite (suite.xml):
<?xml version="1.0" encoding="UTF-8"?> <tf:suite xmlns:tf="http://xmlschemata.org/test-first/" xmlns:todo="http://balisage.net/todo#" title="Basic tests"> <tf:validation href="todo.xsd" type="xsd"/> <todo:list> <tf:case title="Empty list element" expected="error" id="root-empty"/> <todo:title> <tf:case title="empty title" expected="error" id="empty-title"/> <tf:case title="non empty title" expected="valid" id="non-empty-title">A title</tf:case> </todo:title> <tf:case title="Un expected element" expected="error" id="unexpected"> <todo:foo/> </tf:case> </todo:list> <tf:case title="Other root element" expected="error" id="other-root"> <todo:title>A title</todo:title> </tf:case> </tf:suite>
Note
This is the first example with non-top-level tf:case
elements. To understand how this
works, we must look in more detail to the algorithm used by the framework to split
a test suite into
instances. The algorithm consists in two steps:
-
Loop over each
tf:case
element -
Suppression of the
tf:case
elements and of the top level elements which are not ancestors of the currenttf:case
element.
Now that we've updated the test suite, we run it again.
— Expert
Looks good, now we can update the schema.
— Expert
<?xml version="1.0" encoding="UTF-8"?> <xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema" elementFormDefault="qualified" targetNamespace="http://balisage.net/todo#" xmlns="http://balisage.net/todo#"> <xs:element name="list"> <xs:complexType> <xs:sequence> <xs:element name="title"> <xs:simpleType> <xs:restriction base="xs:token"> <xs:minLength value="1"/> </xs:restriction> </xs:simpleType> </xs:element> </xs:sequence> </xs:complexType> </xs:element> </xs:schema>todo.xsd
And run the test suite again.
— Expert
Step 4: Adding to-do item elements
Good. Now I want to add to-do items. And lists should have at least one of them, by the way.
— Customer
Sure, back to the test suite...
— Expert
Test suite (suite.xml):
<?xml version="1.0" encoding="UTF-8"?> <tf:suite xmlns:tf="http://xmlschemata.org/test-first/" xmlns:todo="http://balisage.net/todo#" title="Basic tests"> <tf:validation href="todo.xsd" type="xsd"/> <tf:case title="Empty list element" expected="error" id="root-empty"> <todo:list/> </tf:case> <todo:list> <!-- Testing title elements --> <todo:title> <tf:case title="empty title" expected="error" id="empty-title"/> <tf:case title="non empty title" expected="valid" id="non-empty-title">A title</tf:case> </todo:title> <todo:item> <todo:title>A title</todo:title> </todo:item> <tf:case title="Un expected element" expected="error" id="unexpected"> <todo:foo/> </tf:case> </todo:list> <todo:list> <!-- Testing todo items --> <todo:title>Testing todo items</todo:title> <tf:case title="No todo items" expected="error" id="no-items"/> <todo:item> <tf:case title="empty item" expected="error" id="empty-item"/> <tf:case title="item with a title" expected="valid" id="item-title"> <todo:title>A title</todo:title> </tf:case> </todo:item> </todo:list> <tf:case title="Other root element" expected="error" id="other-root"> <todo:title>A title</todo:title> </tf:case> </tf:suite>
Let's see what we get before any update to the schema.
— Expert
It's time to update the schema and fix what needs to be fixed.
— Expert
<?xml version="1.0" encoding="UTF-8"?> <xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema" elementFormDefault="qualified" targetNamespace="http://balisage.net/todo#" xmlns="http://balisage.net/todo#"> <xs:element name="list"> <xs:complexType> <xs:sequence> <xs:element name="title"> <xs:simpleType> <xs:restriction base="xs:token"> <xs:minLength value="1"/> </xs:restriction> </xs:simpleType> </xs:element> <xs:element maxOccurs="unbounded" name="item"> <xs:complexType> <xs:sequence> <xs:element name="title"> <xs:simpleType> <xs:restriction base="xs:token"> <xs:minLength value="1"/> </xs:restriction> </xs:simpleType> </xs:element> </xs:sequence> </xs:complexType> </xs:element> </xs:sequence> </xs:complexType> </xs:element> </xs:schema>todo.xsd
And now we can check if we get it right.
— Expert
Step 5: Modularizing the schema
Eric, you should be ashamed, it's a pure Russian doll schema, not modular at all! Why not define the title and list elements globally?
— Customer
Sure, we can do that! If we just change the structure of the schema, we don't need to update the test suite and can work directly on the schema.
— Expert
<?xml version="1.0" encoding="UTF-8"?> <xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema" elementFormDefault="qualified" targetNamespace="http://balisage.net/todo#" xmlns="http://balisage.net/todo#"> <xs:element name="list"> <xs:complexType> <xs:sequence> <xs:element ref="title"/> <xs:element maxOccurs="unbounded" ref="item"/> </xs:sequence> </xs:complexType> </xs:element> <xs:element name="title"> <xs:simpleType> <xs:restriction base="xs:token"> <xs:minLength value="1"/> </xs:restriction> </xs:simpleType> </xs:element> <xs:element name="item"> <xs:complexType> <xs:sequence> <xs:element ref="title"/> </xs:sequence> </xs:complexType> </xs:element> </xs:schema>todo.xsd
But of course, each time we update the schema we must check if we've not introduced any bug.
— Expert
Waoo, what's happening now?
— Customer
Now that our elements are global in the schema, we accept a valid title as a root element. Is that what you want?
— Expert
No way, a title is not a valid list!
— Customer
Then we have a number of options... We can go back to local elements, and we can also add a schematron schema to check this constraint.
— Expert
Schematron is fine, we'll probably find many other constraints to check in my to-do lists anyway...
— Customer
OK. We still don't have to update the test suite since we've not changed our requirement. Let's write this Schematron schema then.
— Expert
<?xml version="1.0" encoding="UTF-8"?> <schema xmlns="http://purl.oclc.org/dsdl/schematron" queryBinding="xslt2"> <ns uri="http://balisage.net/todo#" prefix="todo"/> <pattern> <rule context="/*"> <assert test="self::todo:list">The root element should be a todo:list</assert> </rule> </pattern> </schema>todo.sch
Saying that we don't have to update the test suite wasn't totally accurate because the schemas are referenced in ths test suite.
— Expert
Test suite (suite.xml):
<?xml version="1.0" encoding="UTF-8"?> <tf:suite xmlns:tf="http://xmlschemata.org/test-first/" xmlns:todo="http://balisage.net/todo#" title="Basic tests"> <tf:validation href="todo.sch" type="sch"/> <tf:validation href="todo.xsd" type="xsd"/> <tf:case title="Empty list element" expected="error" id="root-empty"> <todo:list/> </tf:case> <todo:list> <todo:title> <tf:case title="empty title" expected="error" id="empty-title"/> <tf:case title="non empty title" expected="valid" id="non-empty-title">A title</tf:case> </todo:title> <todo:item> <todo:title>A title</todo:title> </todo:item> <tf:case title="Un expected element" expected="error" id="unexpected"> <todo:foo/> </tf:case> </todo:list> <todo:list> <todo:title>Testing todo items</todo:title> <tf:case title="No todo items" expected="error" id="no-items"/> <todo:item> <tf:case title="empty item" expected="error" id="empty-item"/> <tf:case title="item with a title" expected="valid" id="item-title"> <todo:title>A title</todo:title> </tf:case> </todo:item> </todo:list> <tf:case title="Other root element" expected="error" id="other-root"> <todo:title>A title</todo:title> </tf:case> </tf:suite>
Time to see if we've fixed our issue!
— Expert
Great, we've made it, thanks!
— Customer
Want to try it?
The application used to run the test suite and display its result is available at http://svn.xmlschemata.org/repository/downloads/tefisc/.
If you just want to understand how the test suite is split into XML instances, you can have a look at http://svn.xmlschemata.org/repository/downloads/tefisc/orbeon-resources/apps/tefisc/ .
In this directory:
-
split-tests.xsl is the XSLT transformation that splits a test suite into top level element test cases. This transformation has no dependence on Orbeon Forms and can be manually run against a test suite.
-
run-test.xpl is the XPL pipeline that runs a test case.
-
list-suites.xpl is the XPL pipeline that gives the list available test cases.
-
view.xhtml is the XForms application that displays the results.
To install this application:
-
Copy the orbeon-resources/ directory under
/WEB-INF/resources/apps/
in your orbeon webapp directory -
Or, alternatively, copy the tefisc/ directory wherever you want, edit web.xml.sav to replace
<param-value>/home/vdv/projects/tefisc/orbeon-resources</param-value>
by the location of this directory on your filesystem, replace/WEB-INF/web.xml
by this file and restart your application server.