Deck by PromptPHP is designed to be easily testable. This guide covers strategies for testing prompts, commands, tracking, and AI SDK integration in your Laravel application.
If you're using Orchestra Testbench (recommended for package testing), register the service provider in your test case:
<?php
namespace Tests;
use Orchestra\Testbench\TestCase as BaseTestCase;
use PromptPHP\Deck\Providers\DeckServiceProvider;
abstract class TestCase extends BaseTestCase
{
protected function getPackageProviders($app): array
{
return [
DeckServiceProvider::class,
];
}
protected function getPackageAliases($app): array
{
return [
'Deck' => \PromptPHP\Deck\Facades\Deck::class,
];
}
}
For most tests, disable caching and tracking to avoid side effects:
protected function defineEnvironment($app): void
{
$app['config']->set('deck.cache.enabled', false);
$app['config']->set('deck.tracking.enabled', false);
$app['config']->set('deck.path', $this->getFixturePath('prompts'));
}
protected function getFixturePath(string $path = ''): string
{
return __DIR__ . '/fixtures/' . $path;
}
Create fixture prompt files in your test directory:
tests/
└── fixtures/
└── prompts/
└── order-summary/
├── v1/
│ ├── system.md
│ ├── user.md
│ └── metadata.json
└── metadata.json
tests/fixtures/prompts/order-summary/v1/system.md:
You are a {{ $tone }} AI assistant specialized in order summaries.
tests/fixtures/prompts/order-summary/v1/user.md:
Summarise this order: {{ $input }}
use PromptPHP\Deck\Facades\Deck;
it('loads a prompt by name', function () {
$prompt = Deck::get('order-summary', 1);
expect($prompt->name())->toBe('order-summary');
expect($prompt->version())->toBe(1);
});
it('throws when prompt does not exist', function () {
Deck::get('non-existent');
})->throws(\PromptPHP\Deck\Exceptions\PromptNotFoundException::class);
it('throws when version does not exist', function () {
Deck::get('order-summary', 999);
})->throws(\PromptPHP\Deck\Exceptions\InvalidVersionException::class);
it('interpolates variables in prompt content', function () {
$prompt = Deck::get('order-summary', 1);
$content = $prompt->system(['tone' => 'friendly']);
expect($content)->toContain('friendly');
expect($content)->not->toContain('{{ $tone }}');
});
it('leaves unmatched placeholders intact', function () {
$prompt = Deck::get('order-summary', 1);
$content = $prompt->system([]);
expect($content)->toContain('{{ $tone }}');
});
it('lists available roles', function () {
$prompt = Deck::get('order-summary', 1);
expect($prompt->roles())->toContain('system', 'user');
});
it('checks if a role exists', function () {
$prompt = Deck::get('order-summary', 1);
expect($prompt->has('system'))->toBeTrue();
expect($prompt->has('nonexistent'))->toBeFalse();
});
it('returns empty string for missing roles', function () {
$prompt = Deck::get('order-summary', 1);
expect($prompt->role('nonexistent'))->toBe('');
});
it('gets raw content without interpolation', function () {
$prompt = Deck::get('order-summary', 1);
$raw = $prompt->raw('system');
expect($raw)->toContain('{{ $tone }}');
});
it('builds messages array for AI APIs', function () {
$prompt = Deck::get('order-summary', 1);
$messages = $prompt->toMessages(['tone' => 'friendly', 'input' => 'Order #123']);
expect($messages)->toBeArray();
expect($messages[0])->toHaveKeys(['role', 'content']);
expect($messages[0]['role'])->toBe('system');
});
it('filters messages to specific roles', function () {
$prompt = Deck::get('order-summary', 1);
$messages = $prompt->toMessages([], ['system']);
expect($messages)->toHaveCount(1);
expect($messages[0]['role'])->toBe('system');
});
You can mock the facade in tests where you don't want filesystem access:
use PromptPHP\Deck\Facades\Deck;
use PromptPHP\Deck\PromptTemplate;
it('uses a mocked prompt', function () {
Deck::shouldReceive('get')
->with('order-summary', null)
->andReturn(new PromptTemplate(
'order-summary',
1,
['system' => 'You are a helpful assistant.'],
['description' => 'Test prompt']
));
$prompt = Deck::get('order-summary');
expect($prompt->system())->toBe('You are a helpful assistant.');
});
it('lists all versions for a prompt', function () {
$versions = Deck::versions('order-summary');
expect($versions)->toBeArray();
expect($versions[0])->toHaveKey('version');
});
it('activates a specific version', function () {
// With tracking disabled, this writes to metadata.json
$result = Deck::activate('order-summary', 1);
expect($result)->toBeTrue();
});
When testing tracking, enable it and run the migrations in your test setup:
protected function defineEnvironment($app): void
{
$app['config']->set('deck.tracking.enabled', true);
$app['config']->set('database.default', 'testing');
}
protected function defineDatabaseMigrations(): void
{
$this->loadMigrationsFrom(__DIR__ . '/../src/database/migrations');
}
use PromptPHP\Deck\Models\PromptExecution;
it('tracks prompt executions', function () {
Deck::track('order-summary', 1, [
'input' => ['message' => 'test'],
'output' => 'response',
'tokens' => 100,
]);
expect(PromptExecution::count())->toBe(1);
expect(PromptExecution::first()->prompt_name)->toBe('order-summary');
expect(PromptExecution::first()->tokens)->toBe(100);
});
it('does not track when disabled', function () {
config(['deck.tracking.enabled' => false]);
Deck::track('order-summary', 1, ['tokens' => 50]);
expect(PromptExecution::count())->toBe(0);
});
Use the included factories to seed test data:
use PromptPHP\Deck\Models\PromptVersion;
use PromptPHP\Deck\Models\PromptExecution;
it('queries execution data', function () {
PromptExecution::factory()
->forPrompt('order-summary', 2)
->count(10)
->create();
$avg = PromptExecution::where('prompt_name', 'order-summary')
->avg('latency_ms');
expect($avg)->toBeGreaterThan(0);
});
it('finds the active version', function () {
PromptVersion::factory()->named('order-summary')->version(1)->create();
PromptVersion::factory()->named('order-summary')->version(2)->active()->create();
$active = PromptVersion::where('name', 'order-summary')
->where('is_active', true)
->first();
expect($active->version)->toBe(2);
});
use Illuminate\Support\Facades\File;
it('creates a prompt via make:prompt', function () {
$this->artisan('make:prompt', ['name' => 'test-prompt'])
->assertSuccessful();
$promptPath = config('deck.path') . '/test-prompt/v1';
expect(File::isDirectory($promptPath))->toBeTrue();
expect(File::exists($promptPath . '/system.md'))->toBeTrue();
});
it('lists prompts via prompt:list', function () {
// Create a prompt first
$this->artisan('make:prompt', ['name' => 'list-test']);
$this->artisan('prompt:list')
->assertSuccessful();
});
it('activates a version via prompt:activate', function () {
$this->artisan('make:prompt', ['name' => 'activate-test']);
$this->artisan('prompt:activate', [
'name' => 'activate-test',
'version' => 1,
])->assertSuccessful();
});
it('tests a prompt via prompt:test', function () {
$this->artisan('make:prompt', ['name' => 'render-test']);
$this->artisan('prompt:test', [
'name' => 'render-test',
])->assertSuccessful();
});
it('shows diff between versions', function () {
$this->artisan('make:prompt', ['name' => 'diff-test']);
$this->artisan('prompt:diff', [
'name' => 'diff-test',
'--v1' => 1,
'--v2' => 1, // Compare with self for basic test
])->assertSuccessful();
});
Create a test agent class that uses the trait:
use PromptPHP\Deck\Concerns\HasPromptTemplate;
class TestAgent
{
use HasPromptTemplate;
}
it('derives prompt name from class name', function () {
$agent = new TestAgent;
expect($agent->promptName())->toBe('test-agent');
});
it('returns null for default prompt version', function () {
$agent = new TestAgent;
expect($agent->promptVersion())->toBeNull();
});
it('returns empty array for default variables', function () {
$agent = new TestAgent;
expect($agent->promptVariables())->toBe([]);
});
When testing with different prompt versions:
it('clears cached template between tests', function () {
$agent = new TestAgent;
// Load v1
$template1 = $agent->promptTemplate();
// Clear cache and load fresh
$agent->forgetPromptTemplate();
$template2 = $agent->promptTemplate();
// Both are fresh instances
expect($template1)->not->toBe($template2);
});
How can I help you explore Laravel packages today?