Skip to content

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

  1. Write it in the feature file first, in the vocabulary of the docs — model, view, element.
  2. Reuse the existing steps if you can; they cover writing files, running convert / render, and asserting on graphs, exit codes and output.
  3. 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))
}