Unit Tests mit dem OXID Framework

Keine Lust zu lesen? Wir haben für dich ein Tutorial Video bereitgestellt welches du unten findest.

Einrichtung OXID VM & Projekt

Du hast bereits eine Funktionierende Installation von OXID Version 6? Dann kannst du diesen Abschnitt überspringen.

Für dieses Tutorial benötigst du die Version 6 von OXID eSales. Du kannst das Projekt in jeder Anwendung aufsetzen – sei es Docker, Mamp, Xampp oder auch Vagrant.

OXID eSales bietet eine Virtual Machine für die OXID Version 6 auf Github an.

Zu finden ist Sie unter GitHub - OXID-eSales/oxvm_eshop: Official OXID eShop VM and SDK integration.

Bitte richte dich nach den Installations-Instruktionen die auf der Github Seite angegeben wurden.

Nachdem die Installation der VM fertiggestellt wurde kannst du dich via vagrant ssh auf die Maschine verbinden und das Projekt aufsetzen.

Wähle hierzu, welche Edition der OXID Version 6 du installieren willst.

cd oxvm_eshop

vagrant up

vagrant ssh

# for the Community Edition

composer create-project oxid-esales/oxideshop-project /var/www/oxideshop dev-b-6.1-ce

# for the Professional Edition

composer create-project oxid-esales/oxideshop-project /var/www/oxideshop dev-b-6.1-pe

# for the Enterprise Edition

composer create-project oxid-esales/oxideshop-project /var/www/oxideshop dev-b-6.1-ee

Nach der Installation kannst du neue Pfade für die test_config eingeben.

In diesem Step empfehle ich die vorgegebenen Pfade beizubehalten und mit Enter weiter zu gehen.

Die Vagrantbox und das Projekt sind nun erfolgreich eingerichtet.

Installation des OXID Onlineshops

Rufe den Shop nun über die von dir vorgegebene (personal.yml) Url auf und folge den Installationsschritten.

Erstellung eines Modules für OXID

Um einen Test zu schreiben benötigen wir ein Modul, welches via composer registriert ist.

Das Modul und die Dateien findest du unter GitHub - CodeCommerce/UnitTestTutorial: Code for unit test tutorial.

Nachdem du die Dateien in dein Projekt kopiert hast (bitte auch die composer.json im Root-Verzeichnis beachten), musst du das Modul via

composer require codecommerce/unittests –n

registrieren.

Unit-Test Testkonfiguration

test_config.yml

Im Verzeichnis oxideshop findest du die Testkonfiguration.

Die Datei heißt test_config.yml und enthalt die Informationen für die Testing Library.

Alle Dateien des Tutorials, unter anderem auch die test_config.yml, findest du auf Github.

Ändere den Parameter test_database_name entsprechend deiner Testdatenbank.

Testeinstellungen in der config.inc.php

Damit die Testdatenbank aufgerufen wird und keine Views benötigt werden, erstellen wir eine weitere config.inc.TEST.php.

Diese Datei enthält Einstellungen für das Testing.

<?php
error_reporting(E_ALL & ~E_STRICT & ~E_DEPRECATED & ~E_WARNING & ~E_NOTICE);
$this->dbName = 'oxid_test'; // database name
$this->sShopDir = __DIR__;
$this->sCompileDir = $this->sShopDir . '/tmp';
$this->iUtfMode = 0;
$this->iDebug = 1;
$this->blSkipViewUsage = true;

Eingebunden wird die Datei am Ende der config.inc.php.

if (defined('OXID_PHP_UNIT')) {
include "config.inc.TEST.php";
}

Beim Aufruf der Testing Library (runtests) wird OXID_PHP_UNIT definiert, somit können wir die Testkonfiguration einbinden.

Testdatenbank

Dupliziere deine Standard Datenbank ohne View-Tabelle und benenne Sie in den Namen aus der test_config.yml um.

Folgende Tabellen müssen mit Inhalt gefüllt sein:

  • oxcountry
  • oxconfig
  • oxshops

Die anderen Tabellen benötigen ausschließlich die Struktur.

Ordnerstruktur

