Multi-module test support framework for Embabel Agent applications providing integration testing, mock AI services, and test configuration utilities
Step-by-step guide for configuring Spring Boot tests with fake AI services.
FakeAiConfiguration is a Spring @TestConfiguration that provides pre-configured fake LLM and embedding service beans. It allows you to test Spring components without requiring API keys or making real API calls.
@SpringBootTest
@Import(FakeAiConfiguration::class) // Add this
class MyTest {
// Tests go here
}@Autowired
private lateinit var cheapest: LlmService<*>
@Autowired
private lateinit var best: LlmService<*>
@Autowired
private lateinit var embeddingService: EmbeddingService@Test
fun `test with fake services`() {
val result = myService.process(input, cheapest)
assertNotNull(result)
}Pre-configured fake LLM service simulating gpt-4o-mini.
@Autowired
@Qualifier("cheapest")
private lateinit var cheapModel: LlmService<*>Or by name:
@Autowired
private lateinit var cheapest: LlmService<*>Configuration:
Pre-configured fake LLM service simulating gpt-4o.
@Autowired
@Qualifier("best")
private lateinit var bestModel: LlmService<*>Or by name:
@Autowired
private lateinit var best: LlmService<*>Configuration:
Pre-configured fake embedding service with 1536 dimensions.
@Autowired
private lateinit var embeddingService: EmbeddingServiceConfiguration:
import com.embabel.common.test.ai.config.FakeAiConfiguration
import org.springframework.boot.test.context.SpringBootTest
import org.springframework.context.annotation.Import
import org.springframework.beans.factory.annotation.Autowired
import org.junit.jupiter.api.Test
@SpringBootTest
@Import(FakeAiConfiguration::class)
class MyServiceTest {
@Autowired
private lateinit var cheapest: LlmService<*>
@Test
fun `test service with fake LLM`() {
val result = myService.process("input", cheapest)
assertNotNull(result)
// No API calls were made
}
}@SpringBootTest
@Import(FakeAiConfiguration::class)
class ModelTierTest {
@Autowired
@Qualifier("cheapest")
private lateinit var cheapModel: LlmService<*>
@Autowired
@Qualifier("best")
private lateinit var bestModel: LlmService<*>
@Test
fun `feature works with both tiers`() {
// Test with cheap model
val cheapResult = feature.execute(input, cheapModel)
assertNotNull(cheapResult)
// Test with best model
val bestResult = feature.execute(input, bestModel)
assertNotNull(bestResult)
// Both succeed without API costs
}
}@SpringBootTest
@Import(FakeAiConfiguration::class)
class EmbeddingTest {
@Autowired
private lateinit var embeddingService: EmbeddingService
@Test
fun `test with embeddings`() {
val embedding = embeddingService.embed("test text")
assertNotNull(embedding)
assertEquals(1536, embedding.size)
}
}@SpringBootTest
@Import(FakeAiConfiguration::class)
class CompleteTest {
@Autowired
private lateinit var cheapest: LlmService<*>
@Autowired
private lateinit var best: LlmService<*>
@Autowired
private lateinit var embeddingService: EmbeddingService
@Test
fun `test complete workflow`() {
// Use cheap model for simple tasks
val summary = summarizer.summarize(doc, cheapest)
// Use best model for complex tasks
val analysis = analyzer.analyze(data, best)
// Use embeddings for search
val embedding = embeddingService.embed(summary)
assertNotNull(summary)
assertNotNull(analysis)
assertEquals(1536, embedding.size)
}
}import com.embabel.common.test.ai.config.FakeAiConfiguration;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.context.annotation.Import;
import org.springframework.beans.factory.annotation.Autowired;
import org.junit.jupiter.api.Test;
@SpringBootTest
@Import(FakeAiConfiguration.class)
public class MyServiceTest {
@Autowired
private LlmService<?> cheapest;
@Test
void testServiceWithFakeLlm() {
String result = myService.process("input", cheapest);
assertNotNull(result);
}
}@SpringBootTest
@Import(FakeAiConfiguration.class)
public class ModelTierTest {
@Autowired
@Qualifier("cheapest")
private LlmService<?> cheapModel;
@Autowired
@Qualifier("best")
private LlmService<?> bestModel;
@Test
void testBothTiers() {
String cheapResult = feature.execute(input, cheapModel);
String bestResult = feature.execute(input, bestModel);
assertNotNull(cheapResult);
assertNotNull(bestResult);
}
}Replace specific beans with custom implementations:
@SpringBootTest
@Import(FakeAiConfiguration::class)
class CustomBeanTest {
@TestConfiguration
class CustomConfig {
@Bean
@Primary
fun customCheapest(): LlmService<*> {
// Custom mock with specific behavior
val mockService = mockk<LlmService<*>>()
every { mockService.generate(any<String>()) } returns "custom response"
return mockService
}
}
@Autowired
private lateinit var cheapest: LlmService<*>
@Test
fun `test with custom bean`() {
val result = cheapest.generate("test")
assertEquals("custom response", result)
}
}Override embedding service with different dimensions:
@SpringBootTest
class CustomEmbeddingTest {
@TestConfiguration
class TestConfig {
@Bean
@Primary
fun customEmbedding(): EmbeddingService {
val fakeModel = FakeEmbeddingModel(dimensions = 768)
return SpringAiEmbeddingService(
fakeModel,
"custom-model",
"CustomProvider"
)
}
}
@Autowired
private lateinit var embeddingService: EmbeddingService
@Test
fun `test with 768 dimensions`() {
val embedding = embeddingService.embed("test")
assertEquals(768, embedding.size)
}
}Use only specific beans from FakeAiConfiguration:
@SpringBootTest
class PartialConfigTest {
@TestConfiguration
class TestConfig {
@Bean
fun embeddingService(): EmbeddingService {
val fakeModel = FakeEmbeddingModel()
return SpringAiEmbeddingService(
fakeModel,
"test-embedding",
"TestProvider"
)
}
// Don't import FakeAiConfiguration
// Only use custom embedding service
}
@Autowired
private lateinit var embeddingService: EmbeddingService
@Test
fun `test with only embedding service`() {
val embedding = embeddingService.embed("test")
assertNotNull(embedding)
}
}Combine with Mockito integration testing:
@SpringBootTest
@Import(FakeAiConfiguration::class)
class CombinedTest : EmbabelMockitoIntegrationTest() {
@Autowired
private lateinit var embeddingService: EmbeddingService
@Test
fun `test with both stubbing and fake beans`() {
// Stub LLM operations
whenGenerateText { it.contains("test") }
.thenReturn("stubbed response")
// Use fake embedding service
val embedding = embeddingService.embed("test")
// Execute code
val result = myAgent.process("test")
// Verify
verifyGenerateText { it.contains("test") }
assertNotNull(result)
assertEquals(1536, embedding.size)
}
}Use with Spring Boot test slices:
@DataJpaTest
@Import(FakeAiConfiguration::class)
class RepositoryTest {
@Autowired
private lateinit var embeddingService: EmbeddingService
@Autowired
private lateinit var repository: DocumentRepository
@Test
fun `test repository with embeddings`() {
val embedding = embeddingService.embed("test")
val document = Document("test", embedding)
repository.save(document)
val found = repository.findById(document.id)
assertNotNull(found)
}
}Test code works with different model strategies:
@Test
fun `test model strategy selection`() {
// Simple task - use cheap model
val simpleResult = taskExecutor.execute(
simpleTask,
strategy = ModelStrategy.CHEAP
)
// Complex task - use best model
val complexResult = taskExecutor.execute(
complexTask,
strategy = ModelStrategy.BEST
)
assertNotNull(simpleResult)
assertNotNull(complexResult)
}Test fallback from best to cheap:
@Test
fun `test fallback to cheaper model`() {
val processor = SmartProcessor(best, cheapest)
// Configure best to fail
every { best.generate(any()) } throws RuntimeException()
// Should fallback to cheapest
val result = processor.processWithFallback(input)
assertNotNull(result)
}Test cost-optimization logic:
@Test
fun `test cost optimization`() {
val optimizer = CostOptimizer(cheapest, best)
// Small input - should use cheap
val cheapResult = optimizer.optimize(smallInput)
verify(exactly = 1) { cheapest.generate(any()) }
// Large input - should use best
val bestResult = optimizer.optimize(largeInput)
verify(exactly = 1) { best.generate(any()) }
}Problem: Spring cannot find the fake AI beans.
Solution: Ensure @Import(FakeAiConfiguration::class) is present:
@SpringBootTest
@Import(FakeAiConfiguration::class) // Must have this
class MyTest { ... }Problem: Unexpected bean is injected.
Solution: Use qualifiers explicitly:
@Autowired
@Qualifier("cheapest") // Be explicit
private lateinit var cheapModel: LlmService<*>Problem: Multiple configurations conflict.
Solution: Use @Primary for custom beans:
@Bean
@Primary // This takes precedence
fun customService(): LlmService<*> { ... }Problem: Generic type issues with LlmService.
Solution: Use wildcard:
// Correct
private lateinit var llmService: LlmService<*>
// Or specific type if known
private lateinit var llmService: LlmService<OpenAiChatOptions>Always Import Configuration: Don't forget @Import(FakeAiConfiguration::class)
Use Qualifiers When Needed: Be explicit about which bean you want
Override for Custom Behavior: Use @Primary when overriding beans
Test Multiple Tiers: Verify code works with both cheap and best models
Combine with Mockito: Use both fake beans and Mockito stubs together
Keep Tests Fast: Fake services are instant - take advantage of this