Es ist wieder Zeit für eine Lern-Kaffeepause, heute zum Thema Softwaretest! Die fünf Minuten reichen diesmal nur für einen Kurzüberblick, um die verlinkten Artikel alle zu lesen, nehmt euch etwas mehr Zeit.
Magento 2 Integration Tests: @magentoConfigFixture
Ich konnte keine gute Dokumentation zur @magentoConfigFixture
Annotation in Magento 2 Integrationstests finden, also halte ich hier mal meine Zusammenfassung fest, nachdem ich den Core Code inspiziert habe (Magento 2.1.0, Magento\TestFramework\Annotation\ConfigFixture
)
Wie man @magentoConfigFixture nutzt
Standardwert 42 für Konfigurationspfad x/y/z
:
/** * @magentoConfigFixture default/x/y/z 42 */
Store-spezifischer Wert 42 für Konfigurationspfad x/y/z
im Store mit Code store1
/** * @magentoConfigFixture store1_store x/y/z 42 */
Store-spezifischer Wert 42 für Konfigurationspfad x/y/z
in aktuellem Store (also Standard-Store)
/** * @magentoConfigFixture current_store x/y/z 42 */
Das sind alle möglichen Formate. Der erste Parameter muss mit _store
enden oder weggelassen werden. Wenn er weggelassen wird, muss der Pfad mit default/
beginnen, sonst wird er ignoriert.
Implikationen
- Konfigurationswerte können nicht auf Website-Ebene gesetzt werden
- Man sollte nicht “current” als echten Store Code verwenden, sonst kann für diesen Store keine Konfigurations-Fixture genutzt werden
EcomDev PHPUnit Tipp #14
Tipp #14: Registry Fixtures
Wie bereits in Tipp #1 erklärt, können Helpers, Singletons und Registry Werte pro Test zurückgesetzt werden. Die problematischen Singletons etc. zu finden war für mich oft der schwierigste Teil beim Schreiben von Integrationstests mit EcomDev, also habe ich angefangen, sie zentral in einer fixtures/registry.yaml
Datei für alle Tests zu sammeln 1. Lieber eins zu viel zurückgesetzt als eins zu wenig.
Die Datei ist aufgebaut wie folgt:
Continue reading “EcomDev PHPUnit Tipp #14”
Notes:
- Siehe auch YAML Directory Fallback ↩
Die Woche Wochen auf StackExchange #24-29 / 2016
Das wöchentliche Format habe ich ja nicht sehr lange durchgehalten, aber ein paar Beiträge auf Magento StackExchange kamen in den letzten 5 Wochen dann doch zusammen, die vielleicht einen Blick wert sind (und ein neues T-Shirt):
Magento 2
- Eine Produktbild URL in einem eigenen Block auszugeben ist offenbar nicht trivial und kann auf viele Weisen “falsch” gemacht werden: Getting full image URL of product in template
- Wie man eine eigene Search Engine definiert: Magento 2: what is the search_engine.xml? How to declare a new search engine? Zu dem Thema plane ich noch einen eigenen Blog-Post.
- Interessante Entdeckung: Die “area” auf
adminhtml
zu setzen, setzt nicht auch automatisch denadmin
store: Magento 2 integration test in admin context - Und noch eine offene Frage: Right way to implement getExtensionAttributes()
Magento 1
- In How to correctly select the first item from a filtered collection? stelle ich fest, dass man
$collection->getData()
besser nicht benutzen sollte (und den obligatorischen Hinweis zur typischen Performance-Falle) - Eine Kurzübersicht über alle CMS und Email Template Direktiven, oder ein Versuch davon: Why do we have to use “store” for links in CMS like <a href=“{{store url=’home’}}”>home</a>
- Eine Erinnerung, besser
theme.xml
stattlocal.xml
zu benutzen: Is it possible to include a parent local.xml?
- Was ist eigentlich die Einheit des Gewicht-Attributs? What Is The Default Magento Weight Unit And How Can Change It
Die Woche auf StackExchange #9 / 2016
Ich versuche mich an einem neuen wöchentlichen Format im Blog mit einer Zusammenfassung von neuen Fragen und Antworten auf StackExchange rund um PHP und Magento. Mal sehen, was daraus wird, und los geht’s:
Neue Antworten
- In Creating Integration Tests for Magento 2 Modules erkläre ich, wie eigene Integrationstests außerhalb von
dev/tests/integration
platziert werden können. - In Protect a site from wappalyzer untersuche ich, ob es möglich ist eine Magento Seite vor automatischer Erkennung zu schützen, und wie.
- Ein Quickie zu Design Patterns: Data Mapper – should I use dependency injection?
Offene Fragen
- Was ist mit der neuen globalen
__()
Funktion aus Translation Scopes geworden: How does translation scope work in Magento 2? - Ich dachte, ich hätte die Generierung von statischen Dateien in Magento 2 verstanden, aber was machen die Templates da: When and how are phtml templates generated in view_preprocessed?
Zum Thema Magento 2 wird es in den kommenden Wochen sicher noch mehr geben, da ich da gerade tiefer in die Entwicklung einsteige.
EcomDev_PHPUnit Tipp #9
Tipp #9: Checkout Test
Vor 3 Jahren habe ich schonmal einen Artikel dazu geschrieben, wie man einen Integrationstest für den Checkout schreibt. Aber die Praktiken, die ich dort angewendet habe sind heute nicht mehr aktuell und einige der Workarounds sind nicht mehr notwendig. Dieser Beitrag zeigt, was notwendig ist um einen Test zu schreiben, der den Magento Checkout simuliert und nutzt dabei die in Tipp #1 gelernte Technik.
- Da einige Singletons involviert sind, stelle sicher, dass ihr Status zurückgesetzt wird:
/* * @test * @singleton checkout/session * @singleton customer/session * @singleton checkout/cart */
- Es ist ratsam, als erstes den Warenkorb zu besuchen, um die Totals Collection zu triggern. Angenommen, der Kunde hat die ID 1 und einen aktiven Warenkorb (von zuvor im Test in den Warenkorb gelegten Produkten oder einer Quote Fixture), dann beginnen wir mit:
$this->customerSession(1); $this->dispatch('checkout/cart');
- Vor jedem neuen Request, muss das Checkout Session Singleton manuell während des Tests zurückgesetzt werden, sonst wird die Quote nicht neu geladen und kann unter Umständen ganz verloren gehen:
Mage::unregister('_singleton/checkout/session'); $this->customerSession(1); $this->dispatch('checkout/onepage');
- Manchmal möchte man einen Kunden mit aktivem Warenkorb ausloggen. Dazu sind drei Schritte notwendig:
Mage::getSingleton('customer/session')->logout(); Mage::getSingleton('checkout/cart')->unsetData(); $this->guestSession();
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();
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
- 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. - 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.