بنقرة واحدة
projection-test-builder
Knows how to generate test cases for commands and projections
التثبيت باستخدام Codex أو Claude انسخ هذا Prompt والصقه في Codex أو Claude أو مساعد آخر ليراجع صفحة Skill ويثبّتها لك.
القائمة
Knows how to generate test cases for commands and projections
التثبيت باستخدام Codex أو Claude انسخ هذا Prompt والصقه في Codex أو Claude أو مساعد آخر ليراجع صفحة Skill ويثبّتها لك.
استنادا إلى تصنيف SOC المهني
| name | projection test builder |
| description | Knows how to generate test cases for commands and projections |
This skill generates projection integration tests from STATE_VIEW slice specifications. It reads the specification's given/then structure and creates Kotlin tests that verify the read model projection behavior using the Axon Framework event sourcing pattern.
Use this skill when:
given (events) and then (expected read model state)The slice JSON contains specifications with this structure:
{
"specifications": [
{
"title": "spec: cart items with removed item",
"given": [
{
"title": "Cart Created",
"type": "SPEC_EVENT",
"fields": [{ "name": "aggregateId", "type": "UUID", "example": "" }]
},
{
"title": "Item Added",
"type": "SPEC_EVENT",
"fields": [
{ "name": "aggregateId", "type": "UUID" },
{ "name": "description", "type": "String" },
{ "name": "price", "type": "Double", "example": "9.99" }
]
}
],
"when": [],
"then": [
{
"title": "cart items",
"type": "SPEC_READMODEL",
"fields": [
{ "name": "aggregateId", "type": "UUID", "idAttribute": true },
{ "name": "totalPrice", "type": "Double", "example": "19.98" }
]
}
],
"comments": [{ "description": "Read Model should display an empty list" }]
}
]
}
Location Pattern: src/test/kotlin/de/alex/<module>/<readmodelname>/integration/<ReadModelName>ProjectionTest.kt
<module> from the readmodel's domain or context field (lowercase)<readmodelname> from the readmodel's title (convert to lowercase, no spaces)src/test/kotlin/de/alex/cart/cartitems/integration/CartItemsProjectionTest.ktTransform event titles to Kotlin event class names:
ItemAddedEventCartCreatedEventItemRemovedEventPattern: Remove spaces, append "Event" suffix.
Look at the aggregate or aggregateDependencies field in the readmodel to identify which aggregate to use:
"aggregate": "Cart",
"aggregateDependencies": ["Cart"]
This becomes CartAggregate for the repository.
Template:
/* (C)2024 */
package de.alex.<module>.<readmodelname>.integration
import de.alex.<module>.<readmodelname>.<ReadModelName>ReadModel
class <ReadModelName>ProjectionTest : BaseIntegrationTest() {
@Autowired private lateinit var commandGateway: CommandGateway
@Autowired private lateinit var queryGateway: QueryGateway
@Autowired private lateinit var repository: Repository<<Aggregate>Aggregate>
// Generate one test method per specification
}
For each specification in specifications[]:
@Test
fun `<spec title>`() {
// Generate UUIDs for id fields
val aggregateId = UUID.randomUUID()
// Generate additional UUIDs for other id fields (itemId, productId, etc.)
var fixture =
ProjectionFixtureConfiguration.aggregateInstance {
repository.newInstance { <Aggregate>Aggregate() }
}
// Apply events from given[] in index order
fixture.given(
*listOf(
// For each event in given[], sorted by index:
RandomData.newInstance<<EventName>Event> {
// Set fields from the spec, using example values or generated UUIDs
this.aggregateId = aggregateId
// Map other fields
},
).toTypedArray(),
)
fixture.apply()
// Verify read model state from then[]
awaitUntilAssserted {
var readModel =
queryGateway.query(
<ReadModelName>ReadModelQuery(aggregateId),
<ReadModelName>ReadModel::class.java
)
// Assert based on then[] specification
// If comments mention "empty list" → assertThat(readModel.get().data).isEmpty()
// Otherwise assert expected field values
}
}
idAttribute: true should use shared UUIDs across related eventsval aggregateId = UUID.randomUUID()itemId, productId), generate eachexample value (e.g., "example": "9.99"), use that value for assertions"9.99" for Double → 9.99"19.98" for Double → 19.98mapping field indicates the source field: "mapping": "productId" means map from productIdWhen comments contain "empty list" or the then[] result should be empty:
assertThat(readModel.get().data).isEmpty()
When multiple items expected:
assertThat(readModel.get().data).hasSize(2)
When then[] has specific field examples:
assertThat(readModel.get().data.totalPrice).isEqualTo(19.98)
Basic non-null check:
assertThat(readModel.get()).isNotNull
Input Spec:
{
"title": "spec: cart items with cleared cart",
"given": [
{ "title": "Cart Created", "index": 0, "fields": [{"name": "aggregateId", "type": "UUID"}] },
{ "title": "Item Added", "index": 1, "fields": [...] },
{ "title": "Cart Cleared", "index": 2, "fields": [{"name": "aggregateId", "type": "UUID"}] }
],
"then": [
{ "title": "cart items", "type": "SPEC_READMODEL", "fields": [...] }
],
"comments": [{ "description": "Read Model should display an empty list" }]
}
Generated Test:
@Test
fun `spec cart items with cleared cart`() {
val aggregateId = UUID.randomUUID()
val itemId = UUID.randomUUID()
val productId = UUID.randomUUID()
var fixture =
ProjectionFixtureConfiguration.aggregateInstance {
repository.newInstance { CartAggregate() }
}
fixture.given(
*listOf(
RandomData.newInstance<CartCreatedEvent> {
this.aggregateId = aggregateId
},
RandomData.newInstance<ItemAddedEvent> {
this.aggregateId = aggregateId
this.itemId = itemId
this.productId = productId
},
RandomData.newInstance<CartClearedEvent> {
this.aggregateId = aggregateId
},
).toTypedArray(),
)
fixture.apply()
awaitUntilAssserted {
var readModel =
queryGateway.query(
CartItemsReadModelQuery(aggregateId),
CartItemsReadModel::class.java
)
assertThat(readModel.get().data).isEmpty()
}
}
When a slice has multiple specifications, generate one test method per spec:
class CartItemsProjectionTest : BaseIntegrationTest() {
@Autowired private lateinit var commandGateway: CommandGateway
@Autowired private lateinit var queryGateway: QueryGateway
@Autowired private lateinit var repository: Repository<CartAggregate>
@Test
fun `spec cart items`() {
// Test for adding items - expects 2 items in list
}
@Test
fun `spec cart items with removed item`() {
// Test for removing item - expects empty list
}
@Test
fun `spec cart items with cleared cart`() {
// Test for clearing cart - expects empty list
}
@Test
fun `spec cart items with archived items`() {
// Test for archiving - expects item removed
}
}
When the given[] has multiple events with the same title (e.g., two "Item Added" events):
itemId for each)aggregateId to link themval itemId1 = UUID.randomUUID()
val itemId2 = UUID.randomUUID()
fixture.given(
*listOf(
RandomData.newInstance<CartCreatedEvent> { this.aggregateId = aggregateId },
RandomData.newInstance<ItemAddedEvent> {
this.aggregateId = aggregateId
this.itemId = itemId1
this.price = 9.99
},
RandomData.newInstance<ItemAddedEvent> {
this.aggregateId = aggregateId
this.itemId = itemId2
this.price = 9.99
},
).toTypedArray(),
)
When an event removes/archives an item, the subsequent event should reference the same ID:
// Item Added with itemId1
RandomData.newInstance<ItemAddedEvent> {
this.aggregateId = aggregateId
this.itemId = itemId1
},
// Item Removed referencing the same itemId1
RandomData.newInstance<ItemRemovedEvent> {
this.aggregateId = aggregateId
this.itemId = itemId1
},
Ensure these imports are available:
de.alex.common.support.BaseIntegrationTestde.alex.common.support.ProjectionFixtureConfigurationde.alex.common.support.RandomDatade.alex.common.support.awaitUntilAsssertedde.alex.events.*de.alex.domain.*