EcomDev_PHPUnit Tipp #9

Seit Jahren ist das Test-Framework EcomDev_PHPUnit quasi-Standard für Magento Unit Tests. Die aktuelle Version ist 0.3.7 und der letzte Stand der offiziellen Dokumentation ist Version 0.2.0 – seitdem hat sich viel getan, was man leider im Code und GitHub Issues selbst zusammensuchen muss. Diese Serie soll praktische Tipps zur Verwendung sammeln.

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.

  1. Da einige Singletons involviert sind, stelle sicher, dass ihr Status zurückgesetzt wird:
    /*
     * @test
     * @singleton checkout/session
     * @singleton customer/session
     * @singleton checkout/cart
     */
    
  2. 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');
    
  3. 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');
  4. 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();

EcomDev_PHPUnit Tipp #8

Seit Jahren ist das Test-Framework EcomDev_PHPUnit quasi-Standard für Magento Unit Tests. Die aktuelle Version ist 0.3.7 und der letzte Stand der offiziellen Dokumentation ist Version 0.2.0 – seitdem hat sich viel getan, was man leider im Code und GitHub Issues selbst zusammensuchen muss. Diese Serie soll praktische Tipps zur Verwendung sammeln.

Tipp #8: “sort_order is ambiguous” Fehler

Zend_Db_Statement_Exception: SQLSTATE[23000]: Integrity constraint violation: 1052 Column ‘sort_order’ in order clause is ambiguous

Wer bei seinen Tests diesen Fehler von MySQL bekommt, hat wahrscheinlich bei EAV Fixtures das Attribut-Set vergessen. attribute_set_id sollte immer gesetzt sein, auch bei Kunden und Adressen (dort einfach “0” eintragen)

EcomDev_PHPUnit Tipp #7

Seit Jahren ist das Test-Framework EcomDev_PHPUnit quasi-Standard für Magento Unit Tests. Die aktuelle Version ist 0.3.7 und der letzte Stand der offiziellen Dokumentation ist Version 0.2.0 – seitdem hat sich viel getan, was man leider im Code und GitHub Issues selbst zusammensuchen muss. Diese Serie soll praktische Tipps zur Verwendung sammeln.

Tipp #7: YAML Verzeichnis-Fallback

YAML-Dateien für Fixtures, Expectations und Data Providers werden standardmäßig in folgendem Schema erwartet, wobei in diesem Beispiel Importer.php den Test Case enthält und testImport die Test-Methode ist:

│   Importer.php
│
├───Importer
│   ├───expectations
│   │       testImport.yaml
│   │
│   ├───fixtures
│   │       testImport.yaml
│   │
│   └───providers
│           testImport.yaml

Hier die Signatur mit Annotations:

    /**
     * @param string $csv CSV file content
     * @test
     * @loadFixture
     * @loadExpectation
     * @dataProvider dataProvider
     */
    public function testImport($csv)

