• Willkommen im Zend Framework Forum

    ZF1 Zend Framework 1 + ZF2 Zend Framework 2

    Das Zend Framework Forum ist seit 2006 die erste Anlaufstelle für Zend Framework Entwickler in Deutschland. Mit über 70.000 Beiträgen und einer steigenden Nutzerzahl bietet das Forum hilfreiche Themen und ZF-Tutorials für professionelle Entwickler, fortgeschrittene Programmierer sowie Zend Framework Einsteiger.
    Wenn dies Dein erster Besuch in der Zend Framework Community ist, lies bitte zuerst die Hilfe - FAQ durch. Du musst Dich registrieren, bevor Du Beiträge verfassen kannst. Klicke oben auf 'Registrieren', um die Registrierung zu starten. Du kannst auch jetzt schon Beiträge lesen. Hier im Forum findest Du die Zend Framework Hilfe, die Du suchst!

    Grüße an alle Zend Framework Entwickler. Das Team vom Zend Framework Forum!

    Drupal Agentur

Quote bei Unit-Tests?

Es gibt ja immer wieder Fälle, die man nicht überprüft bekommt, so ist es zu mindestens bei mir.

Wie geht ihr damit um? Habt ihr immer 100% Testquote? Oder bei welcher Quote sagt ihr, dass passt?

z. B.

