MageTestFest – Eine einzigartige Konferenz und einmalige Gelegenheit

Wenn Du dich für Software Testing und/oder Magento-Entwicklung interessierst, kommt das für interessanteste Event des Jahres näher: MageTestFest in Amerfoort (NL)!

  • 15. Nov: Workshop PHPUnit (Sebastian Bergmann)
  • 16. Nov: Workshop DDD (Mathias Verraes)
  • 17. Nov: Konferenz-Tag (Agenda)
  • 18. Nov: Magento Contribution Day (Hackathon)

Continue reading “MageTestFest – Eine einzigartige Konferenz und einmalige Gelegenheit”

Admin Session mocken mit EcomDev_PHPUnit in Magento Enterprise

In Integrationstests mit EcomDev_PHPUnit_Test_Case_Controller gibt es eine praktische Helper-Methode adminSession(), um Requests im Magento Backend zu testen. Mit Magento Enterprise kann es da allerdings zu dieser Fehlermeldung kommen:

Exception: Warning: in_array() expects parameter 2 to be array, null given in /home/vagrant/mwdental/www/app/code/core/Enterprise/AdminGws/Model/Role.php on line 83

So gesehen in Magento EE 1.14.1.

Ohne Details zu den Enterprise-Modulen preiszugeben, hier eine Lösung in der der schuldige Observer gemockt wird:

$adminObserverMock = $this->getModelMock(
    'enterprise_admingws/observer',
    array('adminControllerPredispatch'));
$adminObserverMock->expects($this->any())
    ->method('adminControllerPredispatch')
    ->will($this->returnSelf());
$this->replaceByMock('singleton', 'enterprise_admingws/observer',
    $adminObserverMock);
$this->adminSession();

PHP: header() mocken, um Controller zu Unit-testen

2011 habe ich eine Technik vorgestellt, Funktionen in PHP Unit Tests zu mocken, die sich die Regeln für Namensauflösung von PHP namespaces zunutze macht. Er kann hier gelesen werden:

Es macht mich stolz, dass der große Matthew Weier O’Phinney 1 nun die selbe Technik beschreibt, um Code zu testen, der Ausgaben erzeugt, insbesondere Code, der HTTP Header mit der Core-Funktion header() sendet. Lies mehr dazu in seinem Artikel:

Meiner Meinung nach ist das ein großartiges Beispiel dafür, wie nützlich diese Methode ist. “Headers already sent” Fehler in Unit Tests können einen in den Wahnsinn treiben. Unglücklicherweise gibt es immer noch viele Anwendungen, die keine Namespaces nutzen (*hust* Magento *hust*), dort funktioniert die Methode nicht.

Notes:

  1. für die, die ihn nicht kennen: Er ist Zend Framework Project Lead und Du solltest seinem Blog auf http://mwop.net/blog.html folgen!

Magento Testing: Formulare mit einem Klick ausfüllen

Wer kennt es nicht: Beim manuellen Testen von Funktionen wie dem Checkout als Gast müssen jedes Mal mühsam alle Formularfelder ausgefüllt werden. Mit Chrome Autocomplete oder “test”, Strg + C, Strg + V geht das noch einigermaßen schnell, nervt aber immer noch ein wenig. Und was, wenn die Testdaten noch einigermaßen sinnvoll sein sollen und nicht jedes Mal gleich?

Inspiriert von diesem Artikel auf css-tricks.com habe ich ein kleines Magento-Modul entwickelt, das das Ausfüllen von Magento-Formularen mit Dummy-Daten mit einem Mausklick ermöglicht. Aktuell implementiert ist es für Rechnungsadresse und Versandadresse.

Hier geht es zum Github-Repository: SSE_FormFiller

Und so sieht es aus:

Screenshot: Formulare im Checkout

Konfiguration

Continue reading “Magento Testing: Formulare mit einem Klick ausfüllen”

Unit-testen von generierten PDFs mit PHPUnit und PDFBox

Zu den Features, die sich nicht leicht mit Unit Tests prüfen lassen gehört das generieren von PDF-Dateien.

Hilfreich dazu ist das Kommandozeilen-Tool PDFBox mit der Option ExtractText:

PDF

This application will extract all text from the given PDF document.

Das erlaubt uns schon mal, den Text-Inhalt des Dokuments zu testen oder nach bestimmten Strings darin zu suchen.

Interessant wird es mit der Option -html, die das PDF zu HTML konvertiert. Damit sind auch Struktur und Formatierungen ansatzweise testbar.

Leider arbeitet das Tool nicht mit Streams, wir müssen also temporäre Dateien nutzen. Ein einfaches Beispiel für eine Funktion, die ein PDF Dokument als String entgegennimmt, mit PdfBox in HTML konvertiert und das HTML als String zurückgibt:

/**
 * @var string $streamIn binary string with generated PDF
 * @return string HTML string
 */
function htmlFromPdf($streamIn)
{
  $pdf = tempnam();
  file_put_contents($pdf, $streamIn);
  $txt = tempnam();
  exec('java -jar pdfbox-app-x.y.z.jar ExtractText -encoding UTF-8 -html ' . $pdf . ' ' . $txt);
  $streamOut = file_get_contents($txt);
  unlink($pdf);
  unlink($txt);
  return $streamOut;
}

Für Regressionstests oder beim Refaktorisieren kann es mitunter ausreichen, zu testen, dass das generierte PDF sich gegenüber dem Referenz-Dokument nicht geändert hat. Hier bietet sich ein Hash an, die Datei selbst ist aber – vermutlich aufgrund von Timestamps – nicht bei jedem Durchlauf exakt gleich. Ein Hash des nach HTML konvertierten Dokuments dagegen genügt:

        // In PHPUnit test case:
        $converter = new PdfBox();
        $html = $converter->htmlFromPdfStream($pdf);
        $this->assertEquals('336edd9ee49b57e6dba5dc04602765056ce05b91', sha1($html), 'Hash of PDF content');

