Laravel предоставляет API для тестирования Artisan-команд, включая симуляцию пользовательского ввода и проверку вывода.
Базовое тестирование команд
Метод artisan()
<?php
declare(strict_types=1);
namespace Tests\Feature;
use Tests\TestCase;
use Illuminate\Foundation\Testing\RefreshDatabase;
class CommandTest extends TestCase
{
use RefreshDatabase;
public function test_inspire_command(): void
{
$this->artisan('inspire')
->assertExitCode(0); // Command::SUCCESS
}
public function test_migrate_status(): void
{
$this->artisan('migrate:status')
->assertSuccessful(); // Same as assertExitCode(0)
}
public function test_command_with_arguments(): void
{
$this->artisan('user:create', [
'name' => 'John Doe',
'--email' => '[email protected]',
'--admin' => true,
])->assertSuccessful();
}
}
Exit Codes
<?php
use Symfony\Component\Console\Command\Command;
// Available exit code assertions
$this->artisan('command:name')
->assertExitCode(0) // Specific code
->assertSuccessful(); // Code === 0 (Command::SUCCESS)
$this->artisan('command:failing')
->assertFailed(); // Code !== 0
$this->artisan('command:invalid')
->assertExitCode(1); // Command::FAILURE
$this->artisan('command:invalid')
->assertExitCode(2); // Command::INVALID
Пример кастомной команды
Создадим команду для тестирования:
<?php
declare(strict_types=1);
namespace App\Console\Commands;
use App\Models\User;
use Illuminate\Console\Command;
class CreateUserCommand extends Command
{
protected $signature = 'user:create
{name : The name of the user}
{--email= : The email of the user}
{--admin : Create as admin user}
{--force : Skip confirmation}';
protected $description = 'Create a new user';
public function handle(): int
{
$name = $this->argument('name');
$email = $this->option('email') ?? $this->ask('Enter email address');
if (!$email) {
$this->error('Email is required!');
return Command::FAILURE;
}
if (User::where('email', $email)->exists()) {
$this->error("User with email {$email} already exists!");
return Command::FAILURE;
}
$isAdmin = $this->option('admin');
if (!$this->option('force')) {
$this->info("Creating user: {$name} ({$email})");
if ($isAdmin) {
$this->warn('This user will have admin privileges!');
}
if (!$this->confirm('Do you want to proceed?')) {
$this->info('User creation cancelled.');
return Command::SUCCESS;
}
}
$role = $this->choice(
'Select user role',
['user', 'editor', 'admin'],
$isAdmin ? 2 : 0
);
$user = User::create([
'name' => $name,
'email' => $email,
'password' => bcrypt('password'),
'role' => $role,
]);
$this->info("User #{$user->id} created successfully!");
$this->table(
['ID', 'Name', 'Email', 'Role'],
[[$user->id, $user->name, $user->email, $user->role]]
);
return Command::SUCCESS;
}
}
expectsQuestion
Симулирует ответы на вопросы команды (ask(), secret(), anticipate()):
<?php
public function test_command_asks_for_email(): void
{
$this->artisan('user:create', ['name' => 'John'])
->expectsQuestion('Enter email address', '[email protected]')
->expectsConfirmation('Do you want to proceed?', 'yes')
->expectsChoice('Select user role', 'user', ['user', 'editor', 'admin'])
->assertSuccessful();
$this->assertDatabaseHas('users', [
'name' => 'John',
'email' => '[email protected]',
]);
}
Подвох на экзамене: expectsQuestion() отвечает на ask(), secret() и anticipate(). Для confirm() используйте expectsConfirmation(). Для choice() используйте expectsChoice(). Они НЕ взаимозаменяемы.
expectsConfirmation
Симулирует ответ на confirm():
<?php
public function test_user_can_cancel_creation(): void
{
$this->artisan('user:create', [
'name' => 'John',
'--email' => '[email protected]',
])
->expectsConfirmation('Do you want to proceed?', 'no')
->expectsOutput('User creation cancelled.')
->assertSuccessful();
$this->assertDatabaseMissing('users', ['email' => '[email protected]']);
}
public function test_user_confirms_creation(): void
{
$this->artisan('user:create', [
'name' => 'John',
'--email' => '[email protected]',
])
->expectsConfirmation('Do you want to proceed?', 'yes')
->expectsChoice('Select user role', 'user', ['user', 'editor', 'admin'])
->assertSuccessful();
$this->assertDatabaseHas('users', ['email' => '[email protected]']);
}
expectsOutput и expectsOutputToContain
<?php
public function test_error_output_for_duplicate_email(): void
{
User::factory()->create(['email' => '[email protected]']);
$this->artisan('user:create', [
'name' => 'John',
'--email' => '[email protected]',
])
->expectsOutput('User with email [email protected] already exists!')
->assertFailed();
}
// Partial output match
public function test_success_message_contains_user_id(): void
{
$this->artisan('user:create', [
'name' => 'John',
'--email' => '[email protected]',
'--force' => true,
])
->expectsChoice('Select user role', 'user', ['user', 'editor', 'admin'])
->expectsOutputToContain('created successfully')
->assertSuccessful();
}
// Assert output does NOT contain
public function test_no_admin_warning_for_regular_user(): void
{
$this->artisan('user:create', [
'name' => 'John',
'--email' => '[email protected]',
])
->doesntExpectOutput('This user will have admin privileges!')
->expectsConfirmation('Do you want to proceed?', 'yes')
->expectsChoice('Select user role', 'user', ['user', 'editor', 'admin'])
->assertSuccessful();
}
// Assert output with warning for admin
public function test_shows_admin_warning(): void
{
$this->artisan('user:create', [
'name' => 'Admin',
'--email' => '[email protected]',
'--admin' => true,
])
->expectsOutput('This user will have admin privileges!')
->expectsConfirmation('Do you want to proceed?', 'yes')
->expectsChoice('Select user role', 'admin', ['user', 'editor', 'admin'])
->assertSuccessful();
}
Подвох на экзамене: expectsOutput() проверяет ТОЧНОЕ совпадение строки вывода. expectsOutputToContain() проверяет частичное совпадение. Если вывод содержит форматирование (ANSI коды), используйте expectsOutputToContain().
expectsChoice
<?php
public function test_role_selection(): void
{
$this->artisan('user:create', [
'name' => 'Editor',
'--email' => '[email protected]',
'--force' => true,
])
->expectsChoice(
'Select user role', // Question text
'editor', // Selected answer
['user', 'editor', 'admin'] // Available options
)
->assertSuccessful();
$this->assertDatabaseHas('users', [
'email' => '[email protected]',
'role' => 'editor',
]);
}
expectsTable
<?php
public function test_command_displays_table(): void
{
$this->artisan('user:create', [
'name' => 'John',
'--email' => '[email protected]',
'--force' => true,
])
->expectsChoice('Select user role', 'user', ['user', 'editor', 'admin'])
->expectsTable(
['ID', 'Name', 'Email', 'Role'],
[['1', 'John', '[email protected]', 'user']]
)
->assertSuccessful();
}
Тестирование команд с Progress Bar
<?php
declare(strict_types=1);
namespace App\Console\Commands;
use App\Models\User;
use Illuminate\Console\Command;
class SendNewsletterCommand extends Command
{
protected $signature = 'newsletter:send';
protected $description = 'Send newsletter to all users';
public function handle(): int
{
$users = User::where('subscribed', true)->get();
if ($users->isEmpty()) {
$this->warn('No subscribed users found.');
return Command::SUCCESS;
}
$this->info("Sending newsletter to {$users->count()} users...");
$bar = $this->output->createProgressBar($users->count());
$bar->start();
$sent = 0;
foreach ($users as $user) {
// Send newsletter logic...
$sent++;
$bar->advance();
}
$bar->finish();
$this->newLine();
$this->info("Newsletter sent to {$sent} users.");
return Command::SUCCESS;
}
}
<?php
public function test_newsletter_sends_to_subscribed_users(): void
{
User::factory(5)->create(['subscribed' => true]);
User::factory(3)->create(['subscribed' => false]);
$this->artisan('newsletter:send')
->expectsOutputToContain('Sending newsletter to 5 users')
->expectsOutputToContain('Newsletter sent to 5 users')
->assertSuccessful();
}
public function test_newsletter_handles_no_subscribers(): void
{
User::factory(3)->create(['subscribed' => false]);
$this->artisan('newsletter:send')
->expectsOutput('No subscribed users found.')
->assertSuccessful();
}
Тестирование Scheduled Commands
<?php
use Illuminate\Console\Scheduling\Schedule;
use Illuminate\Support\Facades\Artisan;
public function test_command_is_scheduled(): void
{
$schedule = app(Schedule::class);
$events = collect($schedule->events());
$this->assertTrue(
$events->contains(fn ($event) =>
str_contains($event->command, 'newsletter:send')
),
'Newsletter command is not scheduled'
);
}
// Laravel 11+: assertScheduled
public function test_prune_is_scheduled_daily(): void
{
// Test that model:prune runs daily
$this->artisan('schedule:list')
->expectsOutputToContain('model:prune')
->assertSuccessful();
}
Тестирование Isolated Commands
<?php
declare(strict_types=1);
namespace App\Console\Commands;
use Illuminate\Console\Command;
use Illuminate\Contracts\Console\Isolatable;
class ImportDataCommand extends Command implements Isolatable
{
protected $signature = 'data:import {source}';
protected $description = 'Import data from external source';
public function handle(): int
{
$this->info("Importing from {$this->argument('source')}...");
// Long-running process
sleep(30);
$this->info('Import complete!');
return Command::SUCCESS;
}
}
<?php
// Test isolated command behavior
public function test_import_command_is_isolatable(): void
{
$command = new \App\Console\Commands\ImportDataCommand();
$this->assertInstanceOf(
\Illuminate\Contracts\Console\Isolatable::class,
$command
);
}
Pest-стиль тестирования команд
<?php
use App\Models\User;
test('create user command works', function () {
$this->artisan('user:create', [
'name' => 'Jane',
'--email' => '[email protected]',
'--force' => true,
])
->expectsChoice('Select user role', 'user', ['user', 'editor', 'admin'])
->assertSuccessful();
expect(User::where('email', '[email protected]')->exists())->toBeTrue();
});
test('create user fails without email', function () {
$this->artisan('user:create', ['name' => 'John'])
->expectsQuestion('Enter email address', '')
->expectsOutput('Email is required!')
->assertExitCode(1);
});
test('create user with admin flag shows warning', function () {
$this->artisan('user:create', [
'name' => 'Admin',
'--email' => '[email protected]',
'--admin' => true,
])
->expectsOutput('This user will have admin privileges!')
->expectsConfirmation('Do you want to proceed?', 'yes')
->expectsChoice('Select user role', 'admin', ['user', 'editor', 'admin'])
->assertSuccessful();
expect(User::where('email', '[email protected]')->first())
->role->toBe('admin');
});
Тестирование вывода в разных форматах
<?php
declare(strict_types=1);
namespace App\Console\Commands;
use App\Models\User;
use Illuminate\Console\Command;
class ListUsersCommand extends Command
{
protected $signature = 'user:list {--format=table : Output format (table, json, csv)}';
protected $description = 'List all users';
public function handle(): int
{
$users = User::all(['id', 'name', 'email', 'role']);
if ($users->isEmpty()) {
$this->warn('No users found.');
return Command::SUCCESS;
}
return match ($this->option('format')) {
'json' => $this->outputJson($users),
'csv' => $this->outputCsv($users),
default => $this->outputTable($users),
};
}
private function outputTable($users): int
{
$this->table(
['ID', 'Name', 'Email', 'Role'],
$users->toArray()
);
return Command::SUCCESS;
}
private function outputJson($users): int
{
$this->line($users->toJson(JSON_PRETTY_PRINT));
return Command::SUCCESS;
}
private function outputCsv($users): int
{
$this->line('id,name,email,role');
$users->each(fn ($u) => $this->line("{$u->id},{$u->name},{$u->email},{$u->role}"));
return Command::SUCCESS;
}
}
<?php
test('list users in table format', function () {
User::factory()->create(['name' => 'John', 'email' => '[email protected]']);
$this->artisan('user:list')
->expectsTable(
['ID', 'Name', 'Email', 'Role'],
[['1', 'John', '[email protected]', 'user']]
)
->assertSuccessful();
});
test('list users in json format', function () {
User::factory()->create(['name' => 'John']);
$this->artisan('user:list', ['--format' => 'json'])
->expectsOutputToContain('"name": "John"')
->assertSuccessful();
});
test('empty user list shows warning', function () {
$this->artisan('user:list')
->expectsOutput('No users found.')
->assertSuccessful();
});