Skip to content

Testing Guide

alexheifetz edited this page Sep 22, 2025 · 1 revision

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

<dependencies>
    <!-- Main Embabel testing support -->
    <dependency>
        <groupId>com.embabel.agent</groupId>
        <artifactId>embabel-agent-test-support</artifactId>
        <version>${embabel-agent.version}</version>
        <scope>test</scope>
    </dependency>
    
    <!-- Spring Boot testing -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-test</artifactId>
        <scope>test</scope>
    </dependency>
    
    <!-- Mockito for mocking -->
    <dependency>
        <groupId>org.mockito</groupId>
        <artifactId>mockito-kotlin</artifactId>
        <scope>test</scope>
    </dependency>
    
    <!-- TestContainers for integration testing -->
    <dependency>
        <groupId>org.testcontainers</groupId>
        <artifactId>junit-jupiter</artifactId>
        <scope>test</scope>
    </dependency>
</dependencies>

Test Configuration

# 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

@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

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

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

@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

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<ProcessedOrder>()
        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

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<ProcessedOrder>()
    }
    
    @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<ProcessedOrder>()
    }
    
    @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<ComprehensivePlan>()
    }
}

Testing Conditions and Business Rules

Custom Condition Testing

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

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

@SpringBootTest
@Testcontainers
class OrderProcessingE2ETest {
    
    @Container
    static val postgres = PostgreSQLContainer<Nothing>("postgres:13").apply {
        withDatabaseName("embabel_test")
        withUsername("test")
        withPassword("test")
    }
    
    @Container
    static val redis = GenericContainer<Nothing>("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<ProcessedOrder>()
        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

@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

@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<ProcessedOrder>())
    val errors = Collections.synchronizedList(mutableListOf<Exception>())
    
    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

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

@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

// 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

// Use domain-specific assertions
fun assertThat(processedOrder: ProcessedOrder): ProcessedOrderAssert {
    return ProcessedOrderAssert(processedOrder)
}

class ProcessedOrderAssert(private val actual: ProcessedOrder) : AbstractAssert<ProcessedOrderAssert, ProcessedOrder>(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

@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

@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

# .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.

Clone this wiki locally