php - 模拟 Controller 从 WebTestCase 调用的服务

标签 php symfony phpunit

我有一个使用 Symfony2 编写的 API,我正在尝试为其编写事后测试。其中一个端点使用电子邮件服务向用户发送密码重置电子邮件。我想模拟此服务,以便我可以检查是否向该服务发送了正确的信息,并防止实际发送电子邮件。

这是我要测试的路线:

/**
 * @Route("/me/password/resets")
 * @Method({"POST"})
 */
public function requestResetAction(Request $request)
{
    $userRepository = $this->get('app.repository.user_repository');
    $userPasswordResetRepository = $this->get('app.repository.user_password_reset_repository');
    $emailService = $this->get('app.service.email_service');
    $authenticationLimitsService = $this->get('app.service.authentication_limits_service');
    $now = new \DateTime();
    $requestParams = $this->getRequestParams($request);
    if (empty($requestParams->username)) {
        throw new BadRequestHttpException("username parameter is missing");
    }
    $user = $userRepository->findOneByUsername($requestParams->username);
    if ($user) {
        if ($authenticationLimitsService->isUserBanned($user, $now)) {
            throw new BadRequestHttpException("User temporarily banned because of repeated authentication failures");
        }
        $userPasswordResetRepository->deleteAllForUser($user);
        $reset = $userPasswordResetRepository->createForUser($user);
        $userPasswordResetRepository->saveUserPasswordReset($reset);
        $authenticationLimitsService->logUserAction($user, UserAuthenticationLog::ACTION_PASSWORD_RESET, $now);
        $emailService->sendPasswordResetEmail($user, $reset);
    }
    // We return 201 Created for every request so that we don't accidently
    // leak the existence of usernames
    return $this->jsonResponse("Created", $code=201);
}

然后我有一个 ApiTestCase 类,它扩展了 Symfony WebTestCase 以提供辅助方法。此类包含一个尝试模拟电子邮件服务的 setup 方法:

class ApiTestCase extends WebTestCase {

    public function setup() {
        $this->client = static::createClient(array(
            'environment' => 'test'
        ));
        $mockEmailService = $this->getMockBuilder(EmailService::class)
            ->disableOriginalConstructor()
            ->getMock();
        $this->mockEmailService = $mockEmailService;
    }

然后在我的实际测试用例中,我试图做这样的事情:

class CreatePasswordResetTest extends ApiTestCase {

    public function testSendsEmail() {
        $this->mockEmailService->expects($this->once())
             ->method('sendPasswordResetEmail');
        $this->post(
            "/me/password/resets",
            array(),
            array("username" => $this->user->getUsername())
        );
    }

}

所以现在的诀窍是让 Controller 使用电子邮件服务的模拟版本。我已经阅读了几种不同的方法来实现这一目标,但到目前为止我运气不佳。

方法一:使用container->set()

参见 How to mock Symfony 2 service in a functional test?

setup() 方法中,告诉容器在请求电子邮件服务时应该返回什么:

static::$kernel->getContainer()->set('app.service.email_service', $this->mockEmailService);
# or
$this->client->getContainer()->set('app.service.email_service', $this->mockEmailService);

这根本不会影响 Controller 。它仍然调用原始服务。我看到的一些文章提到模拟服务在一次调用后被“重置”。我什至没有看到我的第一个电话被 mock ,所以我不确定这个问题是否影响了我。

我应该在另一个容器上调用 set 吗?

还是我模拟该服务为时已晚?

方法二:AppTestKernel

参见:http://blog.lyrixx.info/2013/04/12/symfony2-how-to-mock-services-during-functional-tests.html 请参阅:Symfony2 phpunit functional test custom user authentication fails after redirect (session related)

当涉及到 PHP 和 Symfony2 时,这让我无法理解(我不是真正的 PHP 开发人员)。

目标似乎是更改网站的某种基础类,以允许在请求的早期注入(inject)我的模拟服务。

我有一个新的 AppTestKernel:

<?php
// app/AppTestKernel.php
require_once __DIR__.'/AppKernel.php';
class AppTestKernel extends AppKernel
{
    private $kernelModifier = null;

    public function boot()
    {
        parent::boot();
        if ($kernelModifier = $this->kernelModifier) {
            $kernelModifier($this);
            $this->kernelModifier = null;
        };
    }

