Testing
Strategies for testing Atlas-powered applications.
Testing Philosophy
Atlas is designed for testability:
- Stateless agents — No hidden state to manage
- Dependency injection — All services are injectable
- Contract-based — Mock interfaces, not implementations
- Pipeline system — Intercept and modify behavior in tests
Unit Testing Agents
Testing Agent Configuration
use App\Agents\CustomerSupportAgent;
use PHPUnit\Framework\TestCase;
class CustomerSupportAgentTest extends TestCase
{
public function test_agent_has_correct_configuration(): void
{
$agent = new CustomerSupportAgent();
$this->assertEquals('customer-support', $agent->key());
$this->assertEquals('openai', $agent->provider());
$this->assertEquals('gpt-4o', $agent->model());
$this->assertStringContainsString('customer support', $agent->systemPrompt());
}
public function test_agent_has_required_tools(): void
{
$agent = new CustomerSupportAgent();
$tools = $agent->tools();
$this->assertContains(LookupOrderTool::class, $tools);
$this->assertContains(SearchProductsTool::class, $tools);
}
}Testing System Prompt Variables
public function test_system_prompt_contains_expected_variables(): void
{
$agent = new CustomerSupportAgent();
$prompt = $agent->systemPrompt();
$this->assertStringContainsString('{user_name}', $prompt);
$this->assertStringContainsString('{customer_name}', $prompt);
}Unit Testing Tools
Testing Tool Logic
use App\Tools\LookupOrderTool;
use Atlasphp\Atlas\Tools\Support\ToolContext;
use PHPUnit\Framework\TestCase;
class LookupOrderToolTest extends TestCase
{
public function test_returns_order_when_found(): void
{
// Create a mock order
$order = Order::factory()->create([
'id' => 'ORD-123',
'status' => 'shipped',
]);
$tool = new LookupOrderTool();
$context = new ToolContext([]);
$result = $tool->handle(['order_id' => 'ORD-123'], $context);
$this->assertTrue($result->succeeded());
$this->assertStringContainsString('shipped', $result->toText());
}
public function test_returns_error_when_not_found(): void
{
$tool = new LookupOrderTool();
$context = new ToolContext([]);
$result = $tool->handle(['order_id' => 'INVALID'], $context);
$this->assertTrue($result->failed());
$this->assertStringContainsString('not found', $result->toText());
}
public function test_uses_context_metadata_for_authorization(): void
{
$user = User::factory()->create();
$order = Order::factory()->create(['user_id' => $user->id]);
$tool = new LookupOrderTool();
$context = new ToolContext(['user_id' => $user->id]);
$result = $tool->handle(['order_id' => $order->id], $context);
$this->assertTrue($result->succeeded());
}
}Testing Tool Parameters
public function test_tool_has_correct_parameters(): void
{
$tool = new LookupOrderTool();
$params = $tool->parameters();
$this->assertCount(1, $params);
$this->assertEquals('order_id', $params[0]->name);
$this->assertFalse($params[0]->nullable);
}Integration Testing
Mocking the AtlasManager
use Atlasphp\Atlas\Providers\Services\AtlasManager;
use Mockery;
class ChatControllerTest extends TestCase
{
public function test_chat_endpoint_returns_response(): void
{
// Use Atlas::fake() instead - see "Faking Atlas" section
Atlas::fake();
$response = $this->postJson('/api/chat', [
'message' => 'Hello',
]);
$response->assertOk();
Atlas::assertCalled('support-agent');
}
}Testing with Real Responses
For integration tests that need real AI responses:
/**
* @group integration
* @group requires-api-key
*/
class AtlasIntegrationTest extends TestCase
{
public function test_simple_chat_returns_response(): void
{
$response = Atlas::agent('test-agent')->chat('Say hello');
$this->assertTrue($response->hasText());
$this->assertNotEmpty($response->text);
}
}Faking Atlas
Atlas provides a built-in Atlas::fake() method for testing, following Laravel's Http::fake() pattern:
Basic Usage
use Atlasphp\Atlas\Atlas;
use Atlasphp\Atlas\Testing\Support\FakeResponseSequence;
use Prism\Prism\Text\Response as PrismResponse;
use Prism\Prism\Enums\FinishReason;
use Prism\Prism\ValueObjects\Usage;
public function test_chat_returns_expected_response(): void
{
// Enable fake mode with a default response
Atlas::fake([
new PrismResponse(
text: 'Hello! How can I help?',
finishReason: FinishReason::Stop,
usage: new Usage(promptTokens: 10, completionTokens: 20),
),
]);
// Call your code that uses Atlas
$response = Atlas::agent('support-agent')->chat('Hello');
// Assert the response (returns AgentResponse which wraps PrismResponse)
$this->assertEquals('Hello! How can I help?', $response->text());
// Assert the agent was called
Atlas::assertCalled('support-agent');
}Agent-Specific Responses
Configure different responses for different agents:
use Prism\Prism\Text\Response as PrismResponse;
use Prism\Prism\Enums\FinishReason;
use Prism\Prism\ValueObjects\Usage;
// Helper to create responses
$makeResponse = fn(string $text) => new PrismResponse(
text: $text,
finishReason: FinishReason::Stop,
usage: new Usage(promptTokens: 10, completionTokens: 20),
);
Atlas::fake()
->forAgent('billing-agent', $makeResponse('Your balance is $100'))
->forAgent('support-agent', $makeResponse('How can I help?'));
$billing = Atlas::agent('billing-agent')->chat('Balance?');
$support = Atlas::agent('support-agent')->chat('Help');
Atlas::assertCalled('billing-agent');
Atlas::assertCalled('support-agent');Sequential Responses
Return different responses for consecutive calls:
use Prism\Prism\Text\Response as PrismResponse;
use Prism\Prism\Enums\FinishReason;
use Prism\Prism\ValueObjects\Usage;
$makeResponse = fn(string $text) => new PrismResponse(
text: $text,
finishReason: FinishReason::Stop,
usage: new Usage(promptTokens: 10, completionTokens: 20),
);
Atlas::fake()
->forAgent('assistant')
->sequence([
$makeResponse('First response'),
$makeResponse('Second response'),
$makeResponse('Third response'),
]);
// Each call returns the next response in sequence
$first = Atlas::agent('assistant')->chat('1'); // "First response"
$second = Atlas::agent('assistant')->chat('2'); // "Second response"
$third = Atlas::agent('assistant')->chat('3'); // "Third response"Conditional Responses
Return responses based on input content:
use Prism\Prism\Text\Response as PrismResponse;
use Prism\Prism\Enums\FinishReason;
use Prism\Prism\ValueObjects\Usage;
$makeResponse = fn(string $text) => new PrismResponse(
text: $text,
finishReason: FinishReason::Stop,
usage: new Usage(promptTokens: 10, completionTokens: 20),
);
Atlas::fake()
->forAgent('assistant')
->when(
fn($agent, $input) => str_contains($input, 'order'),
$makeResponse('I can help with your order')
)
->when(
fn($agent, $input) => str_contains($input, 'refund'),
$makeResponse('I can process your refund')
);Dynamic Response Factories
Use respondUsing() for closure-based response generation. The closure receives a RecordedRequest and should return a response text string or a PrismResponse:
use Atlasphp\Atlas\Testing\Support\RecordedRequest;
Atlas::fake()->respondUsing('support-agent', function (RecordedRequest $request) {
return "Echo: {$request->input}";
});
$response = Atlas::agent('support-agent')->chat('Hello');
$this->assertEquals('Echo: Hello', $response->text());This is useful when fake responses need to vary based on the input or context of each request.
Testing Exceptions
Test error handling by throwing exceptions:
use Atlasphp\Atlas\Providers\Exceptions\RateLimitedException;
Atlas::fake()
->forAgent('assistant')
->throw(new RateLimitedException([], 60));
$this->expectException(RateLimitedException::class);
Atlas::agent('assistant')->chat('Hello');Streaming Fakes
Create fake streaming responses:
use Atlasphp\Atlas\Testing\Support\StreamEventFactory;
Atlas::fake([
StreamEventFactory::fromText('Hello, this is streamed!'),
]);
$stream = Atlas::agent('assistant')->chat('Hello', stream: true);
foreach ($stream as $event) {
// Process events...
}Assertion Methods
// Assert an agent was called
Atlas::assertCalled('support-agent');
// Assert an agent was called a specific number of times
Atlas::assertCalledTimes('support-agent', 3);
// Assert an agent was NOT called
Atlas::assertNotCalled('billing-agent');
// Assert nothing was called
Atlas::assertNothingCalled();
// Assert with input matching
Atlas::assertCalled('support-agent', fn($recorded) =>
str_contains($recorded->input, 'order')
);
// Assert with context matching
Atlas::assertSentWithContext('support-agent', fn($context) =>
$context->metadata['user_id'] === 123
);
// Assert schema was used
Atlas::assertSentWithSchema('analyzer', fn($schema) =>
$schema->name === 'sentiment'
);
// Assert streaming was used
Atlas::assertStreamed('support-agent');
Atlas::assertNotStreamed('support-agent');
// Assert context details
Atlas::assertCalledWithVariables('support-agent', ['user' => 'John']);
Atlas::assertCalledWithProvider('support-agent', 'openai');
Atlas::assertCalledWithModel('support-agent', 'gpt-4o');
Atlas::assertCalledWithTools('support-agent', [LookupOrderTool::class]);Accessing Recorded Requests
Atlas::fake(); // Uses empty response by default
Atlas::agent('assistant')->chat('Hello');
Atlas::agent('assistant')->chat('How are you?');
// Get all recorded requests
$recorded = Atlas::recorded();
// Get requests for a specific agent
$assistantRequests = Atlas::recordedFor('assistant');
foreach ($assistantRequests as $request) {
echo $request->agentKey(); // Agent key
echo $request->input; // User input
$request->context; // AgentContext
$request->response; // AgentResponse (wraps PrismResponse)
$request->timestamp; // When it was called
$request->wasStreamed; // Whether streaming was used
}Preventing Stray Requests
Fail tests if an unexpected agent is called:
use Prism\Prism\Text\Response as PrismResponse;
use Prism\Prism\Enums\FinishReason;
use Prism\Prism\ValueObjects\Usage;
Atlas::fake()
->forAgent('support-agent', new PrismResponse(
text: 'OK',
finishReason: FinishReason::Stop,
usage: new Usage(promptTokens: 10, completionTokens: 5),
))
->preventStrayRequests();
// This will pass
Atlas::agent('support-agent')->chat('Hello');
// This will throw an exception - no fake configured for 'other-agent'
Atlas::agent('other-agent')->chat('Hello'); // Throws!Cleanup
Always restore Atlas after tests:
protected function tearDown(): void
{
Atlas::unfake();
parent::tearDown();
}
// Or use the trait
use Atlasphp\Atlas\Testing\Concerns\InteractsWithAtlasFake;
class MyTest extends TestCase
{
use InteractsWithAtlasFake; // Automatically unfakes after each test
}Testing Pipelines
Testing Pipeline Handlers
use Atlasphp\Atlas\Agents\Support\AgentResponse;
use Atlasphp\Atlas\Agents\Support\AgentContext;
use Prism\Prism\Text\Response as PrismResponse;
use Prism\Prism\Enums\FinishReason;
use Prism\Prism\ValueObjects\Usage;
class LogAgentExecutionTest extends TestCase
{
public function test_logs_execution_details(): void
{
Log::shouldReceive('info')
->twice()
->withArgs(function ($message, $context) {
return str_contains($message, 'Agent execution');
});
$handler = new LogAgentExecution();
$agent = new TestAgent();
$handler([
'agent' => $agent,
'input' => 'Hello',
'context' => new AgentContext(),
], fn($data) => new AgentResponse(
response: new PrismResponse(
text: 'Hi',
finishReason: FinishReason::Stop,
usage: new Usage(promptTokens: 10, completionTokens: 5),
),
agent: $agent,
input: 'Hello',
systemPrompt: null,
context: $data['context'],
));
}
}Disabling Pipelines in Tests
public function setUp(): void
{
parent::setUp();
// Disable pipelines for isolated testing
$registry = app(PipelineRegistry::class);
$registry->setActive('agent.before_execute', false);
$registry->setActive('agent.after_execute', false);
}Testing Structured Output
use Atlasphp\Atlas\Schema\Schema;
use Prism\Prism\Structured\Response as StructuredResponse;
use Prism\Prism\Enums\FinishReason;
use Prism\Prism\ValueObjects\Usage;
public function test_extracts_structured_data(): void
{
// Create a fake structured response
$structuredResponse = new StructuredResponse(
text: '{"sentiment": "positive"}',
structured: ['sentiment' => 'positive'],
finishReason: FinishReason::Stop,
usage: new Usage(promptTokens: 10, completionTokens: 20),
);
// Use Atlas::fake() for testing
Atlas::fake([$structuredResponse]);
$response = Atlas::agent('analyzer')
->withSchema(
Schema::object('sentiment', 'Sentiment result')
->string('sentiment', 'The sentiment')
)
->chat('Great product!');
$this->assertTrue($response->isStructured());
$this->assertEquals('positive', $response->structured()['sentiment']);
}Best Practices
1. Use Factories for Test Data
use Prism\Prism\Text\Response as PrismResponse;
use Prism\Prism\Enums\FinishReason;
use Prism\Prism\ValueObjects\Usage;
// PrismResponseFactory.php - Helper for creating test responses
class PrismResponseFactory
{
public static function text(string $text): PrismResponse
{
return new PrismResponse(
text: $text,
finishReason: FinishReason::Stop,
usage: new Usage(promptTokens: 10, completionTokens: 20),
);
}
public static function withUsage(string $text, int $promptTokens, int $completionTokens): PrismResponse
{
return new PrismResponse(
text: $text,
finishReason: FinishReason::Stop,
usage: new Usage(promptTokens: $promptTokens, completionTokens: $completionTokens),
);
}
}2. Test Error Scenarios
public function test_handles_provider_errors(): void
{
$mockManager = Mockery::mock(AtlasManager::class);
$mockManager->shouldReceive('chat')
->andThrow(new ProviderException('Rate limit exceeded'));
$this->app->instance(AtlasManager::class, $mockManager);
$response = $this->postJson('/api/chat', ['message' => 'Hello']);
$response->assertStatus(503)
->assertJson(['error' => 'Service temporarily unavailable']);
}3. Separate Unit and Integration Tests
// phpunit.xml
<testsuites>
<testsuite name="Unit">
<directory>tests/Unit</directory>
</testsuite>
<testsuite name="Integration">
<directory>tests/Integration</directory>
</testsuite>
</testsuites>
<groups>
<exclude>
<group>requires-api-key</group>
</exclude>
</groups>API Reference
// Enabling fake mode
$fake = Atlas::fake(); // Returns AtlasFake instance
$fake = Atlas::fake([PrismResponse $response]); // With default response
// Configuring fake responses
$fake->response(string $agentKey): PendingFakeRequest;
$fake->response(string $agentKey, PrismResponse $response): AtlasFake;
$fake->respondUsing(string $agentKey, Closure $factory): AtlasFake; // Dynamic responses
$fake->sequence(array $responses): AtlasFake; // Default sequence for any agent
$fake->preventStrayRequests(): AtlasFake; // Fail on unconfigured agents
$fake->allowStrayRequests(): AtlasFake; // Allow unconfigured agents
$fake->reset(): AtlasFake; // Clear recorded requests
// PendingFakeRequest fluent API
$fake->response('agent-key')
->return(PrismResponse $response): AtlasFake;
->returnSequence(array $responses): AtlasFake;
->throw(Throwable $exception): AtlasFake;
->whenEmpty(PrismResponse|Throwable $response): PendingFakeRequest;
// Creating fake Prism responses
use Prism\Prism\Text\Response as PrismResponse;
use Prism\Prism\ValueObjects\Usage;
use Prism\Prism\Enums\FinishReason;
// Simple text response
new PrismResponse(
text: 'Hello! How can I help?',
finishReason: FinishReason::Stop,
usage: new Usage(promptTokens: 10, completionTokens: 20),
);
// Assertion methods
$fake->assertCalled(): void; // Any agent called
$fake->assertCalled(string $agentKey): void; // Specific agent called
$fake->assertCalledTimes(string $agentKey, int $times): void;
$fake->assertNotCalled(string $agentKey): void;
$fake->assertNothingCalled(): void;
$fake->assertSent(Closure $callback): void; // Custom assertion
$fake->assertSentWithContext(string $key, mixed $value = null): void;
$fake->assertSentWithSchema(): void;
$fake->assertSentWithInput(string $needle): void;
$fake->assertStreamed(?string $agentKey): void; // Streaming was used
$fake->assertNotStreamed(string $agentKey): void; // Streaming was NOT used
$fake->assertCalledWithVariables(string $agentKey, array $variables): void;
$fake->assertCalledWithProvider(string $agentKey, string $provider): void;
$fake->assertCalledWithModel(string $agentKey, string $model): void;
$fake->assertCalledWithTools(string $agentKey, array $toolClasses): void;
// Accessing recorded requests
$fake->recorded(): array; // All RecordedRequest objects
$fake->recordedFor(string $agentKey): array; // For specific agent
// RecordedRequest properties and methods
$request->agent; // AgentContract instance
$request->input; // User input string
$request->context; // AgentContext
$request->response; // AgentResponse (wraps PrismResponse)
$request->timestamp; // Unix timestamp
$request->wasStreamed; // bool — whether streaming was used
$request->agentKey(): string;
$request->inputContains(string $needle): bool;
$request->hasMetadata(string $key, mixed $value = null): bool;
$request->hasPrismCall(string $method): bool;
$request->getPrismCallArgs(string $method): ?array;
// Cleanup
Atlas::unfake(); // Restore real executor
$fake->restore(); // Alternative restore method
// Auto-cleanup trait
use Atlasphp\Atlas\Testing\Concerns\InteractsWithAtlasFake;Next Steps
- Agents — Build testable agents
- Tools — Build testable tools
- Error Handling — Test error scenarios