Anstelle des Methodennamens kann der Dateiname der YAML-Datei jeweils explizit angegeben werden, hier z.B. in einem weiteren Test, der auch fixtures/testImport.yaml benutzt (wie das auch für Data Provider funktioniert, siehe Tipp #3).

    /**
     * @param string $csv CSV file content
     * @test
     * @loadFixture testImport
     * @dataProvider dataProvider
     * @expectedException RuntimeException
     */
    public function testFailingImport($csv)

Aber auch die Verzeichnisstruktur muss nicht in dieser Form genutzt werden. Findet EcomDev_PHPUnit die jeweilige Datei nämlich nicht an der Stelle, greift folgende Fallback-Hierarchie:

  1. Default: siehe oben
  2. Module: Die Verzeichnisse “fixtures”, “expectations” und “providers” werden direkt im “Test” Verzeichnis des Moduls gesucht
  3. Global: Die Verzeichnisse werden in sämtlichen Oberverzeichnissen des Test Cases bis hin zum Magento Root gesucht

Auf diese Weise lassen sich Fixtures etc. Test Case übergreifend verwenden. Ich bevorzuge mittlerweile zumindest für Fixtures in den meisten Fällen die zweite Variante für modulweite Definition.

Die Hierarchie ist übrigens in der config.xml von EcomDev_PHPUnit wie folgt definiert:

<config>
    <phpunit>
        <suite>
            <yaml>
                <model>ecomdev_phpunit/yaml_loader</model>
                <loaders>
                    <default>ecomdev_phpunit/yaml_loader_default</default>
                    <module>ecomdev_phpunit/yaml_loader_module</module>
                    <global>ecomdev_phpunit/yaml_loader_global</global>
                </loaders>
            </yaml>
        </suite>
    </phpunit>
</config>

Diese Loaders sind Models, die von EcomDev_PHPUnit_Model_Yaml_AbstractLoader erben, mit einem eigenen Modul lassen sich hier also theoretisch weitere, eigene Loader hinzufügen und somit beliebige Quellen für die YAML Dateien verwenden.

EcomDev_PHPUnit Tipp #6

Seit Jahren ist das Test-Framework EcomDev_PHPUnit quasi-Standard für Magento Unit Tests. Die aktuelle Version ist 0.3.7 und der letzte Stand der offiziellen Dokumentation ist Version 0.2.0 – seitdem hat sich viel getan, was man leider im Code und GitHub Issues selbst zusammensuchen muss. Diese Serie soll praktische Tipps zur Verwendung sammeln.

Tipp #6: Fixture rollback

Wenn Du Fixtures nutzt, werden die Fixture-Daten aus der Datenbank entfernt, wenn der jeweilige Test durchgelaufen ist. Was dabei zu beachten ist, ist dass es sich hier nicht um einen Transaktions-Rollback handelt, stattdessen werden alle Tabellen die von der Fixture betroffen waren, geleert (truncated).

Das hat einige Implikationen:

  • Man sollte niemals table fixtures mit Tabellen benutzen, die wichtige Core-Daten enthalten, wie z.B. core_config_data oder eav_attribute
  • Daten in Tabellen, die nicht in der Fixture stehen, bleiben bestehen. Man kann das Löschen von Daten, die während dem Test erstellt wurden, mit einer leern Fixture triggern, zum Beispiel für Quotes und Orders:
    tables:
      sales/quote: []
      sales/order: []
    
  • Wenn dieses Verhalten nicht erwünscht ist, sollte man hinter sich selbst aufräumen, also erstellte Daten in einer tearDown Methode löschen.
  • Verlass Dich nicht zu sehr auf Daten, die in der Original-Datenbank vorhanden sind, denn andere Tests können sie mit ihren Fixtures überschreiben und schließlich löschen.
  • Wenn Du shared fixtures mit @loadSharedFixture nutzt, werden alle Daten in den shared fixtures nur einmal erstellt und entfernt. Alle Tabellen, die von einer shared fixture betroffen sind, werden zwischendurch nicht aufgeräumft, auch wenn andere normale Fixtures Daten hinzufügen. Mein Rat: Shared Fixtures nur mit Bedacht einsetzen, besser gar nicht

Comparable Interface für PHP

Vor etwa 5-6 Jahren hatte ich meine “PHP sollte mehr wie Java sein” Phase und habe viel mit Sachen wie String Objekten und Überladen von Methoden experimentiert, was meistens fiese Workarounds erforderte und die meisten Dinge stellten sich auf lange Sicht nicht als sehr praktikabel heraus.

Aber ein Package aus der Zeit gefällt mir immer noch ziemlich gut, und zwar ComparatorTools, was immerhin Platz 2 in den monatlichen PHPclasses.org Innovation Awards belegte. Es stellt Comparable und Comparator Interfaces zur Verfügung sowie Funktionen, analog zu den Core Array-Funktionen, die mit diesen arbeiten können.

Interfaces

Die Interfaces ähneln den entsprechenden Java interfaces, außer dass wir keine Generics in PHP haben, so dass nicht garantiert werden kann, dass die verglichenen Objekte den selben Typ haben. Dies muss zur Laufzeit in der Implementierung geprüft werden, sofern nötig. Ein Exception-Typ für diese Fälle ist verfügbar:

interface Comparable
{
	/**
	 * @param object $object
	 * @return numeric negative value if $this < $object, positive if $this > $object, 0 otherwise (if objects are considered equal)
	 * @throws ComparatorException if objects are not comparable to each other
	 */
	public function compareTo($object);
}
interface Comparator
{
	/**
	 * @param object $object1
	 * @param object $object2
	 * @return numeric Negative value if $object1 < $object2, positive if $object1 > $object2, 0 otherwise
	 * @throws ComparatorException if objects are not comparable to each other
	 */
	public function compare($object1, $object2);
}

Continue reading “Comparable Interface für PHP”

EcomDev_PHPUnit Tipp #5

Seit Jahren ist das Test-Framework EcomDev_PHPUnit quasi-Standard für Magento Unit Tests. Die aktuelle Version ist 0.3.7 und der letzte Stand der offiziellen Dokumentation ist Version 0.2.0 – seitdem hat sich viel getan, was man leider im Code und GitHub Issues selbst zusammensuchen muss. Diese Serie soll praktische Tipps zur Verwendung sammeln.

Tipp #5: Secure Area

Problem: Testfälle, die EcomDev_PhpUnit_Test_Case_Controller extenden und eine customer Fixture nutzen, schlagen mit Cannot complete this operation from non-admin area bzw. Diese Aktion kann nicht außerhalb des Admin-Bereichs fertiggestellt werden. fehl weil Magento beim tearDown im area=frontend Modus ist und kein Löschen von Kunden erlaubt. Das selbe Problem tritt auf, wenn man versucht, Kunden im Test zu löschen, ohne im adminhtml Kontext zu sein.

Das Customer Model prüft, ob das isSecureArea Flag in der Magento Registry, um das Problem zu lösen, setzen wir den Flag also im Test. Es gibt zwei mögliche Wege dies zu tun:

1.) Wenn Du Kunden im Test selbst erstellst und löschst:

