# Testing Guide
Comprehensive guide to testing Embabel agents, from unit testing individual actions to end-to-end testing complete agent workflows. Embabel is designed for testability from the ground up.
## Testing Philosophy
Embabel follows the same testing principles as Spring Framework:
- **Unit tests** for individual components (actions, conditions)
- **Integration tests** for agent workflows
- **End-to-end tests** for complete business scenarios
- **Test doubles** for external dependencies (LLMs, services)
### The Kitchen Testing Analogy π¨βπ³
Testing agents is like testing a professional kitchen:
- **Unit tests** = Test individual cooking techniques (chop vegetables, make sauce)
- **Integration tests** = Test recipe execution (making a complete dish)
- **E2E tests** = Test entire service (customer order β delivered meal)
- **Mock services** = Practice with fake ingredients instead of expensive real ones
## Testing Strategy Overview
### Testing Pyramid for Agents
```
πΊ E2E Tests
πΊπΊ Integration Tests
πΊπΊπΊ Unit Tests
πΊπΊπΊπΊ Component Tests
```
- **Component Tests** (70%) - Individual actions and conditions
- **Unit Tests** (20%) - Agent behavior and business logic
- **Integration Tests** (8%) - Agent workflow execution
- **E2E Tests** (2%) - Complete business scenarios
## Test Infrastructure Setup
### Maven Dependencies
```xml
com.embabel.agent
embabel-agent-test-support
${embabel-agent.version}
test
org.springframework.boot
spring-boot-starter-test
test
org.mockito
mockito-kotlin
test
org.testcontainers
junit-jupiter
test
```
### Test Configuration
```yaml
# application-test.yml
embabel:
agent:
platform:
test:
mock-mode: true # Use test doubles
logging:
personality: default # Clean test output
verbosity: warn # Minimal logging
infrastructure:
neo4j:
enabled: false # Disable external dependencies
mcp:
enabled: false
observability:
enabled: false
shell:
enabled: false # No interactive shell in tests
spring:
profiles:
active: test
logging:
level:
com.embabel: WARN
org.springframework: ERROR
```
## Unit Testing Actions
### Basic Action Testing
```kotlin
@ExtendWith(MockitoExtension::class)
class OrderProcessingAgentTest {
@Mock
private lateinit var inventoryService: InventoryService
@Mock
private lateinit var paymentService: PaymentService
@InjectMocks
private lateinit var agent: OrderProcessingAgent
@Test
fun `should validate order and identify risk factors`() {
// Arrange
val context = FakeOperationContext()
context.expectResponse(OrderValidation(
isValid = true,
issues = listOf("High quantity order"),
riskScore = 0.3
))
val order = CustomerOrder(
orderId = "ORD-TEST-001",
customerId = "CUSTOMER-001",
items = listOf(
OrderItem("LAPTOP-001", "Gaming Laptop", Money(1500.0), 10) // High quantity
),
shippingAddress = Address("123 Test St", "Test City", "TC", "12345"),
paymentMethod = CreditCard("1234-5678-9012-3456", LocalDate.now().plusYears(2))
)
// Act
val result = agent.validateOrder(order, context)
// Assert
assertThat(result.isValid).isTrue()
assertThat(result.issues).contains("High quantity order")
assertThat(result.riskScore).isEqualTo(0.3)
// Verify LLM interaction
val llmInvocation = context.llmInvocations.first()
assertThat(llmInvocation.prompt).contains("ORD-TEST-001")
assertThat(llmInvocation.prompt).contains("Gaming Laptop")
assertThat(llmInvocation.prompt).contains("quantity: 10")
}
@Test
fun `should check inventory for all order items`() {
// Arrange
`when`(inventoryService.getAvailableQuantity("LAPTOP-001")).thenReturn(15)
`when`(inventoryService.getAvailableQuantity("MOUSE-002")).thenReturn(0)
`when`(inventoryService.getExpectedRestockDate("MOUSE-002"))
.thenReturn(LocalDate.now().plusWeeks(2))
val order = CustomerOrder(
orderId = "ORD-TEST-002",
customerId = "CUSTOMER-002",
items = listOf(
OrderItem("LAPTOP-001", "Gaming Laptop", Money(1500.0), 2),
OrderItem("MOUSE-002", "Gaming Mouse", Money(75.0), 3)
),
shippingAddress = Address("456 Test Ave", "Test Town", "TT", "54321"),
paymentMethod = CreditCard("9876-5432-1098-7654", LocalDate.now().plusYears(1))
)
// Act
val result = agent.checkInventory(order)
// Assert
assertThat(result).hasSize(2)
val laptopCheck = result.find { it.productId == "LAPTOP-001" }!!
assertThat(laptopCheck.availableQuantity).isEqualTo(15)
assertThat(laptopCheck.expectedRestockDate).isNull()
val mouseCheck = result.find { it.productId == "MOUSE-002" }!!
assertThat(mouseCheck.availableQuantity).isEqualTo(0)
assertThat(mouseCheck.expectedRestockDate).isNotNull()
// Verify service interactions
verify(inventoryService).getAvailableQuantity("LAPTOP-001")
verify(inventoryService).getAvailableQuantity("MOUSE-002")
verify(inventoryService).getExpectedRestockDate("MOUSE-002")
}
}
```
### Testing LLM Interactions
```kotlin
class LlmInteractionTest {
@Test
fun `should use correct model and temperature for creative tasks`() {
val context = FakeOperationContext()
context.expectResponse(ProductRecommendations(
recommendations = listOf(
Recommendation("Wireless Headphones", "Perfect for gaming", 4.5),
Recommendation("RGB Keyboard", "Enhance your setup", 4.3)
)
))
val agent = RecommendationAgent()
val customer = Customer("John", interests = listOf("gaming", "technology"))
agent.generateRecommendations(customer, context)
val llmInvocation = context.llmInvocations.first()
// Verify model selection
assertThat(llmInvocation.interaction.llmOptions.model)
.isEqualTo(OpenAiModels.GPT_4O_MINI)
// Verify temperature for creativity
assertThat(llmInvocation.interaction.llmOptions.temperature)
.isEqualTo(0.8)
// Verify prompt engineering
assertThat(llmInvocation.prompt).contains("gaming")
assertThat(llmInvocation.prompt).contains("technology")
assertThat(llmInvocation.prompt).contains("recommendations")
}
@Test
fun `should handle LLM errors gracefully`() {
val context = FakeOperationContext()
context.expectLlmError(RuntimeException("LLM service unavailable"))
val agent = RecommendationAgent()
val customer = Customer("Jane", interests = listOf("books"))
// Should not throw exception
assertThatCode {
agent.generateRecommendations(customer, context)
}.doesNotThrowAnyException()
// Should log error appropriately
// ... verify logging behavior
}
}
```
### Testing Tool Integration
```kotlin
class WebSearchActionTest {
@Test
fun `should use web tools for market research`() {
val context = FakeOperationContext()
context.expectResponse(MarketResearch(
trends = listOf("Increased demand for sustainable products"),
competitors = listOf("EcoTech Corp", "GreenSolutions Inc"),
insights = "Market growing at 15% annually"
))
val agent = MarketResearchAgent()
val query = ResearchQuery("sustainable technology trends 2024")
val result = agent.conductMarketResearch(query, context)
// Verify tool groups were specified
val toolGroups = context.llmInvocations.first().interaction.toolGroups
assertThat(toolGroups).contains(ToolGroup.WEB)
// Verify result structure
assertThat(result.trends).isNotEmpty()
assertThat(result.competitors).hasSize(2)
assertThat(result.insights).contains("15%")
}
}
```
## Integration Testing Agent Workflows
### Agent Integration Test Setup
```kotlin
@SpringBootTest(
classes = [TestConfiguration::class],
webEnvironment = SpringBootTest.WebEnvironment.NONE
)
@TestPropertySource(properties = [
"embabel.agent.platform.test.mockMode=true",
"embabel.agent.logging.personality=default"
])
class OrderProcessingAgentIntegrationTest {
@Autowired
private lateinit var agentPlatform: AgentPlatform
@Autowired
private lateinit var orderProcessingAgent: OrderProcessingAgent
@MockBean
private lateinit var inventoryService: InventoryService
@MockBean
private lateinit var paymentService: PaymentService
@TestConfiguration
class TestConfiguration {
@Bean
@Primary
fun testOrderProcessingAgent(
inventoryService: InventoryService,
paymentService: PaymentService,
fulfillmentService: FulfillmentService,
customerService: CustomerService
): OrderProcessingAgent {
return OrderProcessingAgent(
inventoryService,
paymentService,
fulfillmentService,
customerService
)
}
}
}
```
### Testing Complete Workflows
```kotlin
class CompleteWorkflowTest : OrderProcessingAgentIntegrationTest() {
@Test
fun `should process standard order from validation to fulfillment`() {
// Arrange - Mock external services
`when`(inventoryService.getAvailableQuantity(any())).thenReturn(100)
`when`(paymentService.processPayment(any(), any())).thenReturn(
PaymentResult(success = true, transactionId = "TXN-12345", errorMessage = null)
)
val order = CustomerOrder(
orderId = "ORD-INTEGRATION-001",
customerId = "CUSTOMER-STANDARD",
items = listOf(
OrderItem("BOOK-001", "Spring Framework Guide", Money(45.0), 1),
OrderItem("PEN-002", "Premium Pen", Money(15.0), 2)
),
shippingAddress = Address("789 Integration St", "Test City", "TC", "11111"),
paymentMethod = CreditCard("1111-2222-3333-4444", LocalDate.now().plusYears(1))
)
// Act - Execute agent in focused mode
val result = agentPlatform.run(orderProcessingAgent::class, order)
// Assert - Verify complete workflow execution
assertThat(result).isInstanceOf()
assertThat(result.status).isEqualTo(OrderStatus.COMPLETED)
assertThat(result.originalOrder.orderId).isEqualTo("ORD-INTEGRATION-001")
assertThat(result.trackingNumber).isNotNull()
assertThat(result.estimatedDelivery).isAfter(LocalDate.now())
// Verify service interactions occurred
verify(inventoryService, atLeastOnce()).getAvailableQuantity(any())
verify(paymentService).processPayment(any(), any())
}
@Test
fun `should handle VIP customer orders with priority processing`() {
// Arrange - VIP customer scenario
`when`(inventoryService.getAvailableQuantity(any())).thenReturn(50)
`when`(paymentService.processPayment(any(), any())).thenReturn(
PaymentResult(success = true, transactionId = "TXN-VIP-789", errorMessage = null)
)
val vipOrder = CustomerOrder(
orderId = "ORD-VIP-001",
customerId = "VIP-CUSTOMER-001", // VIP customer
items = listOf(
OrderItem("LUXURY-LAPTOP", "Premium Gaming Laptop", Money(3500.0), 1)
),
shippingAddress = Address("1 Executive Plaza", "Premium City", "PC", "99999"),
paymentMethod = CreditCard("9999-8888-7777-6666", LocalDate.now().plusYears(3))
)
// Act
val result = agentPlatform.run(orderProcessingAgent::class, vipOrder)
// Assert - VIP processing characteristics
assertThat(result.status).isEqualTo(OrderStatus.COMPLETED)
assertThat(result.estimatedDelivery).isBefore(
LocalDate.now().plusDays(2) // Expedited shipping
)
assertThat(result.internalNotes).containsIgnoringCase("VIP")
}
@Test
fun `should handle inventory shortage scenarios`() {
// Arrange - Out of stock scenario
`when`(inventoryService.getAvailableQuantity("RARE-ITEM-001")).thenReturn(0)
`when`(inventoryService.getExpectedRestockDate("RARE-ITEM-001"))
.thenReturn(LocalDate.now().plusWeeks(3))
val backorderOrder = CustomerOrder(
orderId = "ORD-BACKORDER-001",
customerId = "CUSTOMER-PATIENT",
items = listOf(
OrderItem("RARE-ITEM-001", "Limited Edition Collectible", Money(500.0), 1)
),
shippingAddress = Address("456 Collector Lane", "Hobby City", "HC", "22222"),
paymentMethod = CreditCard("2222-3333-4444-5555", LocalDate.now().plusYears(2))
)
// Act - Agent should adapt to inventory constraints
val result = agentPlatform.run(orderProcessingAgent::class, backorderOrder)
// Assert - Graceful handling of out-of-stock
assertThat(result.customerMessage).contains("out of stock", "backorder")
assertThat(result.estimatedDelivery).isAfter(LocalDate.now().plusWeeks(2))
}
}
```
### Testing Agent Modes
```kotlin
class AgentModeTest {
@Test
fun `focused mode should execute specific agent`() {
val userInput = "Process order ORD-123 for customer CUST-456"
// Execute in focused mode - targets specific agent
val result = agentPlatform.run(OrderProcessingAgent::class, userInput)
assertThat(result).isInstanceOf()
}
@Test
fun `closed mode should select appropriate agent dynamically`() {
val userInput = "I want to place an order for some books"
// Execute in closed mode - platform selects best agent
val result = agentPlatform.execute(userInput)
// Platform should have selected OrderProcessingAgent
assertThat(result).isInstanceOf()
}
@Test
fun `open mode should combine multiple agent capabilities`() {
val complexRequest = """
Research the latest sustainable technology trends,
recommend products that match those trends,
and create a purchase plan within a $2000 budget
""".trimIndent()
// Execute in open mode - combines research + recommendation + ordering
val result = agentPlatform.executeOpen(complexRequest)
// Should combine capabilities from multiple agents
assertThat(result).isInstanceOf()
}
}
```
## Testing Conditions and Business Rules
### Custom Condition Testing
```kotlin
class OrderProcessingConditionsTest {
private lateinit var conditions: OrderProcessingConditions
@BeforeEach
fun setUp() {
conditions = OrderProcessingConditions()
}
@Test
fun `should require VIP processing for high-value orders`() {
val highValueOrder = CustomerOrder(
orderId = "ORD-HIGH-001",
customerId = "CUSTOMER-001",
items = listOf(
OrderItem("EXPENSIVE-ITEM", "Luxury Product", Money(5000.0), 1)
),
shippingAddress = Address("123 Rich St", "Wealthy City", "WC", "99999"),
paymentMethod = CreditCard("1234-5678-9012-3456", LocalDate.now().plusYears(2))
)
val requiresPriority = conditions.requiresPriorityProcessing(highValueOrder)
assertThat(requiresPriority).isTrue()
}
@Test
fun `should validate inventory availability`() {
val order = CustomerOrder(
orderId = "ORD-INV-001",
customerId = "CUSTOMER-002",
items = listOf(
OrderItem("ITEM-A", "Product A", Money(100.0), 5),
OrderItem("ITEM-B", "Product B", Money(50.0), 3)
),
shippingAddress = Address("456 Test Ave", "Test City", "TC", "12345"),
paymentMethod = CreditCard("9876-5432-1098-7654", LocalDate.now().plusYears(1))
)
val inventoryChecks = listOf(
InventoryCheck("ITEM-A", availableQuantity = 10, expectedRestockDate = null),
InventoryCheck("ITEM-B", availableQuantity = 2, expectedRestockDate = LocalDate.now().plusWeeks(1))
)
val hasInventory = conditions.hasInventoryAvailable(order, inventoryChecks)
assertThat(hasInventory).isFalse() // Item B has insufficient quantity
}
}
```
### Testing Condition Evaluation
```kotlin
class ConditionEvaluationTest {
@Test
fun `should evaluate multiple conditions correctly`() {
val testContext = TestAgentContext()
testContext.addDomainObject(order)
testContext.addDomainObject(inventoryChecks)
testContext.addDomainObject(customer)
val conditionResults = testContext.evaluateConditions(
"hasInventoryAvailable",
"requiresPriorityProcessing",
"hasValidPaymentMethod"
)
assertThat(conditionResults).containsExactly(
"hasInventoryAvailable" to true,
"requiresPriorityProcessing" to false,
"hasValidPaymentMethod" to true
)
}
}
```
## End-to-End Testing
### Complete Business Scenario Testing
```kotlin
@SpringBootTest
@Testcontainers
class OrderProcessingE2ETest {
@Container
static val postgres = PostgreSQLContainer("postgres:13").apply {
withDatabaseName("embabel_test")
withUsername("test")
withPassword("test")
}
@Container
static val redis = GenericContainer("redis:6-alpine").apply {
withExposedPorts(6379)
}
@DynamicPropertySource
companion object {
@JvmStatic
fun configureProperties(registry: DynamicPropertyRegistry) {
registry.add("spring.datasource.url", postgres::getJdbcUrl)
registry.add("spring.datasource.username", postgres::getUsername)
registry.add("spring.datasource.password", postgres::getPassword)
registry.add("spring.redis.host", redis::getHost)
registry.add("spring.redis.port", redis::getFirstMappedPort)
}
}
@Autowired
private lateinit var agentPlatform: AgentPlatform
@Autowired
private lateinit var orderRepository: OrderRepository
@Test
fun `should process order end-to-end with real infrastructure`() {
// Arrange - Create realistic test data
val customerOrder = CustomerOrder(
orderId = "ORD-E2E-${UUID.randomUUID()}",
customerId = "CUST-E2E-001",
items = listOf(
OrderItem("BOOK-SPRING", "Spring Framework 6 Guide", Money(65.99), 1),
OrderItem("STICKER-KOTLIN", "Kotlin Logo Stickers", Money(9.99), 2)
),
shippingAddress = Address(
street = "123 Developer Way",
city = "Code City",
state = "CA",
zipCode = "94105"
),
paymentMethod = CreditCard(
number = "4111-1111-1111-1111", // Test card number
expiryDate = LocalDate.now().plusYears(2)
)
)
// Act - Execute complete workflow
val startTime = Instant.now()
val result = agentPlatform.run(OrderProcessingAgent::class, customerOrder)
val endTime = Instant.now()
// Assert - Verify complete end-to-end processing
assertThat(result).isInstanceOf()
assertThat(result.status).isEqualTo(OrderStatus.COMPLETED)
assertThat(result.trackingNumber).isNotNull()
// Verify data persistence
val savedOrder = orderRepository.findById(customerOrder.orderId)
assertThat(savedOrder).isPresent
assertThat(savedOrder.get().status).isEqualTo(OrderStatus.COMPLETED)
// Verify performance
val processingTime = Duration.between(startTime, endTime)
assertThat(processingTime).isLessThan(Duration.ofSeconds(30))
// Verify business rules
assertThat(result.estimatedDelivery).isAfter(LocalDate.now())
assertThat(result.customerMessage).contains("order has been processed")
}
@Test
fun `should handle high-volume concurrent order processing`() {
val orderCount = 10
val orders = (1..orderCount).map { index ->
CustomerOrder(
orderId = "ORD-CONCURRENT-$index",
customerId = "CUST-LOAD-$index",
items = listOf(
OrderItem("ITEM-$index", "Test Product $index", Money(25.0), 1)
),
shippingAddress = Address("$index Test St", "Test City", "TC", "12345"),
paymentMethod = CreditCard("4111-1111-1111-111$index", LocalDate.now().plusYears(1))
)
}
// Execute orders concurrently
val results = orders.parallelStream()
.map { order ->
agentPlatform.run(OrderProcessingAgent::class, order)
}
.collect(Collectors.toList())
// Verify all orders processed successfully
assertThat(results).hasSize(orderCount)
assertThat(results).allMatch { it.status == OrderStatus.COMPLETED }
// Verify all orders persisted
val savedOrders = orderRepository.findAllById(orders.map { it.orderId })
assertThat(savedOrders).hasSize(orderCount)
}
}
```
## Performance Testing
### Agent Performance Benchmarks
```kotlin
@BenchmarkMode(Mode.AverageTime)
@OutputTimeUnit(TimeUnit.MILLISECONDS)
@State(Scope.Benchmark)
class AgentPerformanceBenchmark {
private lateinit var agentPlatform: AgentPlatform
private lateinit var testOrder: CustomerOrder
@Setup
fun setup() {
agentPlatform = createTestAgentPlatform()
testOrder = createStandardTestOrder()
}
@Benchmark
fun benchmarkOrderProcessing(): ProcessedOrder {
return agentPlatform.run(OrderProcessingAgent::class, testOrder)
}
@Benchmark
fun benchmarkAgentSelection(): ProcessedOrder {
return agentPlatform.execute("Process order for customer CUST-BENCH-001")
}
@Benchmark
fun benchmarkComplexPlanning(): ComprehensivePlan {
return agentPlatform.executeOpen("""
Research market trends for sustainable technology,
recommend products under $1000 budget,
create purchase timeline and order plan
""".trimIndent())
}
}
```
### Load Testing Patterns
```kotlin
@Test
fun `should maintain performance under load`() {
val threadCount = 20
val executionsPerThread = 50
val executor = Executors.newFixedThreadPool(threadCount)
val latch = CountDownLatch(threadCount)
val results = Collections.synchronizedList(mutableListOf())
val errors = Collections.synchronizedList(mutableListOf())
repeat(threadCount) {
executor.submit {
try {
repeat(executionsPerThread) { iteration ->
val order = createTestOrder("LOAD-${Thread.currentThread().id}-$iteration")
val result = agentPlatform.run(OrderProcessingAgent::class, order)
results.add(result)
}
} catch (e: Exception) {
errors.add(e)
} finally {
latch.countDown()
}
}
}
// Wait for all threads to complete
assertThat(latch.await(60, TimeUnit.SECONDS)).isTrue()
// Verify results
assertThat(errors).isEmpty()
assertThat(results).hasSize(threadCount * executionsPerThread)
assertThat(results).allMatch { it.status == OrderStatus.COMPLETED }
}
```
## Test Data Management
### Test Data Builders
```kotlin
class OrderTestDataBuilder {
companion object {
fun standardOrder(): CustomerOrder {
return CustomerOrder(
orderId = "ORD-STD-${UUID.randomUUID()}",
customerId = "CUST-STANDARD",
items = listOf(
OrderItem("LAPTOP-001", "Business Laptop", Money(899.99), 1)
),
shippingAddress = standardAddress(),
paymentMethod = validCreditCard()
)
}
fun vipOrder(): CustomerOrder {
return CustomerOrder(
orderId = "ORD-VIP-${UUID.randomUUID()}",
customerId = "VIP-CUSTOMER-001",
items = listOf(
OrderItem("LUXURY-001", "Premium Product", Money(2500.0), 1)
),
shippingAddress = vipAddress(),
paymentMethod = premiumCreditCard()
)
}
fun backorderOrder(): CustomerOrder {
return CustomerOrder(
orderId = "ORD-BACK-${UUID.randomUUID()}",
customerId = "CUST-PATIENT",
items = listOf(
OrderItem("RARE-001", "Limited Edition Item", Money(150.0), 1)
),
shippingAddress = standardAddress(),
paymentMethod = validCreditCard()
)
}
private fun standardAddress() = Address(
street = "123 Test Street",
city = "Test City",
state = "TC",
zipCode = "12345"
)
private fun vipAddress() = Address(
street = "1 Executive Plaza",
city = "Premium City",
state = "PC",
zipCode = "99999"
)
private fun validCreditCard() = CreditCard(
number = "4111-1111-1111-1111",
expiryDate = LocalDate.now().plusYears(2)
)
private fun premiumCreditCard() = CreditCard(
number = "5555-5555-5555-4444",
expiryDate = LocalDate.now().plusYears(3)
)
}
}
```
### Test Fixtures and Scenarios
```kotlin
@TestConfiguration
class TestFixtureConfiguration {
@Bean
@Primary
fun testInventoryService(): InventoryService {
return object : InventoryService {
override fun getAvailableQuantity(productId: String): Int {
return when (productId) {
"LAPTOP-001" -> 25
"LUXURY-001" -> 5
"RARE-001" -> 0 // Out of stock
else -> 10
}
}
override fun getExpectedRestockDate(productId: String): LocalDate? {
return if (getAvailableQuantity(productId) == 0) {
LocalDate.now().plusWeeks(2)
} else null
}
}
}
@Bean
@Primary
fun testPaymentService(): PaymentService {
return object : PaymentService {
override fun processPayment(paymentMethod: PaymentMethod, amount: Money): PaymentResult {
return when {
amount.amount > 10000.0 -> PaymentResult(
success = false,
transactionId = null,
errorMessage = "Amount exceeds limit"
)
paymentMethod.isExpired() -> PaymentResult(
success = false,
transactionId = null,
errorMessage = "Payment method expired"
)
else -> PaymentResult(
success = true,
transactionId = "TXN-TEST-${UUID.randomUUID()}",
errorMessage = null
)
}
}
}
}
}
```
## Testing Best Practices
### 1. Test Organization
```kotlin
// Use nested test classes for different scenarios
@DisplayName("Order Processing Agent")
class OrderProcessingAgentTest {
@Nested
@DisplayName("Order Validation")
inner class OrderValidationTests {
// Validation-specific tests
}
@Nested
@DisplayName("Inventory Management")
inner class InventoryTests {
// Inventory-specific tests
}
@Nested
@DisplayName("Payment Processing")
inner class PaymentTests {
// Payment-specific tests
}
}
```
### 2. Assertion Patterns
```kotlin
// Use domain-specific assertions
fun assertThat(processedOrder: ProcessedOrder): ProcessedOrderAssert {
return ProcessedOrderAssert(processedOrder)
}
class ProcessedOrderAssert(private val actual: ProcessedOrder) : AbstractAssert(actual, ProcessedOrderAssert::class.java) {
fun isCompleted(): ProcessedOrderAssert {
if (actual.status != OrderStatus.COMPLETED) {
failWithMessage("Expected order to be completed but was <%s>", actual.status)
}
return this
}
fun hasTrackingNumber(): ProcessedOrderAssert {
if (actual.trackingNumber.isNullOrBlank()) {
failWithMessage("Expected order to have tracking number but was null or blank")
}
return this
}
fun hasEstimatedDeliveryAfter(date: LocalDate): ProcessedOrderAssert {
if (actual.estimatedDelivery == null || !actual.estimatedDelivery!!.isAfter(date)) {
failWithMessage("Expected delivery date after <%s> but was <%s>", date, actual.estimatedDelivery)
}
return this
}
}
// Usage:
assertThat(result)
.isCompleted()
.hasTrackingNumber()
.hasEstimatedDeliveryAfter(LocalDate.now())
```
### 3. Test Documentation
```kotlin
@Test
@DisplayName("Should process VIP orders with expedited shipping when customer has VIP status")
fun `should process VIP orders with expedited shipping`() {
// Given: A VIP customer with a high-value order
val vipOrder = OrderTestDataBuilder.vipOrder()
// When: The order is processed through the agent
val result = agentPlatform.run(OrderProcessingAgent::class, vipOrder)
// Then: The order should be completed with VIP treatment
assertThat(result)
.isCompleted()
.hasEstimatedDeliveryAfter(LocalDate.now())
.hasCustomerMessage(containing("VIP"))
}
```
### 4. Test Isolation
```kotlin
@TestMethodOrder(OrderAnnotation::class)
class IsolatedAgentTest {
@Test
@Order(1)
fun `test should not affect other tests`() {
// Each test should be completely independent
// Use @DirtiesContext if needed for Spring context
}
@BeforeEach
fun resetState() {
// Reset any shared state
TestDataManager.clearAll()
FakeOperationContext.resetGlobalState()
}
}
```
## Continuous Integration Testing
### CI Pipeline Configuration
```yaml
# .github/workflows/test.yml
name: Agent Testing Pipeline
on: [push, pull_request]
jobs:
unit-tests:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-java@v3
with:
java-version: '21'
- run: mvn test -Dtest=**/*Test
integration-tests:
runs-on: ubuntu-latest
services:
postgres:
image: postgres:13
env:
POSTGRES_PASSWORD: test
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
steps:
- uses: actions/checkout@v3
- uses: actions/setup-java@v3
with:
java-version: '21'
- run: mvn test -Dtest=**/*IntegrationTest
env:
SPRING_DATASOURCE_URL: jdbc:postgresql://localhost/test
SPRING_DATASOURCE_USERNAME: postgres
SPRING_DATASOURCE_PASSWORD: test
e2e-tests:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-java@v3
with:
java-version: '21'
- run: mvn test -Dtest=**/*E2ETest
env:
OPENAI_API_KEY: ${{ secrets.TEST_OPENAI_API_KEY }}
```
This comprehensive testing guide ensures your Embabel agents are robust, reliable, and maintainable across all scenarios from simple unit tests to complex end-to-end business workflows.