Install any skill in seconds. Free to start, no credit card required.
Get Started Free →Provides unit test, integration test, and mock AI patterns for LangChain4j applications. Creates mock LLM responses, tests retrieval chains, validates RAG workflows, and implements Testcontainers-based integration tests for Java AI services. Use when unit testing AI services, integration testing LangChain4j components, mocking AI models, or testing LLM-based Java applications.
.claude/skills/giuseppe-trisciuoglio-langchain4j-testing-strategies/SKILL.md| Test case | Without → With | Effect | Δ tokens | Δ turns |
|---|---|---|---|---|
| case-03 | ✗→✓ | ▲ Improved | 16% | 0% |
| case-04 | ✗→✓ | ▲ Improved | 51% | 0% |
| case-05 | ✗→✓ | ▲ Improved | 28% | 0% |
| case-13 | ✗→✓ | ▲ Improved | 80% | 0% |
| case-02 | ✓→✓ | = Same ✓ | 68% | 0% |
Patterns for unit testing with mocks, integration testing with Testcontainers, and end-to-end validation of RAG systems, AI Services, and tool execution.
Use mock models for fast, isolated testing. See references/unit-testing.md.
javaChatModel mockModel = mock(ChatModel.class); when(mockModel.generate(any(String.class))) .thenReturn(Response.from(AiMessage.from("Mocked response"))); var service = AiServices.builder(AiService.class) .chatModel(mockModel) .build();
Setup Maven/Gradle dependencies. See references/testing-dependencies.md.
langchain4j-test - Guardrail assertionstestcontainers - Containerized testingmockito - Mock external dependenciesassertj - Fluent assertionsTest with real services. See references/integration-testing.md.
java@Testcontainers class OllamaIntegrationTest { @Container static GenericContainer<?> ollama = new GenericContainer<>( DockerImageName.parse("ollama/ollama:0.5.4") ).withExposedPorts(11434); @Test void shouldGenerateResponse() { // Verify container is healthy assertTrue(ollama.isRunning()); await().atMost(30, TimeUnit.SECONDS) .until(() -> ollama.getLogs().contains("API server listening")); ChatModel model = OllamaChatModel.builder() .baseUrl(ollama.getEndpoint()) .build(); // Verify model responds before running tests assertDoesNotThrow(() -> model.generate("ping")); String response = model.generate("Test query"); assertNotNull(response); } }
Streaming, memory, error handling patterns in references/advanced-testing.md.
Follow the testing pyramid from references/workflow-patterns.md:
70% Unit Tests ─ Mock ChatModel, guardrails, edge cases
20% Integration Tests ─ Testcontainers, vector stores, RAG
10% End-to-End Tests ─ Complete user journeys@Timeout duration for slow models, check container resource limitsjava@Test void shouldProcessQueryWithMock() { ChatModel mockModel = mock(ChatModel.class); when(mockModel.generate(any(String.class))) .thenReturn(Response.from(AiMessage.from("Test response"))); var service = AiServices.builder(AiService.class) .chatModel(mockModel) .build(); String result = service.chat("What is Java?"); assertEquals("Test response", result); }
java@Testcontainers class RAGIntegrationTest { @Container static GenericContainer<?> ollama = new GenericContainer<>( DockerImageName.parse("ollama/ollama:0.5.4") ); @BeforeAll static void waitForContainerReady() { await().atMost(60, TimeUnit.SECONDS) .until(() -> ollama.getLogs().contains("API server listening")); } @Test void shouldCompleteRAGWorkflow() { assertTrue(ollama.isRunning()); var chatModel = OllamaChatModel.builder() .baseUrl(ollama.getEndpoint()) .build(); var embeddingModel = OllamaEmbeddingModel.builder() .baseUrl(ollama.getEndpoint()) .build(); var store = new InMemoryEmbeddingStore<>(); var retriever = EmbeddingStoreContentRetriever.builder() .chatModel(chatModel) .embeddingStore(store) .embeddingModel(embeddingModel) .build(); var assistant = AiServices.builder(RagAssistant.class) .chatLanguageModel(chatModel) .contentRetriever(retriever) .build(); String response = assistant.chat("What is Spring Boot?"); assertNotNull(response); assertTrue(response.contains("Spring")); } }
@BeforeEach/@AfterEach for test isolation@Timeout for external service callsjavaChatModel mockModel = mock(ChatModel.class); when(mockModel.generate(anyString())).thenReturn(Response.from(AiMessage.from("Mocked"))); when(mockModel.generate(eq("Hello"))).thenReturn(Response.from(AiMessage.from("Hi"))); when(mockModel.generate(contains("Java"))).thenReturn(Response.from(AiMessage.from("Java")));
javaassertThat(response).isNotNull().isNotEmpty(); assertThat(response).containsAll(expectedKeywords); assertThat(response).doesNotContain("error");
| Case | Status | Duration (ms) | Turns | Tokens | Tool calls | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Without | With | Δ | Without | With | Δ | Without | With | Δ | Without | With | Δ | ||
case-01 | fail→fail | 20,464 | 13,635 | -33% | 1 | 1 | 0% | 3,483 | 4,043 | +16% | 0 | 0 | — |
case-02 | pass→pass | 16,219 | 15,424 | -5% | 1 | 1 | 0% | 2,592 | 4,365 | +68% | 0 | 0 | — |
case-03 | fail→pass | 12,479 | 5,333 | -57% | 1 | 1 | 0% | 2,179 | 2,522 | +16% | 0 | 0 | — |
case-04 | fail→pass | 11,686 | 7,980 | -32% | 1 | 1 | 0% | 2,099 | 3,167 | +51% | 0 | 0 | — |
case-05 | fail→pass | 10,902 | 4,805 | -56% | 1 | 1 | 0% | 1,881 | 2,412 | +28% | 0 | 0 | — |
case-06 | pass→pass | 8,180 | 7,357 | -10% | 1 | 1 | 0% | 1,559 | 2,986 | +92% | 0 | 0 | — |
case-07 | pass→pass | 10,569 | 8,342 | -21% | 1 | 1 | 0% | 1,968 | 3,190 | +62% | 0 | 0 | — |
case-08 | pass→pass | 14,211 | 4,702 | -67% | 1 | 1 | 0% | 2,519 | 2,499 | -1% | 0 | 0 | — |
case-09 | pass→pass | 13,700 | 9,964 | -27% | 1 | 1 | 0% | 2,363 | 3,267 | +38% | 0 | 0 | — |
case-10 | pass→pass | 8,658 | 7,380 | -15% | 1 | 1 | 0% | 1,214 | 2,712 | +123% | 0 | 0 | — |
case-11 | pass→pass | 9,805 | 6,617 | -33% | 1 | 1 | 0% | 1,621 | 2,734 | +69% | 0 | 0 | — |
case-12 | pass→pass | 14,017 | 9,136 | -35% | 1 | 1 | 0% | 1,739 | 3,311 | +90% | 0 | 0 | — |
case-13 | fail→pass | 7,708 | 5,031 | -35% | 1 | 1 | 0% | 1,408 | 2,535 | +80% | 0 | 0 | — |
case-14 | pass→pass | 8,725 | 5,697 | -35% | 1 | 1 | 0% | 1,533 | 2,589 | +69% | 0 | 0 | — |
case-15 | pass→pass | 4,522 | 1,977 | -56% | 1 | 1 | 0% | 637 | 1,933 | +203% | 0 | 0 | — |
case-16 | pass→pass | 16,360 | 15,942 | -3% | 1 | 1 | 0% | 3,476 | 4,812 | +38% | 0 | 0 | — |
case-17 | pass→pass | 3,996 | 4,772 | +19% | 1 | 1 | 0% | 603 | 2,385 | +296% | 0 | 0 | — |
case-18 | pass→pass | 18,070 | 16,007 | -11% | 1 | 1 | 0% | 2,784 | 4,261 | +53% | 0 | 0 | — |
case-19 | pass→pass | 12,255 | 8,818 | -28% | 1 | 1 | 0% | 1,887 | 2,994 | +59% | 0 | 0 | — |
case-20 | pass→pass | 5,569 | 5,404 | -3% | 1 | 1 | 0% | 920 | 2,456 | +167% | 0 | 0 | — |
case-21 | pass→pass | 14,287 | 29,385 | +106% | 1 | 1 | 0% | 2,661 | 5,043 | +90% | 0 | 0 | — |
case-22 | pass→pass | 17,491 | 18,988 | +9% | 1 | 1 | 0% | 3,183 | 5,141 | +62% | 0 | 0 | — |
case-23 | pass→pass | 20,300 | 17,865 | -12% | 1 | 1 | 0% | 3,404 | 4,134 | +21% | 0 | 0 | — |
DecimalAI ran this skill against gemini-3.6-flash twice over the same eval suite — once with the skill loaded and once without — and compared the two runs case by case. 23 cases were attempted. The headline lift of +17 percentage points is the difference between those two pass rates over the 23 comparable cases.
Without the skill loaded, the model failed this case. With it loaded, the same prompt on the same model passed. This is one improved case from the latest verified run; every case, including any that regressed, is in the table above.
Other measured skills in the registry, with their headline benchmark lift.