Testing¶
Two levels, on purpose¶
| Level | Where | Written as | Answers |
|---|---|---|---|
| Executable specification | :spec |
Gherkin features run by Cucumber against the built JARs | What does the tool promise? — what an index means, which IRIs a run mints, how a bad index fails, what exit code comes out |
| Unit test | each module's src/test |
JUnit 5, parameterised where the rule is a table | Does this rule hold? — slug shapes, path resolution, emitter output patterns |
The split is deliberate. A feature file is worth its indirection when the behaviour is
user-observable and the wording is the thing people argue about — identity, validation, exit
codes. For a regex or a lookup, @ParameterizedTest says the same thing in a third of the space,
so Gherkin there would only add glue to maintain.
Run tests¶
# Everything
./gradlew test
# Just the executable specification (builds the JARs it runs)
./gradlew :spec:test
# One feature, by tag
./gradlew :spec:test -Dcucumber.filter.tags='@grouping'
./gradlew :spec:test -Dcucumber.filter.tags='@validation and not @bpmn'
# Single module
./gradlew :converter-plantuml:test
./gradlew :core:test
Executable specification (:spec)¶
Features live in spec/src/test/resources/features, step definitions in
spec/src/test/kotlin/.../steps. They are the executable half of
Diagram Index Files — if a scenario and the docs disagree,
the scenario is the one that has been run.
A sample of what is covered — the directory is the full list:
| Feature | Covers |
|---|---|
models-hold-views.feature |
model: / models: grouping, shared elements across views, arch:Model / arch:View typing, metadata levels, SVG naming, the legacy flat schema |
index-validation.feature |
id shape per level, duplicate view ids and files, element collisions, --model-id rules, the no-match warning |
qualified-relations.feature |
the four representations of a relationship: that every resource is reachable from its source, that rdf:reifies carries a real RDF 1.2 triple term, and that retyping a class does not reuse a predicate declared against the class it replaced |
qualified-relations.feature is worth knowing about when adding a converter. Two of its steps —
every relationship is reachable from its source and every direct triple is bridged to its
relationship — assert cross-converter invariants rather than one notation's behaviour. They exist
because those invariants decayed silently: converters were added over time and each settled the
question for itself, so four of six ended up emitting no way into a relationship at all.
Scenarios run the fat JARs as subprocesses, for two reasons: the commands end in
exitProcess(), which would take the test JVM with them, and exit codes plus the
[ERROR] / [WARN] lines are part of the contract, so they need a real process to exist in.
:spec:test therefore depends on the shadowJar tasks. The module ships no production code and
is not part of the distribution.
Assertions read the emitted RDF with RDF4J rather than grepping text, so a step that says "exactly one element" means one subject in the graph:
Scenario: An element drawn in both diagrams is one element
Given a file "index.yaml" containing:
"""
model:
id: order-domain
views:
- id: order-flow
file: order-flow.puml
- id: fulfilment-flow
file: fulfilment-flow.puml
"""
When I convert "order-flow.puml fulfilment-flow.puml" with the index "index.yaml"
Then the run succeeds
And there is exactly one element "plantuml/order-domain/element/OrderService"
And the element "plantuml/order-domain/element/OrderService" is depicted in 2 views
Adding a scenario¶
- Write it in the feature file first, in the vocabulary of the docs — model, view, element.
- Reuse the existing steps if you can; they cover writing files, running
convert/render, and asserting on graphs, exit codes and output. - Only add a step when the observation is new, not when the subject is. Keep new steps thin
and put any logic in
ConverterWorld.
Prefer Scenario Outline when the rule is one behaviour over many inputs — the id-validation
matrix is one outline with two Examples tables rather than six scenarios.
Unit test structure¶
| Module | Test class | Tests |
|---|---|---|
core |
DiagramsIndexTest |
Schema detection (grouped / flat), metadata inheritance, lifecycle-state filtering and validation, id validation per level, duplicate detection, input-to-entry lookup |
converter-plantuml |
PlantUmlConverterTest |
Parser, emitter, verification, type mapping, no gen IRIs |
converter-bpmn |
BpmnConverterTest |
CMOF parser, emitter, endpoints, no gen IRIs |
converter-bpmn |
BpmnTtlOutputTest |
Namespace alignment, element typing, relationship typing, IRI patterns, properties, folders, provenance, views, counts |
ConversionVerifier¶
The shared ConversionVerifier in core provides reusable checks:
// Full verification (all checks)
val result = ConversionVerifier.verify(rdfModel)
assertTrue(result.passed, "Issues: ${result.issues}")
// Individual checks
ConversionVerifier.checkRelationshipsHaveEndpoints(model, nsCore)
ConversionVerifier.checkViewNodesHaveView(model, nsCoreVis)
ConversionVerifier.checkViewLinksHaveSourceTarget(model, nsCoreVis)
ConversionVerifier.checkNoGeneratedIris(model)
ConversionVerifier.checkAllDiagramsHaveNodes(model, nsCore, nsCoreVis)
// Entity counts for assertions
val counts = ConversionVerifier.countEntities(model, nsCore, nsCoreVis)
assertEquals(expectedElements, counts.elements)
assertEquals(expectedRelationships, counts.relationships)
Available checks¶
| Check | What it validates |
|---|---|
checkRelationshipsHaveEndpoints |
Every arch:QualifiedRelationship has arch:source and arch:target |
checkViewNodesHaveView |
Every archvis:ArchNode has archvis:view pointing to a diagram |
checkViewLinksHaveSourceTarget |
Every archvis:Link has archvis:source and archvis:target |
checkNoGeneratedIris |
No _gen_ UUID fallback IRIs in output |
checkAllDiagramsHaveNodes |
Every arch:Diagram has at least one archvis:ArchNode |
Writing TTL assertion tests¶
For verifying specific triple patterns in the output:
@Test
fun `UserTask is typed correctly`() {
val userTaskIri = vf.createIRI("${baseIri}bpmn/$modelId/element/UserTask_1")
val bpmnUserTask = vf.createIRI("https://meta.linked.archi/bpmn/onto#", "UserTask")
val archElement = vf.createIRI("https://meta.linked.archi/core#", "Element")
assertTrue(model.contains(userTaskIri, RDF.TYPE, bpmnUserTask))
assertTrue(model.contains(userTaskIri, RDF.TYPE, archElement))
}
@Test
fun `relationship has correct endpoints`() {
val flowIri = vf.createIRI("${baseIri}bpmn/$modelId/relationship/Flow_1")
val source = vf.createIRI("https://meta.linked.archi/core#", "source")
val startIri = vf.createIRI("${baseIri}bpmn/$modelId/element/StartEvent_1")
assertTrue(model.contains(flowIri, source, startIri))
}