|
| 1 | +# LangChain4J VCR Demo |
| 2 | + |
| 3 | +This demo shows how to use the VCR (Video Cassette Recorder) test system with LangChain4J models. VCR records LLM/embedding API responses to Redis and replays them in subsequent test runs, enabling fast, deterministic, and cost-effective testing. |
| 4 | + |
| 5 | +## Features |
| 6 | + |
| 7 | +- Record and replay LangChain4J `EmbeddingModel` responses |
| 8 | +- Record and replay LangChain4J `ChatLanguageModel` responses |
| 9 | +- Declarative `@VCRTest` and `@VCRModel` annotations |
| 10 | +- Automatic model wrapping via JUnit 5 extension |
| 11 | +- Redis-backed persistence with automatic test isolation |
| 12 | + |
| 13 | +## Quick Start |
| 14 | + |
| 15 | +### 1. Annotate Your Test Class |
| 16 | + |
| 17 | +```java |
| 18 | +import com.redis.vl.test.vcr.VCRMode; |
| 19 | +import com.redis.vl.test.vcr.VCRModel; |
| 20 | +import com.redis.vl.test.vcr.VCRTest; |
| 21 | + |
| 22 | +@VCRTest(mode = VCRMode.PLAYBACK_OR_RECORD) |
| 23 | +class MyLangChain4JTest { |
| 24 | + |
| 25 | + @VCRModel(modelName = "text-embedding-3-small") |
| 26 | + private EmbeddingModel embeddingModel = createEmbeddingModel(); |
| 27 | + |
| 28 | + @VCRModel |
| 29 | + private ChatLanguageModel chatModel = createChatModel(); |
| 30 | + |
| 31 | + // Models must be initialized at field declaration time, |
| 32 | + // not in @BeforeEach (VCR wrapping happens before @BeforeEach) |
| 33 | +} |
| 34 | +``` |
| 35 | + |
| 36 | +### 2. Use Models Normally |
| 37 | + |
| 38 | +```java |
| 39 | +@Test |
| 40 | +void shouldEmbedText() { |
| 41 | + // First run: calls real API and records response |
| 42 | + // Subsequent runs: replays from Redis cassette |
| 43 | + Response<Embedding> response = embeddingModel.embed("What is Redis?"); |
| 44 | + |
| 45 | + assertNotNull(response.content()); |
| 46 | +} |
| 47 | + |
| 48 | +@Test |
| 49 | +void shouldGenerateResponse() { |
| 50 | + String response = chatModel.generate("Explain Redis in one sentence."); |
| 51 | + |
| 52 | + assertNotNull(response); |
| 53 | +} |
| 54 | +``` |
| 55 | + |
| 56 | +## VCR Modes |
| 57 | + |
| 58 | +| Mode | Description | API Key Required | |
| 59 | +|------|-------------|------------------| |
| 60 | +| `PLAYBACK` | Only use recorded cassettes. Fails if cassette missing. | No | |
| 61 | +| `PLAYBACK_OR_RECORD` | Use cassette if available, record if not. | Only for first run | |
| 62 | +| `RECORD` | Always call real API and record response. | Yes | |
| 63 | +| `OFF` | Bypass VCR, always call real API. | Yes | |
| 64 | + |
| 65 | +### Setting Mode via Environment Variable |
| 66 | + |
| 67 | +Override the annotation mode at runtime without changing code: |
| 68 | + |
| 69 | +```bash |
| 70 | +# Record new cassettes |
| 71 | +VCR_MODE=RECORD ./gradlew :demos:langchain4j-vcr:test |
| 72 | + |
| 73 | +# Playback only (CI/CD, no API key needed) |
| 74 | +VCR_MODE=PLAYBACK ./gradlew :demos:langchain4j-vcr:test |
| 75 | + |
| 76 | +# Default behavior from annotation |
| 77 | +./gradlew :demos:langchain4j-vcr:test |
| 78 | +``` |
| 79 | + |
| 80 | +## Running the Demo |
| 81 | + |
| 82 | +### With Pre-recorded Cassettes (No API Key) |
| 83 | + |
| 84 | +The demo includes pre-recorded cassettes in `src/test/resources/vcr-data/`. Run tests without an API key: |
| 85 | + |
| 86 | +```bash |
| 87 | +./gradlew :demos:langchain4j-vcr:test |
| 88 | +``` |
| 89 | + |
| 90 | +### Recording New Cassettes |
| 91 | + |
| 92 | +To record fresh cassettes, set your OpenAI API key: |
| 93 | + |
| 94 | +```bash |
| 95 | +OPENAI_API_KEY=your-key VCR_MODE=RECORD ./gradlew :demos:langchain4j-vcr:test |
| 96 | +``` |
| 97 | + |
| 98 | +## How It Works |
| 99 | + |
| 100 | +1. **Test Setup**: `@VCRTest` annotation triggers the VCR JUnit 5 extension |
| 101 | +2. **Container Start**: A Redis Stack container is started with persistence enabled |
| 102 | +3. **Model Wrapping**: Fields annotated with `@VCRModel` are wrapped with VCR proxies |
| 103 | +4. **Recording**: When a model is called, VCR checks for existing cassette: |
| 104 | + - **Cache hit**: Returns recorded response |
| 105 | + - **Cache miss**: Calls real API, stores response as cassette |
| 106 | +5. **Persistence**: Cassettes are saved to `vcr-data/` directory via Redis persistence |
| 107 | +6. **Cleanup**: Container stops, data persists for next run |
| 108 | + |
| 109 | +## Cassette Storage |
| 110 | + |
| 111 | +Cassettes are stored in Redis JSON format with keys like: |
| 112 | + |
| 113 | +``` |
| 114 | +vcr:embedding:MyTest.testMethod:0001 |
| 115 | +vcr:chat:MyTest.testMethod:0001 |
| 116 | +``` |
| 117 | + |
| 118 | +Data persists to `src/test/resources/vcr-data/` via Redis AOF/RDB. |
| 119 | + |
| 120 | +## Test Structure |
| 121 | + |
| 122 | +``` |
| 123 | +demos/langchain4j-vcr/ |
| 124 | +├── src/test/java/ |
| 125 | +│ └── com/redis/vl/demo/vcr/ |
| 126 | +│ └── LangChain4JVCRDemoTest.java |
| 127 | +└── src/test/resources/ |
| 128 | + └── vcr-data/ # Persisted cassettes |
| 129 | + ├── appendonly.aof |
| 130 | + └── dump.rdb |
| 131 | +``` |
| 132 | + |
| 133 | +## Configuration Options |
| 134 | + |
| 135 | +### @VCRTest Annotation |
| 136 | + |
| 137 | +| Parameter | Default | Description | |
| 138 | +|-----------|---------|-------------| |
| 139 | +| `mode` | `PLAYBACK_OR_RECORD` | VCR operating mode | |
| 140 | +| `dataDir` | `src/test/resources/vcr-data` | Cassette storage directory | |
| 141 | +| `redisImage` | `redis/redis-stack:latest` | Redis Docker image | |
| 142 | + |
| 143 | +### @VCRModel Annotation |
| 144 | + |
| 145 | +| Parameter | Default | Description | |
| 146 | +|-----------|---------|-------------| |
| 147 | +| `modelName` | `""` | Optional model identifier for logging | |
| 148 | + |
| 149 | +## Best Practices |
| 150 | + |
| 151 | +1. **Initialize models at field declaration** - Not in `@BeforeEach` |
| 152 | +2. **Use dummy API key in PLAYBACK mode** - VCR will use cached responses |
| 153 | +3. **Commit cassettes to version control** - Enables reproducible tests |
| 154 | +4. **Use specific test names** - Cassette keys include test class and method names |
| 155 | +5. **Re-record periodically** - API responses may change over time |
| 156 | + |
| 157 | +## Troubleshooting |
| 158 | + |
| 159 | +### Tests fail with "Cassette missing" |
| 160 | + |
| 161 | +- Ensure cassettes exist in `src/test/resources/vcr-data/` |
| 162 | +- Run once with `VCR_MODE=RECORD` and API key to generate cassettes |
| 163 | + |
| 164 | +### API key required error |
| 165 | + |
| 166 | +- In `PLAYBACK` mode, use a dummy key: `"vcr-playback-mode"` |
| 167 | +- VCR won't call the real API when cassettes exist |
| 168 | + |
| 169 | +### Tests pass but call real API |
| 170 | + |
| 171 | +- Verify models are initialized at field declaration, not `@BeforeEach` |
| 172 | +- Check that `@VCRModel` annotation is present on model fields |
| 173 | + |
| 174 | +## See Also |
| 175 | + |
| 176 | +- [Spring AI VCR Demo](../spring-ai-vcr/README.md) |
| 177 | +- [VCR Test System Documentation](../../README.md#-experimental-vcr-test-system) |
0 commit comments