Skip to content

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

php
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

php
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

php
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

php
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

php
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:

php
/**
 * @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

php
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:

php
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:

php
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:

php
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:

php
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:

php
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:

php
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

php
// 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

php
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:

php
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:

php
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

php
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

php
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

php
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

php
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

php
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

php
// 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

php
// 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

Released under the MIT License.