규칙 테스트
매번 전체 빌드로 확인하면 느립니다. 규칙 모듈 안에서 작은 모델을 직접 만들어 규칙만 실행하면 1~2초에 끝납니다. 프로젝트도, 생성 실행도, 데이터베이스도 필요 없습니다.
엔진 만들기
프로젝트와 같은 조합 - 번들 + 우리 룰셋 - 으로 엔진을 만듭니다.
private RuleEngine engine(SourceCode.TemplateSet set) {
return RuleCatalog.standard().engineFor("hexagonal-springboot") // build.gradle 의 architecture 에 맞는 번들
.register(new PetstoreRules())
.templateSet(set); // V3 (Java) 또는 KOTLIN
}
private RuleEngine engine() {
return engine(SourceCode.TemplateSet.V3);
}
| 프로젝트 | 번들 이름 | TemplateSet |
|---|---|---|
layered + java | jpa-springboot-v3 | V3 |
ddd + java | ddd-springboot | V3 |
hexagonal + java | hexagonal-springboot | V3 |
hexagonal + kotlin | hexagonal-springboot-kt | KOTLIN |
모델 만들기 - YAML 한 줄을 그대로
// 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");
}
| 코드 | YAML |
|---|---|
new EntitySpec("Customer").domain("petstore", "Petstore") | Customer: (도메인 petstore) |
.meta("root", ""), .meta("topic", "petstore.order") | meta: entity @root @topic(petstore.order) |
.field("email", "string(100) @email @pii") | 필드 한 줄 그대로 |
.pkg("dto", "…") | namespace 의 패키지 |
.config("ps.maxPageSize", "100") | env.yml 의 config: |
.query("summarizeByStatus", "List<Object[]>", "SELECT …"), .nativeQuery(...) | repository.yml 의 메서드 |
.addCommand("dispatch", Collections.emptyList()) | commands: |
.containedIn("PurchaseOrder") | 루트에 속한 멤버 |
.entitySplitForBehavior("PurchaseOrder") | 두 파일로 나 뉘는 루트 - behavior target 을 테스트할 때 |
new DomainSpec("petstore", "Petstore").entity(e) | 도메인 (도메인 단위 규칙용) |
new EnumSpec("OrderStatus").constant("PLACED", "PLC", "Placed") | enum.yml |
렌더링하고 확인하기
@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(모델, "target").render() 는 생성될 파일 전체를 문자열로 돌려줍니다. 결과가 궁금하면 System.out.println(out) 으로
그대로 보세요. 실패 메시지에 out 을 넘겨 두면 실패할 때 파일이 보입니다.
새 파일 - 패키지와 빈칸
@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());
}
스켈레톤은 규칙 모듈의 src/main/resources 에 있으므로 테스트도 그대로 찾습니다. 테스트에서만 쓰는 스켈레톤은
src/test/resources/elasticore-template/v3/ 에 둡니다.
모델 검사
@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()));
}
ModelCheck.failures() 에는 fail 만 쌓입니다. warn 은 콘솔로 나가므로, 경고를 테스트하려면 System.out 을 잠시 바꿔
잡아서 봅니다.
기본 규칙과 비교하기
"우리 규칙이 기본 출력에서 무엇을 뺐나" 는 룰셋 없이 렌더링한 것과 비교합니다:
@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