본문으로 건너뛰기
ElastiCORE 3.0 은 릴리스 후보(RC)입니다: Maven Central 의 3.0.261022-rc1. 2.2 라인 프로젝트는 2.2 문서를 보세요.
버전: 3.0

규칙 테스트

매번 전체 빌드로 확인하면 느립니다. 규칙 모듈 안에서 작은 모델을 직접 만들어 규칙만 실행하면 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 + javajpa-springboot-v3V3
ddd + javaddd-springbootV3
hexagonal + javahexagonal-springbootV3
hexagonal + kotlinhexagonal-springboot-ktKOTLIN

모델 만들기 - 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

펫스토어의 PetstoreRulesTest 는 21개 테스트이고 2초 안에 끝납니다.

애플리케이션에서 확인하기​

규칙 테스트는 "무엇이 생성되나" 를 봅니다. "생성된 것이 동작하나" 는 애플리케이션 테스트로 봅니다. 규칙이 약속하는 것을 그대로 적습니다:

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

펫스토어에서 실제로 확인한 것:

규칙요청결과
ps-piiPOST /api/petstore/customer (email 포함)200, 응답에 email 없음
ps-like-minPOST /api/petstore/category/search {"name":"a"}400 {"code":"invalid","fieldErrors":[{"field":"name","code":"Size",…}]}
ps-like-min같은 곳 {"name":"fi"}200
ps-no-deleteDELETE /api/petstore/category/1000, POST /api/petstore/category/delete405

ps-like-min 의 400 은 검색 엔드포인트가 @Valid 로 받을 때입니다 - rc1 다음 릴리스부터 생성되는 컨트롤러. rc1 로 생성한 컨트롤러에는 search(@RequestBody …) 에 @Valid 가 없어 규칙의 @Size 가 검사되지 않습니다(200). 컨트롤러는 개발자 파일이므로 이미 고친 컨트롤러에는 직접 @Valid 를 붙이세요.