In eurem Modul erstellt Ihr einen Unterordner Unit. Darunter bildet Ihr die normale Struktur eures Modules nach, also Controller oder Model etc.

Beispiel:

module/Vendor/ModulName/Unit/Controller…

Aktivieren der Tests für Modul

Um ein Modul für das Testen zu aktivieren muss in der Datei test_config.yml unter dem Punkt partial_module_paths das Modul hinzugefügt werden. Solltet Ihr den Code von Github nutzen, so könnt Ihr hier

partial_module_paths: 'CodeCommerce/UnitTests'

hinterlegen.

Erster OXID Unit Test

Zu aller erst testen wir, ob die Testing Library korrekt eingebunden ist.

Dazu erstellen wir eine neue Testklasse unter modules/CodeCommerce/UnitTests/Unit/Controller/FirstControllerTest.php

Die Unit-Test Klassen müssen immer von

\OxidEsales\TestingLibrary\UnitTestCase

erben.

class FirstControllerTest extends \OxidEsales\TestingLibrary\UnitTestCase

In unserem Test prüfen wir, ob true gleich true ist.

Hierzu verwendet Ihr die folgende Methode.

/**
* test if test environment works
*/
public function testAssert()
{
$this->assertTrue(true);
}

Aufgerufen wird der Test auf der Vagrant-Maschine im Verzeichnis /var/www/oxideshop mit dem Befehl

php vendor/bin/runtests /var/www/oxideshop/source/modules/CodeCommerce/UnitTests/tests/Unit/Controller/FirstControllerTest.php

ACHTUNG! Auch wenn unser Ordner Tests großgeschrieben ist, so muss der Aufruf in der Konsole mit einem kleinen ‚tests’ ausgeführt werden. Das Framework prüft explizit auf Kleinschreibung.

Nachdem Ihr den Tests aufgeführt habt, solltet Ihr dieses Ergebnis erhalten

OK (1 test, 1 assertion).

Prima! Wir haben nun erfolgreich die Funktionalität der Testkonfiguration durchgeführt.

Aufruf des OXID Test runners

Wie bereits oben geschrieben könnt Ihr den Test wie folgt aufrufen

php vendor/bin/runtests /var/www/oxideshop/source/modules/CodeCommerce/UnitTests/tests/Unit/Controller/FirstControllerTest.php

Oder Ihr richtet eure IDE entsprechend ein, dass Ihr die OXID Unit-Tests via GUI ausführen könnt. Lest hierzu die Informationen eurer IDE.

Was ist ein Mock

Ein Mock ist ein Fake-Objekt, oder auch Platzhalter für ein echtes Objekt, welchem wir Funktionen und auch Werte vorgeben können.

Wir können bestimmen, welche Methoden aufgerufen werden, mit welchen Parametern und was diese zurückgeben sollen.

Beispielsweise haben wir in unserer Methode welche wir testen wollen einen Funktionsaufruf.

Dieser Funktionsaufruf wird 1 mal aufgerufen, mit dem Parameter ‚xyz’ und diese Methode wird true zurück geben.

$mock->expects($this->once())->method('myMethod')->with('xyz')->willReturn(true);

Weitere Informationen zum Mock-Objekt findet Ihr unter Mock-Objekt – Wikipedia

Einfacher Unit-Test

Wir wollen einen Test für unsere Methode articleCheck in der Klasse UnitTestController erstellen.

Dazu benötigen wir die Klasse unter dem Pfad /modules/CodeCommerce/UnitTests/Controller/ mit dem Namen UnitTestController.php

Erstellt darin eine neue Public Function mit dem Namen articleCheck und dem Übergabeparameter $article.

Wir prüfen in der Methode ob der Artikel eine ID besitzt und geben diese zurück falls vorhanden, oder geben false zurück.

<?php

namespace CodeCommerce\UnitTests\Controller;

use OxidEsales\Eshop\Core\Registry;

class UnitTestController
{
/**
* @param $article
* @return bool
*/
public function articleCheck($article)
{
if ($oxid = $article->getId()) {
return $oxid;
}

return false;
}

}

