VCR Test Harness
The VCR (Video Cassette Recorder) modules provide record/replay infrastructure for AI model API calls. Record responses from embedding and chat models during development, then replay them in CI without network access or API keys.
Concepts
-
Cassette — a stored model response, identified by call type, test class, test method, and call index.
-
Mode — controls VCR behavior:
-
PLAYBACK— serve only a cassette whose request signature matches; fail if missing or stale. -
RECORD— always call the real model and store the response. -
RECORD_NEW— record only if no cassette exists; replay otherwise. -
RECORD_FAILED— re-record only for tests that previously failed. -
PLAYBACK_OR_RECORD— replay a matching cassette; record when it is missing or its request signature is stale. -
OFF— bypass VCR entirely; all calls go to the real model.
-
-
Registry — tracks per-test status (RECORDED, FAILED, MISSING) across test runs.
-
Serializer — encodes cassettes to JSON. Two implementations: Avaje Jsonb (default) and Jackson.
JUnit 5 Setup
Add the dependency:
dependencies {
testImplementation("com.integrallis:vectors-vcr-junit5:0.1.20")
testImplementation("com.integrallis:vectors-vcr-serde-avaje:0.1.20")
// or: vectors-vcr-serde-jackson
// Framework-specific wrappers (pick one or both):
testImplementation("com.integrallis:vectors-vcr-spring-ai:0.1.20")
testImplementation("com.integrallis:vectors-vcr-langchain4j:0.1.20")
}
Annotate your test class:
@VCRTest(mode = VCRMode.PLAYBACK_OR_RECORD,
dataDir = "src/test/resources/vcr-data") // this is the default; omit to inherit it
class EmbeddingTest {
@VCRModel
EmbeddingModel model = new OpenAiEmbeddingModel(apiKey);
@Test
void testEmbed() {
float[] embedding = model.embed("hello world");
assertThat(embedding).hasSize(1536);
}
@VCRRecord // force re-record for this test
@Test
void testReRecord() {
// ...
}
@VCRDisabled // bypass VCR entirely
@Test
void testLive() {
// ...
}
}
On the first run, the real model is called and responses are saved to cassette files under dataDir.
On subsequent runs, responses are replayed from cassettes — no API key or network needed.
Each framework wrapper signs the complete canonical request. Chat signatures include the messages, the configured model label, request options such as temperature and token limits, provider request parameters, tool specifications, and model defaults exposed by the framework. Embedding signatures include the input text or batch and model label. The ordinal key locates the cassette; the signature then proves that the current call is the interaction that was recorded there.
PLAYBACK_OR_RECORD is therefore the automatic maintenance mode: an unchanged request replays,
while a changed prompt, model setting, tool definition, or embedding input invokes the delegate and
replaces the stale cassette. Strict PLAYBACK never refreshes fixtures and throws
VCRCassetteStaleException on a mismatch. Legacy cassettes without signatures are refreshed by
PLAYBACK_OR_RECORD and rejected by strict PLAYBACK.
If a recording test fails after one or more model calls, the JUnit 5 extension deletes every
cassette written by that test and marks the test failed in the registry. A later
RECORD_FAILED run can then create a complete replacement without replaying partial fixtures.
TestNG Setup
dependencies {
testImplementation("com.integrallis:vectors-vcr-testng:0.1.20")
testImplementation("com.integrallis:vectors-vcr-serde-avaje:0.1.20")
}
@VCRTestNG(mode = VCRMode.PLAYBACK_OR_RECORD,
dataDir = "src/test/resources/vcr-data")
public class EmbeddingTest {
@VCRModel
EmbeddingModel model = new OpenAiEmbeddingModel(apiKey);
@Test
public void testEmbed() {
// ...
}
}
The TestNG listener is discovered through the service provider shipped by the module. If a recording test fails, it marks the registry entry failed and deletes every cassette successfully written by that test.
Spring AI Integration
Wrap Spring AI models with VCR-aware decorators:
@VCRModel
EmbeddingModel embeddingModel = new OpenAiEmbeddingModel(apiKey);
@VCRModel
ChatModel chatModel = new OpenAiChatModel(apiKey);
The vectors-vcr-spring-ai module provides VCRSpringAIEmbeddingModel,
VCRSpringAIChatModel, and VCRSpringAIStreamingChatModel wrappers. Blocking chat replay preserves
assistant messages, tool calls, all generations, generation metadata, response attributes, token
usage, rate limits, and prompt-filter metadata. Streaming replay preserves the ordered
ChatResponse chunks.
Field wrapping happens automatically when @VCRModel is used with the JUnit 5 or TestNG extension.
LangChain4j Integration
@VCRModel
dev.langchain4j.model.embedding.EmbeddingModel model =
new AllMiniLmL6V2EmbeddingModel();
@VCRModel
dev.langchain4j.model.chat.ChatModel chatModel = OpenAiChatModel.builder()
.apiKey(apiKey).build();
The vectors-vcr-langchain4j module provides VCREmbeddingModel, VCRChatModel, and
VCRStreamingChatModel wrappers, which decorate the LangChain4j embedding, blocking chat, and
streaming chat interfaces. Streaming cassettes preserve partial text, thinking, partial and
complete tool calls, and the final structured response.
Cassette Storage Options
File-Based (Default)
Cassettes are stored under the configured dataDir using the Vectors local-file backend.
Avaje and Jackson serialize the cassette payload as JSON inside the backend’s file framing.
Semantic Store
vectors-vcr-semantic-db provides a lower-level SemanticCassetteStore for reusing nearby
recorded embeddings in a manually configured LangChain4j embedding wrapper:
dependencies {
testImplementation("com.integrallis:vectors-vcr-semantic-db:0.1.20")
}
Adding the dependency does not replace the JUnit cassette-store factory automatically. In
PLAYBACK_OR_RECORD, the wrapper computes the current embedding with the live delegate before
querying the semantic store. A match above the configured cosine threshold (default: 0.95) can
reuse an existing embedding cassette instead of writing another one. This is not offline
playback, and it does not apply to chat calls or the Spring AI adapter.
Serializer Choice
Two cassette serializers are available:
| Serializer | Dependency | Notes |
|---|---|---|
Avaje Jsonb |
|
Default. Zero-annotation tree API. No annotation processor needed. |
Jackson |
|
Streaming API (JsonGenerator/JsonParser). Lower memory for large cassettes. |
Both delegate the cassette shape to the core CassetteTreeCodec, so cassettes are interchangeable
between serializers and new response fields cannot drift between the two implementations.
