2016-08-11 11 views
1

Ich habe eine API geschrieben mit Symfony2, die ich versuche, Post-hoc-Tests für zu schreiben. Einer der Endpunkte verwendet einen E-Mail-Dienst, um eine E-Mail zum Zurücksetzen des Kennworts an den Benutzer zu senden. Ich möchte diesen Dienst gerne ausspionieren, damit ich überprüfen kann, ob die richtigen Informationen an den Dienst gesendet werden, und auch verhindern, dass eine E-Mail tatsächlich gesendet wird.Mocking einen Service von einem Controller von einem WebTestCase aufgerufen

Hier ist der Weg, den ich zu Test bin versucht:

/** 
* @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); 
} 

ich dann eine ApiTestCase Klasse, die die Symfony WebTestCase erstreckt Methoden Helfer zur Verfügung zu stellen. Diese Klasse enthält eine setup Methode, die den E-Mail-Dienst zu verspotten versucht:

class ApiTestCase extends WebTestCase { 

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

Und dann in meinem eigentlichen Testfall Ich versuche, so etwas zu tun:

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

} 

So, jetzt ist der Trick um den Controller zur Verwendung der verspotteten Version des E-Mail-Dienstes zu veranlassen. Ich habe über verschiedene Wege gelesen, um dies zu erreichen, bisher hatte ich nicht viel Glück.

Methode 1: Verwenden Container-> set()

Siehe How to mock Symfony 2 service in a functional test?

Im setup() Verfahren der Behälter sagen, was es zurückgeben soll, wenn es für den E-Mail-Dienst gefragt wird:

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

Dies wirkt sich nicht auf den Controller aus. Es ruft immer noch den ursprünglichen Dienst an. Einige Erwähnungen haben erwähnt, dass der verspottete Dienst nach einem einzigen Anruf zurückgesetzt wird. Ich sehe meinen ersten Anruf nicht einmal verspottet, also bin ich mir nicht sicher, ob mich dieses Problem schon betrifft.

Gibt es einen anderen Container, den ich anrufen sollte set an?

Oder verspotte ich den Dienst zu spät?

Methode 2: AppTestKernel

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

Dieses mich aus meiner Tiefe zieht, wenn es um PHP und Symfony2 Sachen kommt (ich bin nicht wirklich ein PHP Entwickler).

Das Ziel scheint zu sein, eine Art Basisklasse der Website zu ändern, damit mein Scheindienst sehr früh in die Anfrage eingefügt werden kann.

Ich habe eine neue 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(); 
    } 
} 

Und eine neue Methode in meinem 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; 
    } 

Dann ändere ich meinen setup() den Kernel Modifikator zu verwenden:

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

Das funktioniert! Allerdings kann ich jetzt nicht den Behälter in meinen anderen Tests zugreifen, wenn ich versuche, so etwas zu tun:

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

Die getContainer() Methode gibt null zurück.

Sollte ich den Behälter anders verwenden?

Muss ich den Container in den neuen Kernel injizieren? Es erweitert den ursprünglichen Kernel, so dass ich nicht wirklich weiß, warum/wie es anders ist, wenn es um das Container-Zeug geht.

Methode 3: Ersetzen Sie den Dienst in config_test.yml

See: Symfony/PHPUnit mock services

Diese Methode erfordert, dass ich eine neue Service-Klasse schreiben, die den E-Mail-Dienst außer Kraft setzt. Eine feste Scheinklasse wie diese zu schreiben, scheint weniger nützlich zu sein als ein regulärer dynamischer Schein. Wie kann ich testen, dass bestimmte Methoden mit bestimmten Parametern aufgerufen wurden?

Methode 4: Setup alles innerhalb des Test

Going on @ Matteo Vorschlag schrieb ich einen Test, der das getan hat:

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

Dieser Test, weil die sendPasswordResetEmail erwartete Methode nicht aufgerufen wurde :

+0

Ich habe (im Extremfall) die erste Methode aber nicht in der Setup-Methode, sondern in der Testmethode verwendet. Kannst du es versuchen? – Matteo

+0

Hey Matteo, danke für den Vorschlag. Ich habe es versucht, aber der Test ist fehlgeschlagen. Ich habe meinen Testcode zu der Frage hinzugefügt, damit Sie überprüfen können, ob ich das getan habe, was Sie erwartet haben. Vielen Dank! – WilliamMayor

+0

Ich weiß, dass es dich aus deiner Tiefe reißt, aber dies ist der einzige Ansatz, der für mich funktioniert hat: http://symfony.com/doc/current/email/testing.html Ich würde vorschlagen, den tatsächlichen Code im Beispiel zu probieren zuerst und versuche, es auf deinen Test anzuwenden. Es kann schwierig sein. Komponententests sind viel einfacher. – Cerad

Antwort

0

Dank Cereds Rat habe ich es geschafft, etwas zu arbeiten, das das t testen kann Die E-Mails, die ich erwarte, werden tatsächlich gesendet. Ich habe es nicht geschafft, das Spottwerk zur Arbeit zu bringen, daher zögere ich, dies als "die" Antwort zu bezeichnen.

Hier ist ein Test, der überprüft, ob eine E-Mail gesendet wird:

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('[email protected]', 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() 
    ); 
} 

Es durch die Aktivierung der Symfony-Profiler arbeitet und den Swiftmailer Service Inspektion. Es ist hier dokumentiert: http://symfony.com/doc/current/email/testing.html

Verwandte Themen