mustrd

Spec-By-Example for RDF & SPARQL

Test your SPARQL queries and transformations with precision, using Given-When-Then principles.

# One spec. Given data. When query. Then result.
:my_test
    must:given [ a must:FileDataset ;
                 must:file "test/data/given.ttl" ] ;
    must:when  [ a must:TextSparqlSource ;
                 must:queryText "CONSTRUCT { ?s ?p ?o }
                                WHERE { ?s a ex:Person ; ?p ?o }" ;
                 must:queryType must:ConstructSparql ] ;
    must:then  [ a must:FileDataset ;
                 must:file "test/data/expected.ttl" ] .

Define your data, run your query, check the result. All in Turtle.

What mustrd is

A pytest plugin

Add --mustrd to your pytest command. Specs become tests. Results show in your runner, CI, and VS Code.

Triplestore agnostic

Run specs against embedded RDFLib, GraphDB, or Anzo. Same specs, different engines. Swap with configuration, not code.

Spec-By-Example

Inspired by Cucumber. Define concrete examples of what your query should do, not abstract assertions about what it might.

Not SHACL

SHACL validates data structure. mustrd validates data transformations. Your query produces the right triples? mustrd checks.

Given-When-Then

Every spec follows the same shape. Load data, run a query, check the output.

Given

The input dataset. A Turtle file, inline statements, an Anzo graphmart, or inherited state.

must:given [
  a must:FileDataset ;
  must:file "data/input.ttl"
] ;

When

The SPARQL action. Inline text, a file reference, or a query builder source. SELECT, CONSTRUCT, or UPDATE.

must:when [
  a must:FileSparqlSource ;
  must:file "sparql/enrich.rq" ;
  must:queryType must:UpdateSparql
] ;

Then

The expected result. An RDF graph, a table of bindings, CSV/Excel, or an empty result.

must:then [
  a must:FileDataset ;
  must:file "data/expected.ttl"
] .

Query types

mustrd validates across SPARQL's full query surface.

TypeWhat it doesResult compared as
SELECTReturn variable bindingsTable (pandas DataFrame)
CONSTRUCTBuild an RDF graphGraph isomorphism
UPDATEINSERT/DELETE triplesModified graph comparison
ASKBoolean existence checkTrue/false
DESCRIBEDescribe a resourceGraph isomorphism

A failure that tells you

The first line of a failing SELECT names the binding that differs and what it differs by. The commonest failure of all — right shape, wrong value — used to report two identical shapes and leave the rest to the table.

// same text, different type
Expected 1 row(s) and 1 column(s), got 1 row(s) and 1 column(s)
   differs in: month (datatype: expected xsd:string, actual xsd:gYearMonth)

// wrong term, named with your own prefixes
Expected 2 row(s) and 3 column(s), got 2 row(s) and 3 column(s)
   differs in: o (expected ex:object, actual ex:obj)

// a long value is elided around the difference, not from the start
   differs in: s (expected <…o/the/thing/alpha>, actual <…o/the/thing/beta>)

// same thing, different scheme or host — named, not printed
   differs in: s (scheme: expected http, actual https)
   differs in: s (host: expected company.dev, actual company.com)

Those last two are the hardest to spot: an ontology served from a dev host and a prod one, or over http on one side and https on the other, agrees everywhere the eye lands. Printing both IRIs makes it a spot-the-difference puzzle; eliding them makes it worse. Only claimed when the origin really is the whole difference — if anything after the host differs too you get both IRIs, because naming the host would send you after the wrong thing.

Then the table, narrowed to the columns that actually differ. On a wide result the differing column can sit off the right-hand edge, which is why the line above it carries the answer.

|    | ('month_datatype', 'expected')          | ('month_datatype', 'actual')                |
|---:|:----------------------------------------|:--------------------------------------------|
|  0 | http://www.w3.org/2001/XMLSchema#string | http://www.w3.org/2001/XMLSchema#gYearMonth |

Triplestore support

Write specs once. Run them against any supported engine.

EngineProtocolNotes
RDFLibEmbedded (in-memory)Default. No server needed. Zero config.
GraphDBHTTP SPARQL endpointRepository-based. Optional named graphs.
AnzoHTTP REST APIGraphmart layers, query builders, AnzoGraph.

Flexible datasets

Givens and thens support multiple data formats.

TypeDescription
FileDatasetRDF file — Turtle, NTriples, N3, RDF/XML, TriX
StatementsDatasetInline reified RDF statements in the spec
TableDatasetInline tabular bindings for SELECT results
OrderedTableDatasetTabular data with row ordering (ORDER BY)
FolderDatasetFile path passed as a runtime argument
EmptyTable / EmptyGraphExpect no results
InheritedDatasetKeep existing graph state (chain specs)

VS Code integration

Specs appear as tests in the Test Explorer. Click to run, see diffs on failure.

// settings.json
{
  "python.testing.pytestArgs": [
    "--mustrd",
    "--md=junit/github_job_summary.md",
    "--config=test/test_config_local.ttl"
  ]
}

Reports that are data

A run's output is RDF first: DQV quality measurements over your ontology, a per-term breakdown, competency-question assertions, and a result per test — all with stable IRIs, so runs merge and diff in a triplestore.

The HTML report is rendered from that graph, in the browser, by a single self-contained file. No build step, no CDN, no server: attach it to a CI run or open it from disk. Drop another run's .ttl onto the page to load it.

A run report, driven entirely by the RDF it carries. Open the real thing — it is one file, so View Source is the whole story.

The vocabulary it is all rendered from: the coverage ontology.

Two ways to run

The same engine, the same configuration file, the same reports. Pick whichever fits where you are.

As a pytest plugin

Specs become tests in your existing suite, so they run in CI and appear in the VS Code Test Explorer next to everything else.

pip install mustrd

pytest --mustrd \
  --config=mustrd-config.ttl \
  --viewer=report.html

As a command

No pytest. Useful in a pipeline step, a pre-commit hook, or anywhere a test runner would just be in the way. Exits non-zero if a spec fails.

pip install mustrd

mustrd run    --config mustrd-config.ttl
mustrd report --config mustrd-config.ttl \
  --viewer report.html

mustrd run executes the specs and prints a result table. mustrd report does the same and writes the artefacts you ask for — --viewer for the HTML page above, --md for Markdown, --term-coverage-rdf / --results-rdf for the graph itself. Both take the same flags as the plugin.

Windows, including locked down

Windows is a tested platform, not an afterthought — CI runs the whole suite on it against Python 3.11, 3.12 and 3.13. The reports use box-drawing characters and emoji, and a Windows console still defaults to cp1252 where printing those throws, so the CLI reconfigures its own streams to UTF-8 on startup. That failure mode is handled rather than left to you.

Ordinary Windows

Nothing special to do. Paths inside a config resolve relative to the config file rather than the working directory, so drive letters and backslashes are fine and you can run from anywhere.

pytest --mustrd --config=test\config.ttl

mustrd report --config test\config.ttl \
  --viewer report.html

Locked-down Windows

Enterprise builds commonly block the bare .exe shims pip installs into Scripts\. The package installs fine, but the mustrd command won't start. Run the module through the venv's interpreter instead.

python -m venv .venv
.venv/Scripts/python -m pip install mustrd

.venv/Scripts/python -m mustrd report \
  --config config.ttl

python -m mustrd is exactly equivalent to the mustrd command — same entry point, same flags. It exists for this. More in the README.

Status

Get started

pip install mustrd
pytest --mustrd --config=test/mustrd_configuration.ttl

GitHub · mustrd@semanticpartners.com