Skip to main content
ElastiCORE 3.0 is a release candidate: 3.0.261022-rc1 on Maven Central. Projects on the 2.2 line: see the 2.2 documentation.
Version: 3.0

Testing rules

Checking with a full build every time is slow. Build a small model yourself inside the rules module and run only the rules, and it is done in a second or two. No project, no generation run and no database needed.

Building the engine​

Build the engine with the same combination as the project - the bundle + our ruleset.

    private RuleEngine engine(SourceCode.TemplateSet set) {
return RuleCatalog.standard().engineFor("hexagonal-springboot") // the bundle that matches architecture in build.gradle
.register(new PetstoreRules())
.templateSet(set); // V3 (Java) or KOTLIN
}

private RuleEngine engine() {
return engine(SourceCode.TemplateSet.V3);
}
ProjectBundle nameTemplateSet
layered + javajpa-springboot-v3V3
ddd + javaddd-springbootV3
hexagonal + javahexagonal-springbootV3
hexagonal + kotlinhexagonal-springboot-ktKOTLIN

Building the model - a YAML line as it is​

    // Customer:
// meta: entity @root @expose(20)
// fields:
// customerId: long @id @sequence
// firstName: string(50)! @search(like)
// email: string(100) @email @pii
private EntitySpec customer() {
return new EntitySpec("Customer")
.domain("petstore", "Petstore")
.meta("root", "")
.meta("expose", "20")
.pkg("entity", PKG + ".domain.model")
.pkg("dto", PKG + ".application.dto")
.pkg("srchdto", PKG + ".application.dto")
.field("customerId", "long @id @sequence")
.field("firstName", "string(50)! @search(like)")
.field("email", "string(100) @email @pii");
}
CodeYAML
new EntitySpec("Customer").domain("petstore", "Petstore")Customer: (domain petstore)
.meta("root", ""), .meta("topic", "petstore.order")meta: entity @root @topic(petstore.order)
.field("email", "string(100) @email @pii")the field line as it is
.pkg("dto", "…")a package from namespace
.config("ps.maxPageSize", "100")config: in env.yml
.query("summarizeByStatus", "List<Object[]>", "SELECT …"), .nativeQuery(...)a method in repository.yml
.addCommand("dispatch", Collections.emptyList())commands:
.containedIn("PurchaseOrder")a member that belongs to a root
.entitySplitForBehavior("PurchaseOrder")a root split into two files - for testing the behavior target
new DomainSpec("petstore", "Petstore").entity(e)a domain (for domain rules)
new EnumSpec("OrderStatus").constant("PLACED", "PLC", "Placed")enum.yml

Rendering and checking​

    @Test
void aRootIsAnAggregateRootAndItsIdAnIdentity() {
String out = engine().run(customer(), "entity").render();
assertTrue(out.contains("@AggregateRoot"), out);
assertTrue(out.contains("import org.jmolecules.ddd.annotation.AggregateRoot;"), out);
assertTrue(out.contains("@Identity"), out);
}

@Test
void aPiiFieldIsWriteOnly() {
String out = engine().run(customer(), "dto").render();
assertTrue(out.contains("@JsonProperty(access = JsonProperty.Access.WRITE_ONLY)"), out);
}

@Test
void aLikeSearchNeedsTwoCharacters() {
String out = engine().run(customer(), "srchdto").render();
assertTrue(out.contains("@Size(min = 2)"), out);
}

run(model, "target").render() returns the whole file that would be generated, as a string. If you are curious about the result, just look at it with System.out.println(out). Pass out as the failure message and you see the file when a test fails.

New files - the package and the slots​

    @Test
void eachEventGetsATopic() {
DomainSpec petstore = new DomainSpec("petstore", "Petstore").entity(purchaseOrder());
assertEquals(1, new PetstoreRules().artifactsFor(petstore).size());
// The generator hands the rule the package artifactsFor chose; a test says it.
SourceCode code = engine().packageName(PKG + ".domain.event").run(petstore, "eventTopics");
String out = code.render();
assertTrue(out.contains("PURCHASE_ORDER_SHIPPED = \"petstore.order.shipped\";"), out);
// An unfilled ${slot} renders as an empty line, not an error: ask for them.
assertTrue(code.unfilledSlots().isEmpty(), String.valueOf(code.unfilledSlots()));
}