Ich habe ein Controller-Plugin für den Versand von Emails geschrieben.
PHP:
class SendEmail extends AbstractPlugin
  {
    public function __invoke($sSubject, $sMessage, $aTo = NULL, $sFrom = NULL)
    {
      $aConfig = $this->getController()->getServiceLocator()->get('Config');

      if (empty($aConfig['email']))
      {
        return false;
      }

....
       $oMessage = new Message();

....

       if ($oMessage->isValid())
      {
...
          return true;
       }

       return false;
   }
Den If-Fall bekomme ich nicht überprüft, da ich ja die Config nicht überschreiben darf. Es könnte ja theoretisch vorkommen, dass man vergisst, diese Config nicht auf dem Live-Server zu kopieren. Deswegen habe ich den IF-Fall eingebaut. Aber überprüft bekomme ich das jetzt nicht. Oder?

Das gleiche Problem habe ich auch mit dem zweiten IF, das bekomme ich überprüft, aber an das letzte Return komme ich dann trotzdem nie hin. Wenn der Code oben schon korrekt ist. Was macht ihr hier?
 

[-UFO-]Melkor

New member
Es gibt ja immer wieder Fälle, die man nicht überprüft bekommt, so ist es zu mindestens bei mir.
Wenn man etwas nicht getestet, ist das meist ein Hinweis darauf, dass der Code an dieser Stelle überarbeitet werden sollte.

Wie geht ihr damit um? Habt ihr immer 100% Testquote? Oder bei welcher Quote sagt ihr, dass passt?
Das hängt auch von deinem Code ab. Je schwieriger und wichtiger der Code ist, umso wichtiger ist es auch, dass er getestet ist. Bei Settern und Gettern kann nicht so viel kaputt gehen, wie bei komplexen Business-Prozessen. Eine Persistenz-Schicht, von der ein großer Teil der gesamten Anwendung abhängt, ist wichtiger als Funktionen, die nur für eine Hilfsseite für den Programmierer Anwendung finden. Es reicht nicht, einfach eine bestimmte Quote zu erfüllen, sondern du solltest schon auch einen Blick auf die Komplexität und die Wichtigkeit werfen.

Den If-Fall bekomme ich nicht überprüft, da ich ja die Config nicht überschreiben darf. Es könnte ja theoretisch vorkommen, dass man vergisst, diese Config nicht auf dem Live-Server zu kopieren. Deswegen habe ich den IF-Fall eingebaut. Aber überprüft bekomme ich das jetzt nicht. Oder?

Das gleiche Problem habe ich auch mit dem zweiten IF, das bekomme ich überprüft, aber an das letzte Return komme ich dann trotzdem nie hin. Wenn der Code oben schon korrekt ist. Was macht ihr hier?
Das, was man bei jedem Unit-Test machen sollte: Die Abhängigkeiten durch Mocks ersetzen. Du willst ja bei einem Unit-Test nicht die gesamte Anwendung inklusive ihrer Konfiguration testen, sondern die einzelne Komponente („Unit“) unabhängig von anderen Komponenten auf ihre Funktion hin überprüfen.
 
Kannst du mir ein Beispiel für das Mock geben? Ich habe dies noch nie gemacht.

Wie kann ich das obige Beispiel Mocken? So dass ich ein leeres Config-Objekt für Email habe?

PHP:
class SendEmailTest extends BGAbstractTestCase
  {
    public function testNoEmailConfig()
    {
      $oMock = $this->getMock('Module\Controller\Plugin\SendEmail', array('__invoke'));
      
      ????
    }
  }
 

[-UFO-]Melkor

New member
PHP:
public function testShouldReturnFalseOnMissingConfig()
{
    $config   = array();
    $services = $this->getMockForAbstractClass('Zend\\ServiceManager\\ServiceLocatorInterface');
    $services->expects($this->any())->method('get')->with('Config')->will($this->returnValue($config));

    $controller = $this->getMock('Zend\\Mvc\\Controller\\AbstractController');
    $controller->expects($this->any())->method('getServiceLocator')->will($this->returnValue($services));

    $plugin = new \Module\Controller\Plugin\SendEmail();
    $plugin->setController($controller);
    $this->assertFalse($plugin->__invoke('foo', 'bar'));
}
Und wenn du dir jetzt mal anschaust, wie viel Arbeit es ist, die ganzen Mocks zu erstellen, dann kannst du dir nun überlegen, ob du die Config nicht lieber gleich mit einer Factory an den Konstructor übergibst.
 
Ja, so komme ich schon weiter.

Gibt es zum Thema irgendwo eine gute Anleitung, wo ich das Nachlesen kann? Mir sind die ganze Befehle noch nicht ganz klar.
 
Danke!

Ich komme mit einem kleinen Problem nicht weiter.

Ich habe in der SendEmail, die Geschichte mit dem Transport in eine Factory ausgelagert:
PHP:
if ($oMessage->isValid())
      {
        $oTransport = $this->getController()->getServiceLocator()->get('mail.transport'); /* @var $oTransport \Zend\Mail\Transport\Smtp */
        $oTransport->send($oMessage);

        return true;
      }
So dass ich jetzt das Config & mail.transport nachladen muss. Wie kann ich das machen?

Bisheriger Test-Code:
PHP:
$oController = $this->getMock('Zend\\Mvc\\Controller\\AbstractController');

      $aConfig   = array(...);

      $oTransport = $this->getMockForAbstractClass('Zend\\ServiceManager\\ServiceLocatorInterface');
      $oTransport->expects($this->any())
                 ->method('get')
                 ->with('mail.transport')
                 ->will($this->returnValue($aConfig));

      $oController->expects($this->any())
                  ->method('getServiceLocator')
                  ->will($this->returnValue($oTransport));

      $oConfig = $this->getMockForAbstractClass('Zend\\ServiceManager\\ServiceLocatorInterface');
      $oConfig->expects($this->any())
              ->method('get')
              ->with('Config')
              ->will($this->returnValue($aConfig));

      $oController->expects($this->any())
                  ->method('getServiceLocator')
                  ->will($this->returnValueMap($oConfig));

      $oPlugin = new SendEmail();
      $oPlugin->setController($oController);

      $this->assertFalse($oPlugin->__invoke('Betreff', 'Message'));
Es geht entweder das eine oder das andere, aber nicht beides Zusammen.

MailTransportFactory
PHP:
class MailTransportFactory implements FactoryInterface
  {
    public function createService(ServiceLocatorInterface $oServiceLocator)
    {
      $aConfig = $oServiceLocator->get('Config');
      $oTransport = new Smtp();
      $oTransport->setOptions(new SmtpOptions($aConfig['mail']['transport']['options']));

      return $oTransport;
    }
  }
Zusatzfrage: Wie kann ich die vorhandene Config einlesen, die schon gespeichert sind im Verzeichnis /config/autoload?
 
Zuletzt bearbeitet:
Bin ein Stück weiter, aber leider noch nicht am Ziel.

Ich erhalte immer noch die folgende Fehlermeldung von PHPunit:
Code:
1) ModuleTest\Controller\Plugin\SendEmailTest::testCall
Expectation failed for method name is equal to <string:get> when invoked zero or more times
Parameter 0 for invocation Zend\ServiceManager\ServiceLocatorInterface::get('mail.transport') does not match expected value.
Failed asserting that two strings are equal.
--- Expected
+++ Actual
@@ @@
-'Config'
+'mail.transport'
Was ist an dem folgenden Code noch falsch?
PHP:
      $oConfig = $this->getMockForAbstractClass('Zend\\ServiceManager\\ServiceLocatorInterface');
      $oConfig->expects($this->any())
              ->method('get')
              ->with('Config')
              ->will($this->returnValue($aConfig));

      $oTransport = $this->getMockForAbstractClass('Zend\\ServiceManager\\ServiceLocatorInterface');
      $oTransport->expects($this->any())
                 ->method('get')
                 ->with('mail.transport')
                 ->will($this->returnValue($aConfig));

      $aMap = array(array($oConfig), array($oTransport));

      $oController = $this->getMock('Zend\\Mvc\\Controller\\AbstractController');
      $oController->expects($this->any())
                  ->method('getServiceLocator')
                  ->will($this->returnValueMap($aMap));

      $oPlugin = new SendEmail();
      $oPlugin->setController($oController);

      $this->assertFalse($oPlugin->__invoke('Betreff', 'Message'));
