【问题标题】:Symfony mock service when test console command测试控制台命令时的 Symfony 模拟服务
【发布时间】:2018-04-15 10:07:21
【问题描述】:

测试控制台命令时如何模拟某些服务。我有一些控制台命令,在这个命令中我得到了一些服务,我想模拟这个服务

控制台命令

const APP_SATISFACTION_REPORT = 'app:satisfactionrepor';

protected function configure()
{
    $this
        ->setName(self::APP_SATISFACTION_REPORT)
        ->setDescription('Send Satisfaction Report');
}

/**
 * @param InputInterface  $input
 * @param OutputInterface $output
 */
protected function execute(InputInterface $input, OutputInterface $output)
{
    $container = $this->getContainer();
    $serviceCompanyRepo = $container->get('app.repository.orm.service_company_repository');
    $satisfactionReport = $container->get('app.services.satisfaction_report');

    /** @var ServiceCompany $serviceCompany */
    foreach ($serviceCompanyRepo->findAll() as $serviceCompany) {
        try {
            $satisfactionReport->sendReport($serviceCompany);
        } catch (\Exception $e) {
            $io->warning(sprintf(
                'Failed to send satisfaction report for service company with ID %s',
                $serviceCompany->getId()
            ));
        }
    }
}

和我的测试

 /** @var  Console\Application $application */
protected $application;
protected $container;

/** @var BufferedOutput $output */
protected $output;

/**
 * @var ServiceCompanyRepository
 */
private $serviceCompanyRepository;

准备控制台命令

public function setUp()
{
    parent::setUp();

    $entityManager = $this->getEntityManager();

    $this->serviceCompanyRepository = $entityManager->getRepository(ServiceCompany::class);

    static::bootKernel();
    $this->container = static::$kernel->getContainer();
    $this->application = new Console\Application(static::$kernel);
    $this->application->setAutoExit(false);
    $master = new SatisfactionReportCommand();
    $this->application->add($master);
}

public function setUpMaster() {
    $this->output = new BufferedOutput();
    $this->application->run(new ArgvInput([
        './bin/console',
        SatisfactionReportCommand::APP_SATISFACTION_REPORT,
    ]), $this->output);
} 

public function testGetMasterOutput()
{
    $this->loadFixture(ServiceCompany::class);

    /** @var ServiceCompany[] $serviceCompanies */
    $serviceCompanies = $this->serviceCompanyRepository->findAll();
    $this->assertCount(2, $serviceCompanies);

    $client = self::createClient();

模拟服务app.services.satisfaction_report

    $service = $this->getMockService($serviceCompanies);

并将其设置在容器中

    $client->getContainer()->set('app.services.satisfaction_report', $service);

    $this->setUpMaster();
    $output = $this->output->fetch();
}

protected function getMockService($serviceCompanies)
{
    $service = $this->getMockBuilder(SatisfactionReport::class)
        ->setMethods(['sendReport'])
        ->disableOriginalConstructor()
        ->getMock();

    $service
        ->expects($this->exactly(2))
        ->method('sendReport')
        ->withConsecutive(
            [$serviceCompanies[0]],
            [$serviceCompanies[1]]
        );

    return $service;
}

如何模拟app.services.satisfaction_report?设置在容器中 app.services.satisfaction_report 帮不了我

