13 KiB
13 KiB
Workflow Patterns and Best Practices
Test Pyramid Strategy
Unit Tests (70%)
// Fast, isolated tests focusing on business logic
@Test
void shouldValidateUserInput() {
InputGuardrail guardrail = new InputGuardrail();
UserMessage message = UserMessage.from("Legitimate query");
GuardrailResult result = guardrail.validate(message);
assertThat(result).isSuccessful();
}
@Test
void shouldDetectInvalidInput() {
InputGuardrail guardrail = new InputGuardrail();
UserMessage message = UserMessage.from(""); // Empty input
GuardrailResult result = guardrail.validate(message);
assertThat(result).hasFailures();
}
Integration Tests (20%)
@Testcontainers
class AiServiceIntegrationTest {
@Container
static OllamaContainer ollama = new OllamaContainer("ollama/ollama:latest");
@Test
void shouldProcessEndToEndRequest() {
ChatModel model = OllamaChatModel.builder()
.baseUrl(ollama.getEndpoint())
.modelName("llama2")
.timeout(Duration.ofSeconds(10))
.build();
var assistant = AiServices.builder(Assistant.class)
.chatModel(model)
.build();
String response = assistant.chat("Test query");
assertNotNull(response);
assertFalse(response.trim().isEmpty());
}
}
End-to-End Tests (10%)
@Test
@DisplayName("Complete AI workflow test")
void shouldCompleteFullWorkflow() {
// Test complete user journey
// Includes all components, real models, and external services
// Arrange
var userQuery = "What is the weather today?";
var service = new CompleteAIService();
// Act
var result = service.processCompleteQuery(userQuery);
// Assert
assertNotNull(result);
assertTrue(result.isSuccess());
assertNotNull(result.getAnswer());
// Verify all components were used
verify(weatherService, atLeastOnce()).getWeather();
verify(guardrail, atLeastOnce()).validate(any());
}
Mock vs Real Model Strategy
When to Use Mock Models
// Fast unit tests (< 50ms)
@Test
void shouldProcessSimpleQueryFast() {
ChatModel mockModel = mock(ChatModel.class);
when(mockModel.generate(anyString()))
.thenReturn(Response.from(AiMessage.from("Mocked response")));
var service = AiServices.builder(AiService.class)
.chatModel(mockModel)
.build();
String response = service.chat("What is Java?");
// Fast assertions
assertEquals("Mocked response", response);
}
// Business logic validation
@Test
void shouldApplyBusinessRules() {
var guardrail = new BusinessRuleGuardrail();
String result = guardrail.validateBusinessLogic("Test input");
assertBusinessRulesApplied(result);
}
// Edge case testing
@Test
void shouldHandleEdgeCases() {
var service = createTestService();
// Test edge cases
String emptyResponse = service.chat("");
String longResponse = service.chat("a".repeat(10000));
verifyEdgeCaseHandling(emptyResponse, longResponse);
}
When to Use Real Models
// Integration tests with real model
@Testcontainers
void shouldIntegrateWithRealModel() {
@Container
OllamaContainer ollama = new OllamaContainer();
ChatModel model = OllamaChatModel.builder()
.baseUrl(ollama.getEndpoint())
.modelName("llama2")
.build();
// Test with real model behavior
String response = model.generate("What is Java?");
// Verify model-specific behavior
assertTrue(response.toLowerCase().contains("programming"));
assertTrue(response.toLowerCase().contains("java"));
}
// Model-specific behavior validation
@Test
void shouldValidateModelSpecificBehavior() {
var model = OpenAiChatModel.builder()
.apiKey(testApiKey)
.modelName("gpt-4")
.build();
// Test model-specific patterns
String response = model.generate("List 3 numbers");
// Verify specific model behavior
assertTrue(response.matches(".*\\d+.*")); // Contains numbers
}
// Performance benchmarking
@Test
@Timeout(10)
void shouldBenchmarkPerformance() {
var model = OpenAiChatModel.builder()
.apiKey(testApiKey)
.modelName("gpt-3.5-turbo")
.build();
Instant start = Instant.now();
String response = model.generate("Complex query");
Duration duration = Duration.between(start, Instant.now());
// Performance assertions
assertTrue(duration.toSeconds() < 5);
assertTrue(response.length() > 100);
}
Test Data Management
Test Fixtures
class TestDataFixtures {
public static final String SAMPLE_QUERY = "What is Java?";
public static final String SAMPLE_RESPONSE = "Java is a programming language...";
public static final Document DOCUMENT_1 = Document.from(
"Spring Boot is a Java framework for building microservices"
);
public static final Document DOCUMENT_2 = Document.from(
"Maven is a build automation tool for Java projects"
);
public static UserMessage createTestMessage(String content) {
return UserMessage.from(content);
}
public static AiMessage createAiMessage(String content) {
return AiMessage.from(content);
}
public static List<Document> createSampleDocuments() {
return List.of(DOCUMENT_1, DOCUMENT_2);
}
public static Embedding createTestEmbedding() {
float[] vector = new float[1536];
Arrays.fill(vector, 0.1f);
return new Embedding(vector);
}
}
// Usage
class MyTest {
@Test
void useTestDataFixtures() {
var message = TestDataFixtures.createTestMessage("Hello");
var documents = TestDataFixtures.createSampleDocuments();
// Test with fixtures
var service = new AIService();
var response = service.process(message, documents);
// Verify
assertNotNull(response);
}
}
Configuration Management
@TestPropertySource(properties = {
"langchain4j.openai.api-key=test-key",
"langchain4j.ollama.base-url=http://localhost:11434",
"app.test.mode=true"
})
class ConfigurationTest {
@Autowired
private TestConfig config;
@Test
void shouldUseTestConfiguration() {
// Uses application-test.properties
// Ensures test isolation
assertEquals("test-key", config.getOpenaiApiKey());
assertEquals("http://localhost:11434", config.getOllamaBaseUrl());
}
}
// Configuration class
@Configuration
@ConfigurationProperties(prefix = "langchain4j")
class TestConfig {
private String openaiApiKey;
private String ollamaBaseUrl;
// Getters and setters
public String getOpenaiApiKey() { return openaiApiKey; }
public void setOpenaiApiKey(String key) { this.openaiApiKey = key; }
public String getOllamaBaseUrl() { return ollamaBaseUrl; }
public void setOllamaBaseUrl(String url) { this.ollamaBaseUrl = url; }
}
Test Data Cleanup
class DataCleanupTest {
@BeforeEach
void setupTestData() {
// Setup test data
prepareTestDatabase();
}
@AfterEach
void cleanupTestData() {
// Clean up test data
cleanupDatabase();
}
@Test
void shouldMaintainDataIsolation() {
// Act
createTestData();
// Assert
assertTestDataExists();
}
private void prepareTestDatabase() {
// Setup test database schema and initial data
}
private void cleanupDatabase() {
// Clean up test data
}
private void createTestData() {
// Create test data for specific test
}
private void assertTestDataExists() {
// Verify test data
}
}
Test Organization Patterns
Package Structure
src/test/java/com/example/ai/
├── service/
│ ├── unit/
│ │ ├── ChatServiceUnitTest.java
│ │ ├── GuardrailServiceUnitTest.java
│ │ └── ToolServiceUnitTest.java
│ ├── integration/
│ │ ├── OllamaIntegrationTest.java
│ │ ├── VectorStoreIntegrationTest.java
│ │ └── RagSystemIntegrationTest.java
│ └── e2e/
│ ├── CompleteWorkflowTest.java
│ ├── PerformanceTest.java
│ └── LoadTest.java
├── fixture/
│ ├── AiTestFixtures.java
│ ├── TestDataFactory.java
│ └── MockConfig.java
└── utils/
├── TestAssertions.java
├── PerformanceMetrics.java
└── TestDataBuilder.java
Test Naming Conventions
// Unit tests
@Test
void shouldProcessSimpleQuery() { }
@Test
void shouldValidateInputFormat() { }
@Test
void shouldHandleEmptyInput() { }
// Integration tests
@Testcontainers
@DisplayName("Ollama Integration")
class OllamaIntegrationTest {
@Test
void shouldGenerateResponse() { }
@Test
void shouldHandleLargeQueries() { }
}
// Edge case tests
@Test
@DisplayName("Edge Cases")
class EdgeCaseTest {
@Test
void shouldHandleVeryLongInput() { }
@Test
void shouldHandleSpecialCharacters() { }
@Test
void shouldHandleNullInput() { }
}
// Performance tests
@Test
@DisplayName("Performance")
class PerformanceTest {
@Test
@Timeout(5)
void shouldRespondWithinTimeLimit() { }
@Test
void shouldMeasureTokenUsage() { }
}
Test Grouping
@Tag("unit")
@Tag("service")
class UnitTestGroup { }
@Tag("integration")
@Tag("ollama")
class IntegrationTestGroup { }
@Tag("performance")
@Tag("e2e")
class PerformanceTestGroup { }
// Running specific test groups
mvn test -Dgroups="unit,service" // Run unit service tests
mvn test -Dgroups="integration" // Run all integration tests
mvn test -Dgroups="performance" // Run performance tests
Assertion Best Practices
Clear Assertions
// Good
assertEquals(5, result, "Addition should return 5");
// Better with AssertJ
assertThat(result)
.as("Sum of 2+3")
.isEqualTo(5);
// Even better - domain-specific
assertThat(result)
.as("Calculation result")
.isCorrectAnswer(5); // Custom assertion
Multiple Assertions
// Use assertAll for better error messages
assertAll(
() -> assertNotNull(response),
() -> assertTrue(response.contains("data")),
() -> assertTrue(response.length() > 0)
);
// With AssertJ
assertThat(response)
.isNotNull()
.contains("data")
.hasSizeGreaterThan(0);
Assertion Helpers
class AiTestAssertions {
static void assertValidResponse(String response) {
assertThat(response)
.isNotNull()
.isNotEmpty()
.doesNotContain("error");
}
static void assertResponseContainsKeywords(String response, String... keywords) {
assertThat(response).containsAll(List.of(keywords));
}
static void assertResponseFormat(String response, ResponseFormat expectedFormat) {
assertThat(response).matches(expectedFormat.getPattern());
}
static void assertResponseQuality(String response, String query) {
assertThat(response)
.isNotNull()
.hasLengthGreaterThan(10)
.doesNotContain("error")
.containsAnyOf(query.split(" "));
}
}
// Usage
@Test
void testResponseQuality() {
String response = assistant.chat("What is AI?");
AiTestAssertions.assertResponseQuality(response, "What is AI?");
}
Test Isolation Techniques
Mock Spy for Partial Mocking
@Test
void testSpyPartialMocking() {
Calculator real = new Calculator();
Calculator spy = spy(real);
// Mock specific method
doReturn(10).when(spy).add(5, 5);
// Real implementation for other methods
int sum = spy.add(3, 4); // Returns 7 (real implementation)
int special = spy.add(5, 5); // Returns 10 (mocked)
}
Test Double Setup
class TestDoubleSetup {
private ChatModel mockModel;
private EmbeddingStore mockStore;
private AiService service;
@BeforeEach
void setupTestDoubles() {
// Setup mocks
mockModel = mock(ChatModel.class);
mockStore = mock(EmbeddingStore.class);
// Setup behavior
when(mockModel.generate(anyString()))
.thenReturn(Response.from(AiMessage.from("Test response")));
// Create service
service = AiServices.builder(AiService.class)
.chatModel(mockModel)
.build();
}
@AfterEach
void verifyInteractions() {
// Verify key interactions
verify(mockModel, atLeastOnce()).generate(anyString());
verifyNoMoreInteractions(mockModel);
}
}
Resetting Mocks
class MockResetTest {
private ChatModel mockModel;
@BeforeEach
void setup() {
mockModel = mock(ChatModel.class);
// Setup initial behavior
when(mockModel.generate("hello")).thenReturn("Hi");
}
@AfterEach
void cleanup() {
reset(mockModel); // Clear all stubbing
}
@Test
void firstTest() {
// Use mock
}
@Test
void secondTest() {
// Fresh mock state due to reset
when(mockModel.generate("hello")).thenReturn("Hello");
}
}