Ich habe das returnValueMap eingebaut, aber egal, in welcher Reihenfolge ich die Objekte in $aMap ablege wird mir immer eines als Fehlerhaft angezeigt. Wo liegt mein Fehler?
 

[-UFO-]Melkor

New member
Du fügst die ValueMap an der falschen Stelle ein ;)

PHP:
      $aConfig = [/*…*/];
      $oTransport = $this->getMock('...\\Smtp');

      $services = $this->getMockForAbstractClass('Zend\\ServiceManager\\ServiceLocatorInterface');
      $services->expects($this->any())
              ->method('get')
              ->will($this->returnValueMap([['Config', $aConfig], ['mail.transport', $oTransport]));

      $oController = $this->getMock('Zend\\Mvc\\Controller\\AbstractController');
      $oController->expects($this->any())
                  ->method('getServiceLocator')
                  ->will($this->returnValue($services));

      $oPlugin = new SendEmail();
      $oPlugin->setController($oController);

      $this->assertFalse($oPlugin->__invoke('Betreff', 'Message'));
 
Ich habe das ganze Überprüft, dabei habe ich festgestellt, dass noch nicht alles so funktioniert, wie es sein sollte. Habs aber jetzt fast hin bekommen.

Ich erhalte aber jetzt im Testlauf im Controller-Plugin
PHP:
...
      if ($oMessage->isValid())
      {
        $oTransport = $this->getController()->getServiceLocator()->get('mail.transport'); /* @var $oTransport \Zend\Mail\Transport\Smtp */
        $oTransport->send($oMessage);

        return true;
      }
...
eine Fehlermeldung:
Code:
PHP Fatal error:  Call to undefined method Mock_MailTransportFactory_16010cb7::send() in /var/www/NAME/module/MODUL/src/MODUL/Controller/Plugin/SendEmail.php on line 45
Warum findet der Testlauf die send()-Methode nicht?

Wenn ich das Objekt ($oTransport) im Testlauf ausgeben lasse, erhalte ich die folgende Ausgabe:
Code:
.Mock_MailTransportFactory_bcc5c8f7 Object
(
    [__phpunit_invocationMocker:Mock_MailTransportFactory_bcc5c8f7:private] => 
)
Hast du eine Idee, wo der Fehler liegt?
 
Ich fange mal von vorne an:

MailTransportFactory
PHP:
class MailTransportFactory implements FactoryInterface
  {
    public function createService(ServiceLocatorInterface $oServiceLocator)
    {
      $aConfig = $oServiceLocator->get('Config');
      $oTransport = new Smtp();
      $oTransport->setOptions(new SmtpOptions($aConfig['mail']['transport']['options']));

      return $oTransport;
    }
  }
SendMail.php
PHP:
class SendEmail extends AbstractPlugin
  {
    public function __invoke($sSubject, $sMessage, $aTo = NULL, $sFrom = NULL)
    {
      $aConfig = $this->getController()->getServiceLocator()->get('Config');

      if (empty($aConfig['mail']))
      {
        return false;
      }

      $sFrom = ($sFrom != NULL)
                  ? $sFrom
                  : $aConfig['mail']['from'];

      $aTo = ($aTo != NULL)
                ? $aTo
                : new Address($aConfig['mail']['admin']);

      $oMessage = new Message();
      $oMessage->setFrom($sFrom);
      $oMessage->addTo($aTo);
      $oMessage->setSubject($sSubject);
      $oMessage->setBody($sMessage);
      $oMessage->setEncoding('UTF-8');
      $oMessage->getHeaders()->addHeaderLine('content-type', 'text/plain; charset=utf-8');

      $oMessageId = new MessageId();

      $oMessage->getHeaders()->addHeaderLine('Message-ID', $oMessageId->createMessageId());

      if ($oMessage->isValid())
      {
        $oTransport = $this->getController()->getServiceLocator()->get('mail.transport'); /* @var $oTransport \Zend\Mail\Transport\Smtp */
        $oTransport->send($oMessage);

        return true;
      }

      return false;
    }
  }
module.config.php
PHP:
return array(
    'controller_plugins' => array(
      'invokables'=> array(
        'sendEmail' => 'MODUL\Controller\Plugin\SendEmail',
      ),
    ),
    'service_manager' => array(
      'factories' => array(
        'mail.transport' => 'MODUL\Model\MailTransportFactory',
      ),
    ),
  );
SendEmailTest.php
PHP:
class SendEmailTest extends BGAbstractTestCase
  {
    public function testSendEmailSuccess()
    {
      $aConfigGlobal = include Bootstrap::getPath() . 'config/autoload/mail.global.php';
      $aConfigLocal  = include Bootstrap::getPath() . 'config/autoload/local/mail.local.php';

      $aConfig = array_merge_recursive($aConfigGlobal, $aConfigLocal);

      $oTransport = $this->getMock('MODUL\Model\MailTransportFactory');

      $oService = $this->getMockForAbstractClass('Zend\\ServiceManager\\ServiceLocatorInterface');
      $oService->expects($this->any())
               ->method('get')
               ->will($this->returnValueMap([['Config', $aConfig], ['mail.transport', $oTransport]]));

      $oController = $this->getMock('Zend\\Mvc\\Controller\\AbstractController');
      $oController->expects($this->any())
                  ->method('getServiceLocator')
                  ->will($this->returnValue($oService));

      $oPlugin = new SendEmail();
      $oPlugin->setController($oController);

      $this->assertTrue($oPlugin->__invoke('Betreff', 'Message'));
    }
Ich habe am Code, wie du mir diesen Vorgegeben hast nichts geändert. Hast du eine Idee?
 

michl

New member
Du möchtest hier ja nicht die Factory mocken, sondern die SMTP klasse.

PHP:
class SendEmailTest extends BGAbstractTestCase
  {
    public function testSendEmailSuccess()
    {
      $aConfigGlobal = include Bootstrap::getPath() . 'config/autoload/mail.global.php';
      $aConfigLocal  = include Bootstrap::getPath() . 'config/autoload/local/mail.local.php';

      $aConfig = array_merge_recursive($aConfigGlobal, $aConfigLocal);

      $oTransport = $this->getMock('MODUL\Model\Smtp');
      // $oTransport->setOptions([...])

      $oService = $this->getMockForAbstractClass('Zend\\ServiceManager\\ServiceLocatorInterface');
      $oService->expects($this->any())
               ->method('get')
               ->will($this->returnValueMap([['Config', $aConfig], ['mail.transport', $oTransport]]));

      $oController = $this->getMock('Zend\\Mvc\\Controller\\AbstractController');
      $oController->expects($this->any())
                  ->method('getServiceLocator')
                  ->will($this->returnValue($oService));

      $oPlugin = new SendEmail();
      $oPlugin->setController($oController);

      $this->assertTrue($oPlugin->__invoke('Betreff', 'Message'));
    }
Vielleicht kurz zur erläuterung ...
Wenn du dir mail.transport vom ServiceLocator holst, wird deine Factory und dort die createService() methode aufgerufen.
Diese Methode liefert dir dann deine Smtp Instanz zurück. Genau das möchtest du mocken! Die Factory brauchst du hier nicht!
 
Zuletzt bearbeitet:
Daran lag es. Also dann heißt es dann Factories nicht gemockt werden können? Diese müssen manuell im Testcase nachgebaut werden?
 

michl

New member
Daran lag es. Also dann heißt es dann Factories nicht gemockt werden können? Diese müssen manuell im Testcase nachgebaut werden?
Doch, du kannst sie schon mocken. Dann musst du sie allerdings im servicemanager als service via
PHP:
$serviceLocator->setService('name', $service)
registrieren.
Vorher musst du das überschreiben via
PHP:
$serviceLocator->setAllowOverride(true);
aktivieren.

Du hast dann allerdings eine weitere Abhängigkeit geschaffen und das möchtest du ja bei unit tests weitestgehend vermeiden, da die tests nicht voneinander abhängig sein sollen.
 

michl

New member
schreib nen separaten test dafür.

Gibts ja nicht viel zu testen.
Du erwartest eine MODUL\Model\Smtp Instanz ,wenn du die createService() Methode der Factory aufrufst ....
 
Oben