Skip to main content

Defining Test Models with Onion Specs

Onion specs are an API to define test models in a compact way. They are layered, like onions, to construct a hierarchical model.

In this example, you can see how a Component with 3 DataElements is constructed by nesting the specification methods:

component("testComponent",
dataElement("City"),
dataElement("Country"),
dataElement("District"));

Each specification methods has a similar signature:

public static ComponentSpec component(String specification, Spec... children) {}
  1. The first argument, specification, describes the element itself. Usually, it is the name of the element.
  2. After this, you can add any number of child specifications, which are then applied to the parent.

Setting Attributes/References

To set an attribute other than name, we use CommonSpecs.set(attributeName, value).

E.g. to set the packageName of a DataElement:

import static net.democritus.spec.CommonSpecs.set;
//...
dataElement("City",
set("packageName", "net.demo.cities"))

References to other elements are defined with CommonSpecs.dataRef(referenceName, key).

E.g. to set the targetElement of a TaskElement:

import static net.democritus.spec.CommonSpecs.dataRef;
//...
component("testComp",
dataElement("City"),
taskElement("CityExporter",
dataRef("targetElement", "testComp::City")))
element keys

The notation used here to define the City dataElement is a functional key. It references the element by adding the name of the component. In the same way, we can reference a field with testComp::City::postcode

Element Options

Options can be added to elements using option("name: value").

dataElement("City",
option("cruds.table.pageSize: 50"))

If the option does not require a value, use option("name") instead.

DataElements, Fields and Finders

A dataElement can contain fields and finders. To reduce the complexity, fields and finders have a shorthand syntax:

dataElement("City",
field("postcode: String*"),
field("country: Country[Ln01]"),
field("/population: Long*"),
finder("findByPostcodeEq"))

In this example we have:

  • A ValueField postcode with ValueType String
  • A LinkField country with target element Country and LinkFieldType Ln01
  • A CalculatedField (recognized by the /-prefix) population with ValueType Long
  • A Finder findByPostcodeEq with FieldOperatorPair postcode-eq, which is added based on the Finder's name
ValueType vs ValueFieldType

A ValueField can either be defined as field("name: valueType*") or field("name: valueFieldType"). The first variation (with *) will use the newer ValueType. The second variation will use the outdated ValueFieldType.

DataProjections

To define a dataProjection, you can add referenceFields and calculatedFields. CalculatedFields will have a syntax similar to the value-fields. ReferenceFields should contain the name of the fields they are referencing.

dataElement("City",
field("postcode: String*"),
field("country: Country[Ln01]"),
dataProjection("AreaInfo",
referenceField("postcode"),
calculatedField("size: Long")))

DataCommands

DataCommands can have connectorFields which are defined the same as regular fields, except they cannot be calculated.

dataElement("City",
field("postcode: String*"),
field("country: Country[Ln01]"),
dataCommand("addDistrict",
set("hasTargetInstance", true),
connectorField("name: String*"),
connectorField("location: Location[Ln01]")))