572 lines
13 KiB
Markdown
572 lines
13 KiB
Markdown
# Workflow Patterns and Best Practices
|
|
|
|
## Test Pyramid Strategy
|
|
|
|
### Unit Tests (70%)
|
|
|
|
```java
|
|
// 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%)
|
|
|
|
```java
|
|
@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%)
|
|
|
|
```java
|
|
@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
|
|
|
|
```java
|
|
// 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
|
|
|
|
```java
|
|
// 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
|
|
|
|
```java
|
|
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
|
|
|
|
```java
|
|
@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
|
|
|
|
```java
|
|
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
|
|
|
|
```java
|
|
// 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
|
|
|
|
```java
|
|
@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
|
|
|
|
```java
|
|
// 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
|
|
|
|
```java
|
|
// 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
|
|
|
|
```java
|
|
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
|
|
|
|
```java
|
|
@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
|
|
|
|
```java
|
|
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
|
|
|
|
```java
|
|
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");
|
|
}
|
|
}
|
|
``` |