Symfony命令测试时为何必须向构造函数传递参数?
问题:Symfony命令正常运行但测试时触发构造函数参数错误
环境:Symfony 6、Doctrine 3、PHPUnit
执行php bin/phpunit tests/Command/IsbnCleanupCommandTest.php时出现错误:
存在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
相关产品推荐
相关产品推荐

