Fake components to help you test your AI powered system
When you test an agent, you don't want every test run to make real API calls to OpenAI, Anthropic, or any other provider. Real calls are slow, cost money, and return different results every time, making your tests flaky and expensive. The same applies to RAG agents: you don't want to spin up a vector database or call an embeddings API just to verify your agent's logic.
Neuron ships with drop-in test doubles that solve this problem. FakeAIProvider replaces the AI provider, FakeEmbeddingsProvider replaces the embeddings provider, and FakeVectorStore replaces the vector store. They return predetermined responses, never hit the network, and record every interaction so you can assert exactly what your agent did.
Setup
Create a FakeAIProvider with the responses you expect the model to return, then inject it into your agent:
use NeuronAI\Chat\Messages\Stream\AssistantMessage;use NeuronAI\Testing\FakeAIProvider;$provider=newFakeAIProvider(newAssistantMessage('Hello! How can I help you?'));$agent=MyAgent::make()->setAiProvider($provider);
Responses are returned in order. If your agent makes multiple calls to the provider (e.g. tool calls), queue multiple responses:
The fake provider splits the response text into chunks, simulating a real stream:
You can change the chunk size with setStreamChunkSize():
Structured Output
Provide a JSON string that matches your output class schema. The agent will deserialize and validate it as usual:
Tool Calls
When the model decides to call a tool, it returns a ToolCallMessage. The agent executes the tool and loops back to the provider for a final answer. Queue both responses:
Assertions
FakeAIProvider includes built-in assertions you can use in your tests:
Inspecting Requests
For more advanced checks, access the raw recorded requests:
RAG
RAG agents depend on an embeddings provider and a vector store in addition to the AI provider. Neuron provides FakeEmbeddingsProvider and FakeVectorStore to replace both in tests.
FakeEmbeddingsProvider
Generates deterministic embeddings without calling any external API. Drop it in wherever you need an embeddings provider:
FakeVectorStore
Returns predetermined documents from similaritySearch() regardless of the embedding passed in. Pass the documents you want returned to the constructor:
RAG Chat
Adding Documents
Test that your indexing pipeline correctly embeds and stores documents:
public function test_agent_responds(): void
{
$provider = new FakeAIProvider(
new AssistantMessage('The capital of France is Paris.')
);
$agent = MyAgent::make()->setAiProvider($provider);
$message = $agent->chat(new UserMessage('What is the capital of France?'))->getMessage();
$this->assertSame('The capital of France is Paris.', $message->getContent());
$provider->assertCallCount(1);
}
public function test_agent_streams_response(): void
{
$provider = new FakeAIProvider(
new AssistantMessage('Hello world')
);
$agent = MyAgent::make()->setAiProvider($provider);
$handler = $agent->stream(new UserMessage('Hi'));
$chunks = [];
foreach ($handler->events() as $event) {
if ($event instanceof \NeuronAI\Chat\Messages\Stream\Chunks\TextChunk) {
$chunks[] = $event->content;
}
}
// The response is split into chunks of 5 characters by default
$this->assertSame(['Hello', ' worl', 'd'], $chunks);
// The final message is available after the stream is consumed
$state = $handler->run();
$this->assertSame('Hello world', $state->getMessage()->getContent());
}
$provider->setStreamChunkSize(10);
public function test_agent_returns_structured_output(): void
{
$provider = new FakeAIProvider(
new AssistantMessage('{"name": "Alice"}')
);
$agent = MyAgent::make()->setAiProvider($provider);
$user = $agent->structured(new UserMessage('Generate a user'), User::class);
$this->assertInstanceOf(User::class, $user);
$this->assertSame('Alice', $user->name);
}
use NeuronAI\Chat\Messages\ToolCallMessage;
public function test_agent_uses_tools(): void
{
$searchTool = Tool::make('search', 'Search the web')
->addProperty(new ToolProperty('query', PropertyType::STRING, 'Search query', true))
->setCallable(fn (string $query): string => "Results for: {$query}");
$provider = new FakeAIProvider(
// First call: the model asks to use the search tool
new ToolCallMessage(null, [
(clone $searchTool)->setCallId('call_1')->setInputs(['query' => 'PHP frameworks']),
]),
// Second call: the model responds using the tool result
new AssistantMessage('Here are the top PHP frameworks...')
);
$agent = MyAgent::make()
->setAiProvider($provider)
->addTool($searchTool);
$message = $agent->chat(new UserMessage('Best PHP frameworks?'))->getMessage();
$this->assertSame('Here are the top PHP frameworks...', $message->getContent());
$provider->assertCallCount(2);
}
// Verify the total number of provider calls
$provider->assertCallCount(2);
// Verify calls by method
$provider->assertMethodCallCount('chat', 1);
$provider->assertMethodCallCount('stream', 1);
// Verify no calls were made
$provider->assertNothingSent();
// Verify the system prompt
$provider->assertSystemPrompt('You are a helpful assistant.');
// Verify tools were configured
$provider->assertToolsConfigured(['search', 'calculator']);
// Custom assertion with a callback
$provider->assertSent(fn (RequestRecord $record): bool =>
$record->method === 'chat'
&& $record->messages[0]->getContent() === 'Hello'
);
$records = $provider->getRecorded();
$records[0]->method; // 'chat', 'stream', or 'structured'
$records[0]->messages; // Message[] sent to the provider
$records[0]->systemPrompt; // The system prompt at call time
$records[0]->tools; // The tools configured at call time
$records[0]->structuredClass; // The output class (structured calls only)
$records[0]->structuredSchema; // The JSON schema (structured calls only)
use NeuronAI\Testing\FakeEmbeddingsProvider;
$embeddings = new FakeEmbeddingsProvider();
use NeuronAI\RAG\Document;
use NeuronAI\Testing\FakeVectorStore;
$vectorStore = new FakeVectorStore([
new Document('Paris is the capital of France.'),
new Document('Berlin is the capital of Germany.'),
]);
public function test_rag_answers_from_documents(): void
{
$provider = new FakeAIProvider(
new AssistantMessage('Paris is the capital of France.')
);
$vectorStore = new FakeVectorStore([
new Document('France is a country in Europe. Its capital is Paris.'),
]);
$rag = MyRAG::make()
->setAiProvider($provider);
->setEmbeddingsProvider(new FakeEmbeddingsProvider());
->setVectorStore($vectorStore);
$message = $rag->chat(new UserMessage('What is the capital of France?'))->getMessage();
$this->assertSame('Paris is the capital of France.', $message->getContent());
$provider->assertCallCount(1);
$vectorStore->assertSearchCount(1);
}
public function test_documents_are_embedded_and_stored(): void
{
$embeddings = new FakeEmbeddingsProvider();
$vectorStore = new FakeVectorStore();
$rag = MyRAG::make()
->setAiProvider(new FakeAIProvider());
->setEmbeddingsProvider($embeddings);
->setVectorStore($vectorStore);
$rag->addDocuments([
new Document('First document'),
new Document('Second document'),
]);
$embeddings->assertCallCount(2);
$vectorStore->assertDocumentCount(2);
$vectorStore->assertHasDocumentWithContent('First document');
}