@Test
void eventTopicsInKotlin() {
DomainSpec petstore = new DomainSpec("petstore", "Petstore").entity(purchaseOrder());
SourceCode code = engine(SourceCode.TemplateSet.KOTLIN).packageName(PKG + ".domain.event")
.run(petstore, "eventTopics");
assertTrue(code.render().contains("const val PURCHASE_ORDER_SHIPPED = \"petstore.order.shipped\""));
assertTrue(code.unfilledSlots().isEmpty(), String.valueOf(code.unfilledSlots()));
}

@Test
void noTopicNoFile() {
assertTrue(new PetstoreRules().artifactsFor(new DomainSpec("petstore", "Petstore").entity(customer())).isEmpty());
}

The skeletons are in the rules module's src/main/resources, so tests find them as they are. Put a skeleton used only in tests in src/test/resources/elasticore-template/v3/.

Model checks​

    @AfterEach
void forget() {
ModelCheck.reset();
}

@Test
void selectStarIsRefused() {
EntitySpec order = purchaseOrder()
.nativeQuery("everything", "List<Object[]>", "SELECT * FROM ps_purchase_order");
engine().run(order, "repository");
assertEquals(1, ModelCheck.failures().size(), String.valueOf(ModelCheck.failures()));
}

Only fail collects in ModelCheck.failures(). warn goes to the console, so to test a warning, swap System.out for a moment and capture it.

Comparing with the default rules​

"What did our rule take out of the default output" is checked against a render without the ruleset:

    @Test
void dynamicUpdateIsTakenOff() {
assertFalse(engine().run(customer(), "entity").render().contains("@DynamicUpdate"));
String builtIn = RuleCatalog.standard().engineFor("hexagonal-springboot")
.templateSet(SourceCode.TemplateSet.V3).run(customer(), "entity").render();
assertTrue(builtIn.contains("@DynamicUpdate"), builtIn);
}
./gradlew :ps-rules:test

The pet store's PetstoreRulesTest has 21 tests and finishes in under 2 seconds.

Checking in the application​

Rule tests check "what is generated". Whether "what is generated works" is checked with application tests. Write down what the rules promise:

src/test/java/io/elasticore/sample/petstore/PetstoreRulesAppliedTest.java
/** What the team rules promise, on the application they generated. */
@SpringBootTest
@AutoConfigureMockMvc // Boot 4: org.springframework.boot.webmvc.test.autoconfigure
class PetstoreRulesAppliedTest {

@Autowired MockMvc mvc;

@Test
void everyRootSaysItsRole() {
for (Class<?> root : new Class<?>[]{Category.class, Product.class, Item.class, Customer.class}) {
assertTrue(root.isAnnotationPresent(AggregateRoot.class), root.getSimpleName());
}
}

@Test
void aOneLetterSearchIsRefused() throws Exception {
mvc.perform(post("/api/petstore/category/search").contentType(MediaType.APPLICATION_JSON)
.content("{\"name\":\"a\"}"))
.andExpect(status().isBadRequest());
mvc.perform(post("/api/petstore/category/search").contentType(MediaType.APPLICATION_JSON)
.content("{\"name\":\"fi\"}"))
.andExpect(status().isOk());
}

@Test
void aCategoryCannotBeDeleted() throws Exception {
mvc.perform(delete("/api/petstore/category/1000")).andExpect(status().isMethodNotAllowed());
}
}
./gradlew :test --tests '*PetstoreRulesAppliedTest'     # 3 tests, 0 failures

What was actually checked in the pet store:

RuleRequestResult
ps-piiPOST /api/petstore/customer (with email)200, no email in the response
ps-like-minPOST /api/petstore/category/search {"name":"a"}400 {"code":"invalid","fieldErrors":[{"field":"name","code":"Size",…}]}
ps-like-minthe same with {"name":"fi"}200
ps-no-deleteDELETE /api/petstore/category/1000, POST /api/petstore/category/delete405

The 400 from ps-like-min needs the search endpoint to take its body with @Valid - controllers generated from the release after rc1 do. A controller generated by rc1 has no @Valid on search(@RequestBody …), so the rule's @Size is not checked (200). A controller is a developer's file, so add @Valid yourself to controllers you have already changed.