    public function setKernelModifier(\Closure $kernelModifier)
    {
        $this->kernelModifier = $kernelModifier;

        // We force the kernel to shutdown to be sure the next request will boot it
        $this->shutdown();
    }
}

还有我的 ApiTestCase 中的一个新方法:

// https://stackoverflow.com/a/19705215
    protected static function getKernelClass(){
        $dir = isset($_SERVER['KERNEL_DIR']) ? $_SERVER['KERNEL_DIR'] : static::getPhpUnitXmlDir();
        $finder = new Finder();
        $finder->name('*TestKernel.php')->depth(0)->in($dir);
        $results = iterator_to_array($finder);
        if (!count($results)) {
            throw new \RuntimeException('Either set KERNEL_DIR in your phpunit.xml according to http://symfony.com/doc/current/book/testing.html#your-first-functional-test or override the WebTestCase::createKernel() method.');
        }
        $file = current($results);
        $class = $file->getBasename('.php');
        require_once $file;
        return $class;
    }

然后我更改我的 setup() 以使用内核修饰符:

public function setup() {
        ...
    $mockEmailService = $this->getMockBuilder(EmailService::class)
        ->disableOriginalConstructor()
        ->getMock();
    static::$kernel->setKernelModifier(function($kernel) use ($mockEmailService) {
        $kernel->getContainer()->set('app.service.email_service', $mockEmailService);
    });
    $this->mockEmailService = $mockEmailService;
}

这行得通!但是,当我尝试执行以下操作时,我现在无法在其他测试中访问容器:

$c = $this->client->getKernel()->getContainer();
$repo = $c->get('app.repository.user_password_reset_repository');
$resets = $repo->findByUser($user);

getContainer() 方法返回 null。

我应该以不同的方式使用容器吗?

我需要将容器注入(inject)新内核吗?它扩展了原始内核,所以我真的不知道为什么/它在容器方面有什么不同。

方法三:替换config_test.yml中的服务

参见:Symfony/PHPUnit mock services

此方法要求我编写一个覆盖电子邮件服务的新服务类。像这样编写固定模拟类似乎不如常规动态模拟有用。如何测试某些方法是否已使用某些参数调用?

方法 4:在测试中设置所有内容

按照@Matteo 的建议,我编写了一个测试来执行此操作:

public function testSendsEmail() {
    $mockEmailService = $this->getMockBuilder(EmailService::class)
        ->disableOriginalConstructor()
        ->getMock();
    $mockEmailService->expects($this->once())
         ->method('sendPasswordResetEmail');
    static::$kernel->getContainer()->set('app.service.email_service', $mockEmailService);
    $this->client->getContainer()->set('app.service.email_service', $mockEmailService);
    $this->post(
        "/me/password/resets",
        array(),
        array("username" => $this->user->getUsername())
    );
}

此测试失败,因为未调用预期的方法 sendPasswordResetEmail:

There was 1 failure:

1) Tests\Integration\Api\MePassword\CreatePasswordResetTest::testSendsEmail
Expectation failed for method name is equal to <string:sendPasswordResetEmail> when invoked 1 time(s).
Method was expected to be called 1 times, actually called 0 times.

最佳答案

多亏了 Cered 的建议,我已经设法让一些东西起作用,可以测试我期望发送的电子邮件是否确实存在。我无法真正让模拟工作,所以我有点不愿意将其标记为“答案”。

这是一个检查电子邮件是否已发送的测试:

public function testSendsEmail() {
    $this->client->enableProfiler();
    $this->post(
        "/me/password/resets",
        array(),
        array("username" => $this->user->getUsername())
    );
    $mailCollector = $this->client->getProfile()->getCollector('swiftmailer');
    $this->assertEquals(1, $mailCollector->getMessageCount());
    $collectedMessages = $mailCollector->getMessages();
    $message = $collectedMessages[0];

    $this->assertInstanceOf('Swift_Message', $message);
    $this->assertEquals('Reset your password', $message->getSubject());
    $this->assertEquals('info@example.com', key($message->getFrom()));
    $this->assertEquals($this->user->getEmail(), key($message->getTo()));
    $this->assertContains(
        'This link is valid for 24 hours only.',
        $message->getBody()
    );
    $resets = $this->getResets($this->user);
    $this->assertContains(
        $resets[0]->getToken(),
        $message->getBody()
    );
}

它通过启用 Symfony 分析器和检查 swiftmailer 服务来工作。它记录在这里:http://symfony.com/doc/current/email/testing.html

关于php - 模拟 Controller 从 WebTestCase 调用的服务,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/38896165/

相关文章:

php - 一次在 PHPunit 中运行所有测试,但彼此隔离

php - 当我尝试从 PhpStorm 运行 PHPUnit 时出错

PHP preg_replace 返回编译失败 : PCRE does not support

regex - Symfony2.0 使用正则表达式验证

php - 为什么 PHPUnit 在模拟不存在的方法时默默地不返回任何内容?

javascript - 使用多个独立的 gulp 文件构建不同的包

symfony - 如何使用 Twig 动态更改页面标题

php - Drupal - Webform 元素主题

php - 为什么 Symfony 2 在我的环境中响应极其缓慢?

php - 多个文件上传的 Symfony 验证