Files
gh-giuseppe-trisciuoglio-de…/skills/langchain4j-testing-strategies/references/workflow-patterns.md
2025-11-29 18:28:34 +08:00

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");
    }
}