원클릭으로
testing
JOB-257 — FakeLLM, in-process test doubles, writing offline chain/agent tests.
Codex 또는 Claude로 설치 이 Prompt를 복사해 Codex, Claude 또는 다른 어시스턴트에 붙여 넣으면 Skill 페이지를 검토하고 설치를 진행할 수 있습니다.
메뉴
JOB-257 — FakeLLM, in-process test doubles, writing offline chain/agent tests.
Codex 또는 Claude로 설치 이 Prompt를 복사해 Codex, Claude 또는 다른 어시스턴트에 붙여 넣으면 Skill 페이지를 검토하고 설치를 진행할 수 있습니다.
SOC 직업 분류 기준
AgentExecutor, ChatAgent (ReAct), OpenAIToolsAgent, and the Agent trait.
JOB-251 — Why Arc<dyn LLM> over LLMClone, migration pattern, and correct LLM ownership model.
All Chain types — LLMChain, ConversationalChain, SequentialChain, StuffDocuments, ConversationalRetrievalQA, SqlDatabaseChain.
Trait conformance tests, property-based tests (proptest), fuzz targets, and boundary contracts for langchainx traits.
Core langchainx patterns — LLMChain, builder pattern, Chain trait, prompt macros, and basic invocation.
Constructing LLM backends (OpenAI, Claude, DeepSeek, Qwen, Ollama) and configuring CallOptions.
| name | testing |
| description | JOB-257 — FakeLLM, in-process test doubles, writing offline chain/agent tests. |
Every chain and agent test is gated by #[ignore]:
#[tokio::test]
#[ignore] // requires live OPENAI_API_KEY
async fn test_invoke_chain() { ... }
There is zero automated test coverage for chain or agent behavior.
## FakeLLM (implement in src/test_utils.rs)// src/test_utils.rs — gated behind #[cfg(any(test, feature = "test-utils"))]
use std::collections::VecDeque;
use std::sync::{Arc, atomic::{AtomicUsize, Ordering}};
use tokio::sync::Mutex;
use async_trait::async_trait;
use langchainx::{
language_models::{llm::LLM, options::CallOptions, GenerateResult, LLMError},
schemas::{Message, StreamData},
};
#[derive(Clone)]
pub struct FakeLLM {
pub responses: Arc<Mutex<VecDeque<String>>>,
pub call_count: Arc<AtomicUsize>,
}
impl FakeLLM {
pub fn new(responses: Vec<&str>) -> Self {
Self {
responses: Arc::new(Mutex::new(
responses.into_iter().map(String::from).collect()
)),
call_count: Arc::new(AtomicUsize::new(0)),
}
}
pub fn call_count(&self) -> usize {
self.call_count.load(Ordering::SeqCst)
}
}
#[async_trait]
impl LLM for FakeLLM {
async fn generate(&self, _messages: &[Message]) -> Result<GenerateResult, LLMError> {
self.call_count.fetch_add(1, Ordering::SeqCst);
let mut responses = self.responses.lock().await;
let generation = responses.pop_front().unwrap_or_default();
Ok(GenerateResult { generation, ..Default::default() })
}
async fn stream(
&self,
_messages: &[Message],
) -> Result<std::pin::Pin<Box<dyn futures::Stream<Item = Result<StreamData, LLMError>> + Send>>, LLMError> {
unimplemented!("FakeLLM::stream — use FakeStreamingLLM for stream tests")
}
}
## Writing Offline Chain Tests
#[cfg(test)]
mod tests {
use super::*;
use crate::test_utils::FakeLLM;
use langchainx::{
chain::{Chain, LLMChainBuilder},
message_formatter, fmt_template,
prompt::{HumanMessagePromptTemplate, MessageOrTemplate},
prompt_args, template_fstring,
};
#[tokio::test]
async fn test_llm_chain_invoke() {
let fake = FakeLLM::new(vec!["Hello from FakeLLM!"]);
let prompt = message_formatter![
fmt_template!(HumanMessagePromptTemplate::new(
template_fstring!("{input}", "input")
)),
];
let chain = LLMChainBuilder::new()
.llm(fake.clone())
.prompt(prompt)
.build()
.unwrap();
let result = chain.invoke(prompt_args! { "input" => "Hi" }).await.unwrap();
assert_eq!(result, "Hello from FakeLLM!");
assert_eq!(fake.call_count(), 1);
}
#[tokio::test]
async fn test_chain_returns_error_on_empty_responses() {
let fake = FakeLLM::new(vec![]); // no responses queued
let chain = LLMChainBuilder::new()
.llm(fake)
.prompt(prompt)
.build()
.unwrap();
let result = chain.invoke(prompt_args! { "input" => "Hi" }).await.unwrap();
assert_eq!(result, ""); // unwrap_or_default in FakeLLM
}
}
## Local LLMs for Tests Needing Real Generated Output
When a test requires actual language model reasoning (not canned responses), use a small local model via Ollama instead of a cloud API. This keeps tests offline, fast, and free.
# Install Ollama: https://ollama.com
ollama pull qwen2.5:0.5b # 400MB — fastest, good for basic reasoning
ollama pull llama3.2:1b # 1.3GB — better quality
ollama pull phi3:mini # 2.2GB — strong reasoning
// Cargo.toml: langchainx = { features = ["ollama"] }
#[tokio::test]
#[cfg_attr(not(feature = "local-llm-tests"), ignore)]
async fn test_chain_with_real_generation() {
use langchainx::llm::ollama::client::Ollama;
let llm = Ollama::default()
.with_model("qwen2.5:0.5b") // smallest available
.with_base_url("http://localhost:11434");
let chain = LLMChainBuilder::new()
.llm(llm)
.prompt(prompt)
.build()
.unwrap();
let result = chain.invoke(prompt_args! {
"input" => "Say only the word 'pong'."
}).await.unwrap();
assert!(result.to_lowercase().contains("pong"));
}
Add to Cargo.toml to gate local-LLM tests separately from cloud tests:
[features]
local-llm-tests = ["ollama"]
Run local LLM tests:
cargo test --features local-llm-tests -- --ignored
Local LLM tests should verify doneness, not correctness. The model completed the task and returned something — not that it returned a specific string.
// WRONG — brittle, model-dependent phrasing
assert_eq!(result, "The capital of France is Paris.");
assert!(result.contains("Paris"));
// CORRECT — verify task completion
assert!(!result.is_empty(), "model returned no output");
assert!(result.len() > 10, "response too short to be a real answer");
// CORRECT — verify structural properties, not content
assert!(result.trim().ends_with('.') || result.trim().ends_with('?') || result.trim().ends_with('!'),
"response should be a complete sentence");
// CORRECT — for agent/tool tests, verify the tool was called
assert!(executor_called_tool, "agent should have invoked the tool");
assert!(!result.is_empty(), "agent should have produced a final answer");
Test failure means the chain/agent broke (panicked, returned error, returned empty), not that the model gave a different phrasing than expected.
| Model | Size | Use for |
|---|---|---|
qwen2.5:0.5b | 400MB | basic doneness checks (non-empty, no panic) |
llama3.2:1b | 1.3GB | chain flow tests, multi-turn memory |
phi3:mini | 2.2GB | agent tool-use loop tests |
Always use the smallest model that passes the test — prefer qwen2.5:0.5b by default.
#[cfg_attr(not(feature = "local-llm-tests"), ignore)]#[ignore] in tests/integration/, require explicit env var#[ignore] only after replacing the live-API call with FakeLLM or local Ollamaassert_eq!(fake.call_count(), N) to verify chain invocation countsFailingLLM that always returns Err(LLMError::...)OPENAI_API_KEY or CLAUDE_API_KEY in automated CI