Für den Test brauchen wir eine Test Klasse. Diese erstellen wir unter dem Pfad /modules/CodeCommerce/UnitTests/Tests/Unit/Controller mit dem Namen UnitTestControllerTest.php.

Diese Klasse muss von OxidEsales\TestingLibrary\UnitTestCase erben.

Wir erstellen eine neue Test Methode mit dem gleichen Namen wie unsere zu testende Methode und fügen ‚test’ vor den Namen an (testArticleCheck()).

Nun erstellen wir unseren ersten Mock. Dazu holen wir uns das Mock via getMockBuilder(UnitTestController::class).

Wir geben an, dass wir keine Methoden der Originalklasse mocken möchten, deaktivieren den Originalen Konstruktor und lassen uns das Mock-Objekt via getMock() zurück geben.

Das gleiche machen wir mit dem Artikel Objekt. Allerdings mocken wir hier die Funktion getId(), die wir selber mit Daten und Rückgabewerten vorbelegen.

Die Vorbelegung findet wie folgt statt:

$article->expects($this->once())
->method('getId')
->willReturn($oxid);

Wir erwarten, dass die Methode getID einmal auf dem Artikel Objekt aufgerufen wird und diese die von uns gesetzte $oxid zurück gibt.

<?php

namespace CodeCommerce\UnitTests\Controller;

use OxidEsales\TestingLibrary\UnitTestCase;

class UnitTestControllerTest extends UnitTestCase
{
/**
* @param $oxid
* @param $expected
* @dataProvider dataProviderArticleCheck
*/
public function testArticleCheck($oxid, $expected)
{
$unitTestController = $this->getMockBuilder(UnitTestController::class)
->setMethods(null)
->disableOriginalConstructor()
->getMock();

$article = $this->getMockBuilder(Article::class)
->setMethods(['getId'])
->disableOriginalConstructor()
->getMock();

$article->expects($this->once())
->method('getId')
->willReturn($oxid);

$this->assertEquals($expected, $unitTestController->articleCheck($article));
}

/**
* @return array
*/
public function dataProviderArticleCheck()
{
/** $oxid, $expected */
return [
['123', '123'],
[null, false],
];
}

}

Dataprovider

Damit wir eine Funktion mit mehreren Werten testen können gibt es Dataprovider.

Dataprovider sind Arrays die wir in unsere Testmethode übergeben.

Erstellt dazu eine neue Methode Namens dataProviderArticleCheck und gebt ein multidimesionales Array zurück.

Die Reihenfolge der Array Values werden in der zu testenden Methode übergeben.

…

/** $oxid, $expected */
return [
['123', '123'],
[null, false],
];

public function testArticleCheck($oxid, $expected) …

Um den DataProvider nutzen zu können muss dieser über eine Annotation der Testmethode bekannt gemacht werden.

/**
* @dataProvider dataProviderArticleCheck
*/

Testen von Proteced Functions

Durch das OXID Unit-Test Framework besteht die Möglichkeit Protected Functios testen zu können.

Anstelle des Klassennamens geben wir eine Methode in den MockBuilder.


$unitTestController = $this->getMockBuilder($this->getProxyClassName(UnitTestController::class))
->setMethods(null)
->disableOriginalConstructor()
->getMock();

Im Hintergrund wird eine Proxy-Klasse erstellt, die auf Protected Functions zugreifen kann.

Der Aufruf der Protected Function wird wie eine Public Function durchgeführt.

Solltet Ihr die Protected Function wie im Core mit einem Unterstrich geschrieben haben, ersetzt Ihr den Unterstrich mit ‚UNIT’.

/**
* @dataProvider dataProviderArticleTitleCheck
* @param $title
* @param $expected
*/
public function testArticleTitleCheck($title, $expected)
{
$unitTestController = $this->getMockBuilder($this->getProxyClassName(UnitTestController::class))
->setMethods(null)
->disableOriginalConstructor()
->getMock();

$article = $this->getMockBuilder(Article::class)
->setMethods(['getTitle'])
->disableOriginalConstructor()
->getMock();

$article->expects($this->once())
->method('getTitle')
->willReturn($title);

$this->assertEquals($expected, $unitTestController->UNITarticleTitleCheck($article));
}

