MidПрактика8 min

Тестирование консольных команд

artisan(), expectsQuestion, expectsOutput, expectsConfirmation, тестирование exit codes

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();
});

Проверь себя

Какой метод используется для симуляции ответа на `confirm()` в тестах?

Чем отличается `expectsOutput()` от `expectsOutputToContain()`?

Что означает assertSuccessful() в контексте тестирования Artisan-команд?

Как передать опцию --admin (boolean) при вызове artisan() в тесте?