【问题讨论】:

    标签: symfony unit-testing mocking


    【解决方案1】:

    这是我的示例类:

    class MainCommandTest extends IntegrationTestCase
    {
    
        /**
         * @var MainCommand
         */
        protected $subject;
    
        /**
         * @var Application
         */
        protected $application;
    
        /**
         * sets test subject
         *
         * @return void
         */
        public function setUp()
        {
            parent::setUp();
    
            static::bootKernel();
    
            $readStreams = new ReadStreams();
    
            $udpStreamMock = $this->getMockBuilder(UdpStream::class)->disableOriginalConstructor()->setMethods(['readIncomingStreams'])->getMock();
            $udpStreamMock->expects($this->once())->method('readIncomingStreams')->willReturn($readStreams);
            static::$kernel->getContainer()->set(UdpStream::class, $udpStreamMock);
    
            $application = new Application($this::$kernel);
            $this->subject = $this->getService(MainCommand::class);
            $application->add( $this->subject);
            $this->application = $application;
        }
    
        /**
         * Tests command in $subject command,
         *
         * @return void
         */
        public function testCommand()
        {
            $command = $this->application->find( $this->subject->getName());
            $commandTester = new CommandTester($command);
            $commandTester->execute(
                [
                    'command' => $this->subject->getName()
                ]
            );
    
            $this->stringContains($commandTester->getDisplay(true), 'finished');
            $this->assertEquals($commandTester->getStatusCode(), 0);
        }
    }
    

    【讨论】:

      【解决方案2】:

      我有同样的问题,但我解决了。

      我有基类:

      class TestCase extends WebTestCase
      {
          /** @var Application */
          private $application;
          private $mailServiceMock;
      
          protected function setMailService(MailService $mailServiceMock): void
          {
              $this->mailServiceMock = $mailServiceMock;
          }
      
          protected function getApplication(): Application
          {
              static::bootKernel();
              static::$kernel->getContainer()->get('test.client');
              $this->setMocks();
              $this->application = new Application(static::$kernel);
              $this->application->setCatchExceptions(false);
              $this->application->setAutoExit(false);
              return $this->application;
          }
      
          protected function execute(string $action, array $arguments = [], array $inputs = []): CommandTester
          {
              $tester = (new CommandTester($this->getApplication()->find($action)))->setInputs($inputs);
              $tester->execute($arguments);
              return $tester;
          }
      
          private function setMocks(): void
          {
              if ($this->mailServiceMock) {
                  static::$kernel->getContainer()->set('mail', $this->mailServiceMock);
              }
          }
      }
      

      和测试类

      class SendEmailCommandTest extends TestCase
      {
          public function testExecuteSendingError(): void
          {
              $mailServiceMock = $this->getMockBuilder(MailService::class)->disableOriginalConstructor()
              ->setMethods(['sendEmail'])->getMock();
              $mailServiceMock->method('sendEmail')->willThrowException(new \Exception());
              $this->setMailService($mailServiceMock);
              $tester = $this->execute(SendEmailCommand::COMMAND_NAME, self::NORMAL_PAYLOAD);
              $this->assertEquals(SendEmailCommand::STATUS_CODE_EMAIL_SENDING_ERROR, $tester->getStatusCode());
          }
      }
      

      如您所见,我在启动内核后立即设置了mail 服务。

      在这里你可以看到我的services.yaml

      services:
        mail.command.send.email:
          autowire: true
          class: App\Command\SendEmailCommand
          arguments: ["@logger", "@mail"]
          tags:
            - {name: console.command, command: "mail:send.email"}
      

      【讨论】:

        【解决方案3】:

        如果您将命令创建为 service,其中框架会自动(自动装配或使用显式参数列表)将服务注入构造函数(提示:在命令中,调用 parent::__construct()),然后测试可以创建与参数类型提示(或接口)匹配的任何模拟或其他替换服务。

        【讨论】:

        • 在测试中究竟在哪里调用了构造函数?如果我们不明确地做一个“新”,那么我们必须在哪里设置模拟以便内核启动或任何可以进行正确的自动装配?
        • 如果命令被设置为服务(它应该是,使用 Symfony 4.0+),那么你可能(/应该)根本不需要服务容器。您可以从头开始创建对象 - 使用 new,以及根据需要使用任何真实或模拟参数。这可能发生在测试 setup() 或单个测试(或两者的混合)中
        • 啊完美。是的,我将命令视为服务,并在构造函数中注入依赖项。所以我们甚至可以从 TestCase 而不是 KernelTestCase 继承,并且完全跳过引导 $kernel 和创建 $application。更接近于对命令进行单元测试,远离对它进行功能测试。测试了它,它的工作原理!完美的!!谢谢
        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 2021-12-23
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2019-04-13
        相关资源
        最近更新 更多