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:
nichtpublic 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 :
Ich habe (im Extremfall) die erste Methode aber nicht in der Setup-Methode, sondern in der Testmethode verwendet. Kannst du es versuchen? – Matteo
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
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