Der restliche Test verhält sich wie bei einer Public Function.

Siehe hierzu den Quellcode auf Github.

Registry Mock

Auch die über die Registry aufgerufenen Methoden wie zum Beispiel Registry::getConfig() können gemocked werden.

Hierzu gibt es eine einfache Möglichkeit.

Wir setzen das normale Config Object mit einem Config-Mock Objekt.

/**
* @param $configParam
* @param $expected
* @dataProvider dataProviderGetConfigParamForArticleOption
*/
public function testGetConfigParamForArticleOption($configParam, $expected)
{
$unitTestController = $this->getMockBuilder($this->getProxyClassName(UnitTestController::class))
->setMethods(null)
->disableOriginalConstructor()
->getMock();
$registry = $this->getMockBuilder(Registry::class)
->setMethods(null)
->disableOriginalConstructor()
->getMock();
$config = $this->getMockBuilder(Config::class)
->setMethods(['getConfigParam'])
->disableOriginalConstructor()
->getMock();
$registry->set(Config::class, $config);

$config->expects($this->once())
->method('getConfigParam')
->with('testParam')
->willReturn($configParam);

$this->assertEquals($expected, $unitTestController->getConfigParamForArticleOption());
}

Natürlich könnt Ihr auch hier einen DataProvider einsetzen um mehrere Testfälle zu prüfen.

Testen eines Multiplen Aufrufs einer Methode

Es kann vorkommen, das eine Methode mehrfach in einer Methode aufgerufen wird.

Dazu können verschiedene Übergabe- und Rückgabeparameter von Nöten sein.

Um die Übergabeparameter zu separieren gibt es die Funktion withConsecutive in der pro Aufruf ein Array mit den Entsprechenden Werten übergeben werden kann.

Die Funktion willReturnOnConsecutiveCalls gibt für jeden Aufruf einen Wert zurück.

    $config->expects($this->exactly($configParamCount))
->method('getConfigParam')
->withConsecutive(['testParam1'], ['testParam2'])
->willReturnOnConsecutiveCalls($configParam1, $configParam2);

Beim Testen von Multiplen aufrufen und ggf. If-Abfragen solltet Ihr bei der Nutzung eines DataProviders darauf achten, dass Ihr die korrekte Anzahl von den Aufrufen in die Testmethode übergebt.

 /**
* @return array
*/
public function dataProviderGetMultipleConfigParams()
{
/**
* $configParam1, $configParam2, $expected, $configParamCount
*/
return [
[true, true, true, 2],
[true, false, false, 2],
[false, true, false, 1],
[false, false, false, 1],
];
}

Das komplette Beispiel:

/**
* @param $configParam1
* @param $configParam2
* @param $expected
* @param $configParamCount
* @dataProvider dataProviderGetMultipleConfigParams
*/
public function testGetMultipleConfigParams($configParam1, $configParam2, $expected, $configParamCount)
{
$unitTestController = $this->getMockBuilder($this->getProxyClassName(UnitTestController::class))
->setMethods(null)
->disableOriginalConstructor()
->getMock();
$registry = $this->getMockBuilder(Registry::class)
->setMethods(null)
->disableOriginalConstructor()
->getMock();
$config = $this->getMockBuilder(Config::class)
->setMethods(['getConfigParam'])
->disableOriginalConstructor()
->getMock();
$registry->set(Config::class, $config);

$config->expects($this->exactly($configParamCount))
->method('getConfigParam')
->withConsecutive(['testParam1'], ['testParam2'])
->willReturnOnConsecutiveCalls($configParam1, $configParam2);

$this->assertEquals($expected, $unitTestController->getMultipleConfigParams());
}

/**
* @return array
*/
public function dataProviderGetMultipleConfigParams()
{
/**
* $configParam1, $configParam2, $expected, $configParamCount
*/
return [
[true, true, true, 2],
[true, false, false, 2],
[false, true, false, 1],
[false, false, false, 1],
];
}
1 Like