/*
 * @test
 * @registry isSecureArea
 */
public function testThatNeedsToDeleteCustomers()
{
    Mage::register('isSecureArea', true);
    // ...
}

(Beachte, dass die @registry Annotation den Flag nachher zurücksetzt, siehe Tipp #1)

2.) Wenn Du eine Fixture mit Kunden nutzt:

protected function setUp()
{
    Mage::register('isSecureArea', true);
    parent::setUp();
}
protected function tearDown()
{
    parent::tearDown();
    Mage::unregister('isSecureArea');
}

oder bei Fixtures auf Klassen-Ebene:

    public static function setUpBeforeClass()
    {
        Mage::register('isSecureArea', true);
    }
    public static function tearDownAfterClass()
    {
        Mage::unregister('isSecureArea');
    }

EcomDev_PHPUnit Tipp #4

Seit Jahren ist das Test-Framework EcomDev_PHPUnit quasi-Standard für Magento Unit Tests. Die aktuelle Version ist 0.3.7 und der letzte Stand der offiziellen Dokumentation ist Version 0.2.0 – seitdem hat sich viel getan, was man leider im Code und GitHub Issues selbst zusammensuchen muss. Diese Serie soll praktische Tipps zur Verwendung sammeln.

Tipp #4: Benannte Parameter

Der Vorteil von YAML-Dateien für die Konfiguration soll einfache Lesbarkeit sein. Wenn aber ein Data Provider so aussieht, ist es nicht weit her mit der Lesbarkeit:

-
  - 7
  - 1
  -
    5: 7
    6: 9
-
  - 7
  - 1
  -
    5: 8
    6: 10

Technisch das selbe, aber deutlich besser wartbar:

Bundle_X_A1_B2:
  product_id: 7
  qty: 1
  bundle_selections_by_option:
    5: 7
    6: 9
