EcomDev PHPUnit Tipp #14

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 #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:

  1. Siehe auch YAML Directory Fallback

EcomDev PHPUnit Tipp #13

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 #13: EAV Fixtures beschleunigen

Nutzt man die EAV Fixtures, um Produkte, Kategorien und Kunden für Tests anzulegen, macht der Fixture Processor von EcomDev_PHPUnit vor jedem Test ein Backup der jeweils bestehenden Tabellen und spielt es anschließend wieder zurück. Da die “magento_unit_tests” Test-Datenbank standardmäßig als Kopie der aktuellen Magento-Datenbank angelegt wird, kann das sehr viel unnötiger Overhead sein.

Die EAV-Tabellen in der Test-Datenbank einmalig zu bereinigen, kann Tests, die sonst mehrere Minuten laufen um ein vielfaches beschleunigen. Dazu löschen wir in der Test-DB alle Datensätze in den Main Tables (die verknüpften Attribute etec. werden über Trigger automatisch gelöscht), mit Ausnahme der Standard Root Kategorie:

delete from catalog_product_entity;
delete from catalog_category_entity where entity_id > 2;
delete from customer_entity;

EcomDev PHPUnit Tipp #12

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 #12: Cache

Man kann Caches pro Test an- und abschalten, mit den folgenden Annotations:

/**
 * @test
 * @cache on all
 */
public function testWithCache()

/**
 * @test
 * @cache off all
 */
public function testWithoutCache()

/**
 * @test
 * @cache off all
 * @cache on layout
 * @cache on block_html
 */
public function testWithSomeCachesTurnedOn()


/**
 * @test
 * @cache on all
 * @cache off translate
 * @cache off config
 */
public function testWithSomeCachesTurnedOff()

Die Annotations werden in der angegebenen Reihenfolge ausgeführt, wenn man also “all” benutzt, sollte es immer vor allen anderen @cache Annotations stehen.

EcomDev_PHPUnit Tipp #11

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 #11: Minimal-Fixtures für Produkttypen

Es folgen einige Beispiel Fixtures für verschiedene Produkttypen. Sie sind so minimal gehalten, dass die Daten gerade ausreichen, um die Produkte und ggf. Produktbeziehungen anzulegen. In den meisten Fällen wird man zusätzliche Attribute wie Name, Status, Sichtbarkeit, Preis und Lagerbestand benötigen.

Continue reading “EcomDev_PHPUnit Tipp #11”

EcomDev_PHPUnit Tipp #10

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 #10: Defekte Übersetzungen

Es kann passieren, dass Übersetzungen in Unit Tests nicht funktionieren, wenn man einen Helper mit einem Helper-Mock ersetzt (mit mockHelper() und replaceByMock()), insbesondere im Developer Mode, wo Übersetzungen nur funktionieren wenn sie für das richtige Modul definiert wurden.

Der Modulname wird in Mage_Core_Helper_Abstract mit $this->_getModuleName() ermittelt, also schauen wir uns die Methode einmal an:

/**
 * Retrieve helper module name
 *
 * @return string
 */
protected function _getModuleName()
{
    if (!$this->_moduleName) {
        $class = get_class($this);
        $this->_moduleName = substr($class, 0, strpos($class, '_Helper'));
    }
    return $this->_moduleName;
}

Wenn Deine Helper Klasse Your_Awesome_Helper_Data ist, wird die Klasse des Helper-Mocks Mock_Your_Awesome_Helper_Data heißen. Weil ein Modul namens Mock_Your_Awesome nicht existiert, gibt es keine Übersetzungen.

Lösung

Um Helper Unit-Test-sicher zu machen (und als Nebeneffekt auch Rewrite-sicher), definiere _moduleName explizit:

class Your_Awesome_Helper_Data extends Mage_Core_Helper_Abstract
{
    protected $_moduleName = 'Your_Awesome';
}

Zuerst beschrieben hier: http://magento.stackexchange.com/questions/46255/ecomdev-phpunit-translation-not-working-with-mocked-helper

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

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');
    }