You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Symfony命令测试时为何必须向构造函数传递参数?

问题:Symfony命令正常运行但测试时触发构造函数参数错误

环境:Symfony 6、Doctrine 3、PHPUnit

执行php bin/phpunit tests/Command/IsbnCleanupCommandTest.php时出现错误:

存在1个错误:

  1. App\Tests\Command\IsbnCleanupCommandTest::testExecute
    ArgumentCountError: App\Command\IsbnCleanupCommand::__construct()需要传入3个参数,但/var/www/company/tests/Command/IsbnCleanupCommandTest.php第18行仅传入0个

为何直接运行命令时无需传递构造函数参数,而测试时却必须传递?


命令代码

<?php

namespace App\Command;

use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Style\SymfonyStyle;
use Doctrine\ORM\EntityManagerInterface;
use App\Repository\TitelmetaRepository;
use App\Repository\IsbnRepository;

#[AsCommand(
    name: 'IsbnCleanup',
    description: 'Add a short description for your command',
)]
class IsbnCleanupCommand extends Command
{
    private $entityManager;
    private $titelMetaRepository;
    private $isbnRepository;

    public function __construct(EntityManagerInterface $entityManager, TitelMetaRepository $titelMetaRepository, IsbnRepository $isbnRepository)
    {
        parent::__construct();
        $this->entityManager = $entityManager;
        $this->titelMetaRepository = $titelMetaRepository;
        $this->isbnRepository = $isbnRepository;
    }    

    protected function configure(): void
    {
        $this
            ->addArgument('arg1', InputArgument::OPTIONAL, 'Argument description')
            ->addOption('option1', null, InputOption::VALUE_NONE, 'Option description')
        ;
    }

    protected function execute(InputInterface $input, OutputInterface $output): int
    {
        $io = new SymfonyStyle($input, $output);
        $arg1 = $input->getArgument('arg1');
        
        // set state value to NULL (=reset)
        $this->isbnRepository->resetStateValues();

        /* … flawless code  … */

        $io->success('You have a new command! Now make it your own! Pass --help to see your options.');

        return Command::SUCCESS;
    }
}

测试代码(原错误版本)

<?php

namespace App\Tests\Command;

use App\Command\IsbnCleanupCommand;
use Symfony\Bundle\FrameworkBundle\Console\Application;
use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase;
use Symfony\Component\Console\Tester\CommandTester;

class IsbnCleanupCommandTest extends KernelTestCase
{
    public function testExecute()
    {
        $kernel = static::createKernel();
        $kernel->boot();

        $application = new Application($kernel);
        $application->add(new IsbnCleanupCommand());

        $command = $application->find('app:IsbnCleanup');
        $commandTester = new CommandTester($command);
        $commandTester->execute([
            // pass arguments to the helper
            // 'username' => 'John',
            // ...
        ]);

        // the output of the command in the console
        $output = $commandTester->getDisplay();
        $this->assertStringContainsString('...', $output);

        // or use the tester to assert on the exit code
        // $this->assertSame(0, $commandTester->getStatusCode());
    }
}

解答

原因分析

  • 正常运行命令时,Symfony的依赖注入容器会自动处理构造函数参数:你的命令通过#[AsCommand]注解被容器注册,容器会自动查找并注入EntityManagerInterface、TitelMetaRepository和IsbnRepository这三个依赖,所以直接执行命令时不需要手动传参。
  • 测试代码中,你通过new IsbnCleanupCommand()手动实例化命令,此时没有经过Symfony容器,必须自己传入构造函数要求的所有参数,否则就会触发参数数量错误。

修正方案

方案1:从容器获取命令实例(推荐,集成测试)

利用KernelTestCase启动内核后,直接从应用中查找已注入依赖的命令实例,无需手动构造:

<?php

namespace App\Tests\Command;

use Symfony\Bundle\FrameworkBundle\Console\Application;
use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase;
use Symfony\Component\Console\Tester\CommandTester;

class IsbnCleanupCommandTest extends KernelTestCase
{
    public function testExecute()
    {
        $kernel = static::createKernel();
        $kernel->boot();

        $application = new Application($kernel);
        // 直接查找命令,容器已自动注入所有依赖
        // 注意命令名要和#[AsCommand]注解中的name一致:'IsbnCleanup'
        $command = $application->find('IsbnCleanup');
        $commandTester = new CommandTester($command);
        
        $commandTester->execute([
            // 按需传入命令的参数或选项
            // 'arg1' => 'test-value',
            // '--option1' => true,
        ]);

        $output = $commandTester->getDisplay();
        $this->assertStringContainsString('You have a new command!', $output);

        // 验证命令执行成功
        $this->assertSame(0, $commandTester->getStatusCode());
    }
}

方案2:手动模拟依赖(单元测试,无需完整内核)

如果想做轻量化的单元测试,不启动完整Symfony内核,可以用PHPUnit的模拟对象构造依赖:

<?php

namespace App\Tests\Command;

use App\Command\IsbnCleanupCommand;
use Doctrine\ORM\EntityManagerInterface;
use App\Repository\TitelmetaRepository;
use App\Repository\IsbnRepository;
use Symfony\Component\Console\Tester\CommandTester;
use Symfony\Component\Console\Command\Command;

class IsbnCleanupCommandTest extends \PHPUnit\Framework\TestCase
{
    public function testExecute()
    {
        // 创建三个依赖的模拟对象
        $emMock = $this->createMock(EntityManagerInterface::class);
        $titelMetaRepoMock = $this->createMock(TitelmetaRepository::class);
        $isbnRepoMock = $this->createMock(IsbnRepository::class);

        // 可选:验证resetStateValues方法是否被调用
        $isbnRepoMock->expects($this->once())
            ->method('resetStateValues');

        // 手动传入模拟依赖实例化命令
        $command = new IsbnCleanupCommand($emMock, $titelMetaRepoMock, $isbnRepoMock);
        $commandTester = new CommandTester($command);
        
        $commandTester->execute([]);

        $output = $commandTester->getDisplay();
        $this->assertStringContainsString('You have a new command!', $output);

        $this->assertSame(Command::SUCCESS, $commandTester->getStatusCode());
    }
}

内容的提问来源于stack exchange,提问作者Juergen Schulze

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.21 16:42:00