Bundle_X_A2_B2:
  product_id: 7
  qty: 1
  bundle_selections_by_option:
    5: 8
    6: 10

Dass es hier um das in den Warenkorb legen von Bündelprodukten geht, kann man zumindest erahnen und wenn man es weiß, sind die Testdaten einfach zu verstehen, auch ohne in den Quelltext zu sehen.

Ein weiterer positiver Effekt, ist dass PHPUnit bei fehlgeschlagenen Tests nicht mehr TestCase::test() with data set #1 ausgibt, sondern z.B. TestCase::test() with data set "Bundle_X_A1_B2"

Magento Theme Refactoring

Ein sehr häufiges Problem in 2014 war „Ich habe auf Magento 1.8 aktualisiert und jetzt funktioniert das (Login|Warenkorb)-Formular nicht mehr“. Der Grund war, dass seit Magento 1.8 der form key in mehr Formularen genutzt wird, als Security Feature (verhindert CSRF-Angriffe). Aber in jedem Theme, das eine eigene Version der jeweiligen Templates enthielt, fehlte der Form Key, so dass die serverseitige Validierung fehlschlug, leider ohne jede Fehlermeldung.

Natürlich gibt es Themes, deren Markup so verschieden vom Default Theme ist, dass die meisten Templates aus gutem Grund überschrieben wurden. Aber ich sehe mindestens genauso viele Themes, insbesondere Eigenentwicklungen, bei denen erst einmal alle Templates aus base/default kopiert und dann angepasst wurden. Für das Überschreiben von Layout-XML-Dateien gibt es fast keine Entschuldigung, das Layout kann man in allen erdenklichen Möglichkeiten mit einer Theme-spezifischen local.xml Datei anpassen.

Das obengenannte Problem ist ein gutes Beispiel für die Gründe des „Achte auf Updatefähigkeit“ Mantras. Die Fehler hätten vermieden werden können, wenn nur Dateien kopiert worden wären, die wirklich Anpassung benötigten.

In diesem Artikel möchte ich meinen Prozess zum Theme Refactoring darlegen (nur den strukturellen Teil, der HTML-Quelltext wird anschließend exakt gleich aussehen wie vorher, abgesehen von Änderungen durch neuere Versionen der Default-Templates).

Weiterlesen auf integer-net.de

EcomDev_PHPUnit Tipp #3

Seit Jahren ist das Test-Framework EcomDev_PHPUnit quasi-Standard für Magento Unit Tests. Die aktuelle Version ist 0.3.7 und der letzte Stand der offiziellen Dokumentation ist Version 0.2.0 – seitdem hat sich viel getan, was man leider im Code und GitHub Issues selbst zusammensuchen muss. Diese Serie soll praktische Tipps zur Verwendung sammeln.

Tipp #3: Gemeinsame Data Provider

Hast Du dich jemals gefragt, warum man für Expectations und Fixtures den Dateinamen spezifizieren kann aber für Data Providers scheinbar auf den Standard “Testname punkt yaml” festgelegt ist? Der einfache Grund ist, dass @dataProvider ein natives Feature von PHPUnit ist und sein Parameter ein Methodenname sein muss.

Also bedeutet @dataProvider dataProvider, dass die Methode EcomDev_PHPUnit_Test_Case::dataProvider() als Data Provider genutzt wird:

    /**
     * Implements default data provider functionality,
     * returns array data loaded from Yaml file with the same name as test method
     *
     * @param string $testName
     * @return array
     */
    public function dataProvider($testName)
    {
        return TestUtil::dataProvider(get_called_class(), $testName);
    }

Aber EcomDev_PHPUnit bietet auch eine Möglichkeit, den Dateinamen für Data Provider explizit anzugeben, was dann z.B. gemeinsame Data Provider ermöglicht:

/**
 * @dataProvider dataProvider
 * @dataProviderFile customFileName.yaml
 */
public function testSomething($something)