Continue reading “Unit-testen von generierten PDFs mit PHPUnit und PDFBox”

Magento: Integrationstest für Checkout

Gelegentlich funktioniert die Weiterleitung beim One Page Checkout nicht mehr, nachdem irgendein neues Modul Debug-Ausgaben macht oder Fehler wirft. Exceptions beim Versand der Bestätigungs-Mail sind auch eine häufige Ursache.
Das ist ein ziemlich gravierender Fehler, der aber leicht unbemerkt bleibt. Nachdem ich das nun ein paar Mal hatte, war klar: Der Checkout muss beim Regressionstest automatisch getestet werden, so dass ich über so einen Fehler rechtzeitig informiert werde. Alistair Steads hat glücklicherweise beispielhaft einen Checkout Integration Test online gestellt, seine Bibliothek Mage-Test benutze ich allerdings nicht mehr, habe den Test also für EcomDev PHPUnit 0.2 umgeschrieben. Am Code der Test-Methode ändert sich zunächst nicht viel, nur ein paar Methoden heißen anders. Das Setup ist allerdings mal wieder etwas kniffelig, daher will ich es hier mal etwas näher beleuchten. Links zum fertigen Test Case folgen unten.

Zunächst mal wird mindestens ein Produkt für den Warenkorb benötigt, die Unit Tests laufen ja auf ihrer eigenen Datenbank, die für jeden Test mittels Fixtures entsprechend vorbereitet werden muss (mehr dazu im EcomDev PHPUnit Manual). Hier die Minimal-Daten für unsere Zwecke:

eav:
  catalog_product:
    -
      entity_id: 1
      stock:
        qty: 100
        is_in_stock: 1
      website_ids:
        - default
      sku: test
      name: Test
      status: 1 # enabled
      type_id: simple
      price: 1.00

Um sicherzustellen, dass die Bestellung akzeptiert wird, sollte das Empfängerland explizit erlaubt sein und die Funktion “Terms & Conditions” deaktiviert werden:

config:
  default/general/country/allow: DE,GB
  stores/default/checkout/options/enable_agreements: 0

Außerdem muss die Zahlungsmethode konfiguriert sein. Ich habe mich dazu entschieden, für den Test “Scheck / Zahlungsanweisung” statt Paypal zu verwenden, die Standard-Methode, die keine Konfiguration benötigt. Wenn explizit verschiedene Zahlungsmethoden getestet werden sollen, ist ein aufwändigeres Setup nötig, das macht meiner Meinung nach in der isolierten Testumgebung aber wenig Sinn.

Ganz wichtig ist, dass nachdem der Warenkorb gefüllt wurde, der Session-Wert cart_was_updated auf false gesetzt wird, ansonsten bricht der Test mit einer wenig aussagekräftigen “Headers already sent”-Exception ab (mehr dazu unten).

    protected function _fixCheckout()
    {
        Mage::getSingleton('checkout/session')->setCartWasUpdated(false);
    }

Eine weitere Überraschung gab es, nachdem der Test das erste Mal erfolgreich durchlief und beim nächsten Mal ohne ersichtlichen Grund einen Fehler in der JSON-Ausgabe meldete. Gut, wenn man Exception-Logging aktiviert hat: Grund war eine PDOException aufgrund doppeltem Schlüssel in der Order-Tabelle. Das interessante: Das EcomDev Test-Framework räumt die Datenbank eigentlich nach jedem Testlauf ab (wenn nicht gerade ein Fatal Error der Ausführung der tearDown-Methoden zuvorkommt) aber für die beim Checkout generierte Bestellung (und nur die) funktionierte das nicht. Als Ursache vermute ich die zusätzliche Kontrolle im Order-Model, die verhindern soll, dass Bestellungen von irgendeinem Modul außerhalb des Admin-Bereichs gelöscht werden können. Jedenfalls hat das setzen des isSecureArea-Flags geholfen, generierte Bestellungen wieder zu löschen:

    /**
     * Deletes any created order
     * 
     * @return void
     */
    protected function _deleteOrders()
    {
        Mage::register('isSecureArea', true);
        /* @var $orders Mage_Sales_Model_Mysql4_Order_Collection */
        $orders = Mage::getModel('sales/order')->getCollection()->load();
        $orders->walk('delete');
    }

Dinge, die ich beim Debuggen gelernt habe

  1. Der One Page Checkout Controller reagiert auf diverse Fehler mit einem 403 Session Expired Header, was gemeinerweise vom Test-Framework nur mit einer “Headers already sent”-Exception beim verarbeiten des Response-Objekts quittiert wird. Den Fehler habe ich natürlich erst bei mir selbst gesucht, aber das Response-Objekt wurde überall sauber resettet.
    Zu den verursachenden Fehlern dieses Headers gehören ein leerer Warenkorb und jegliche Fehler aus dem Quote-Model (siehe dazu die Methode OnepageController::­_expireAjax()). Um so etwas zu finden, empfiehlt es sich, die Header direkt an der Stelle, wo die Exception geworfen wird zu analysieren und dann im Magento-Source nach dem Header-Text zu suchen.
  2. Um aufzudecken, welche Exceptions zu (oft wenig hilfreichen) Magento-Fehlermeldungen geführt haben, ist das Exception-Log bei Integrationstests unentbehrlich. Die Fixture dazu sieht so aus:
     stores/default/dev/log/active: 1
      stores/default/dev/log/file: tests-system.log
      stores/default/dev/log/exception_file: tests-exception.log
    

Quelltext

Wie versprochen, stehen Fixture & Test Case als github:gist online zur Verfügung.