German (Deutsch) translation by Valentina (you can also view the original English article)
Wenn Sie erfahren möchten, warum Tests von Vorteil sind, ist dies nicht der richtige Artikel für Sie. Im Verlauf dieses Tutorials gehe ich davon aus, dass Sie die Vorteile bereits verstehen und hoffen, zu lernen, wie Sie Ihre Tests in Laravel 4 am besten schreiben und organisieren können.
Version 4 von Laravel bietet im Vergleich zu seiner vorherigen Version ernsthafte Verbesserungen in Bezug auf Tests. Dies ist der erste Artikel einer Reihe, in dem beschrieben wird, wie Tests für Laravel 4-Anwendungen geschrieben werden. Wir beginnen die Serie mit der Erörterung von Modelltests.
Einstellung
In-Memory-Datenbank
Sofern Sie keine Rohabfragen in Ihrer Datenbank ausführen, ermöglicht Laravel Ihrer Anwendung, datenbankunabhängig zu bleiben. Mit einem einfachen Treiberwechsel kann Ihre Anwendung jetzt mit anderen DBMS (MySQL, PostgreSQL, SQLite usw.) arbeiten. Unter den Standardoptionen bietet SQLite eine besondere, aber sehr nützliche Funktion: In-Memory-Datenbanken.
Mit Sqlite können wir die Datenbankverbindung auf :memory:
setzen, was unsere Tests drastisch beschleunigt, da die Datenbank nicht auf der Festplatte vorhanden ist. Darüber hinaus wird die Produktions- /Entwicklungsdatenbank niemals mit verbleibenden Testdaten gefüllt, da die Verbindung :memory:
immer mit einer leeren Datenbank beginnt.
Kurz gesagt: Eine In-Memory-Datenbank ermöglicht schnelle und saubere Tests.
Erstellen Sie im Verzeichnis app/config/testing
eine neue Datei mit dem Namen database.php
und füllen Sie sie mit folgendem Inhalt:
// app/config/testing/database.php <?php return array( 'default' => 'sqlite', 'connections' => array( 'sqlite' => array( 'driver' => 'sqlite', 'database' => ':memory:', 'prefix' => '' ), ) );
Die Tatsache, dass database.php
im Konfiguration-testing
-Verzeichnis abgelegt ist, bedeutet, dass diese Einstellungen nur in einer Testumgebung verwendet werden (die Laravel automatisch festlegt). Wenn auf Ihre Anwendung normal zugegriffen wird, wird die speicherinterne Datenbank daher nicht verwendet.
Vor dem Ausführen von Tests
Da die In-Memory-Datenbank beim Herstellen einer Verbindung immer leer ist, ist es wichtig, die Datenbank vor jedem Test zu migrieren. Öffnen Sie dazu app/tests/TestCase.php
und fügen Sie am Ende der Klasse die folgende Methode hinzu:
/** * Migrates the database and set the mailer to 'pretend'. * This will cause the tests to run quickly. * */ private function prepareForTests() { Artisan::call('migrate'); Mail::pretend(true); }
HINWEIS: Die
setUp()
-Methode wird vor jedem Test von PHPUnit ausgeführt.
Diese Methode bereitet die Datenbank vor und ändert den Status der Mailer
-Klasse von Laravel, um dies pretend
. Auf diese Weise sendet der Mailer beim Ausführen von Tests keine echte E-Mail. Stattdessen werden die "gesendeten" Nachrichten protokolliert.
Um app/tests/TestCase.php
abzuschließen, rufen Sie prepareForTests()
innerhalb der PHPUnit setUp()
-Methode auf, die vor jedem Test ausgeführt wird.
Vergessen Sie nicht
parent::setUp()
, da wir die Methode der übergeordneten Klasse überschreiben.
/** * Default preparation for each test * */ public function setUp() { parent::setUp(); // Don't forget this! $this->prepareForTests(); }
Zu diesem Zeitpunkt sollte app/tests/TestCase.php
wie folgt aussehen. Denken Sie daran, dass createApplication
automatisch von Laravel erstellt wird. Sie müssen sich keine Sorgen machen.
// app/tests/TestCase.php <?php class TestCase extends Illuminate\Foundation\Testing\TestCase { /** * Default preparation for each test */ public function setUp() { parent::setUp(); $this->prepareForTests(); } /** * Creates the application. * * @return Symfony\Component\HttpKernel\HttpKernelInterface */ public function createApplication() { $unitTesting = true; $testEnvironment = 'testing'; return require __DIR__.'/../../start.php'; } /** * Migrates the database and set the mailer to 'pretend'. * This will cause the tests to run quickly. */ private function prepareForTests() { Artisan::call('migrate'); Mail::pretend(true); } }
Um unsere Tests zu schreiben, erweitern Sie einfach TestCase
. Die Datenbank wird vor jedem Test initialisiert und migriert.
Die Tests
Es ist richtig zu sagen, dass wir in diesem Artikel den TDD-Prozess nicht verfolgen werden. Das Problem hier ist didaktisch, mit dem Ziel zu demonstrieren, wie die Tests geschrieben werden können. Aus diesem Grund habe ich mich entschlossen, zuerst die fraglichen Modelle und dann die zugehörigen Tests zu enthüllen. Ich glaube, dass dies ein besserer Weg ist, um dieses Tutorial zu veranschaulichen.
Der Kontext dieser Demo-Anwendung ist ein einfaches Blog/CMS, das Benutzer(Authentifizierung), Beiträge und statische Seiten enthält (die im Menü angezeigt werden).
Postmodell
Bitte beachten Sie, dass das Modell die Klasse Ardent und nicht Eloquent erweitert. Ardent ist ein Paket, das eine einfache Validierung beim Speichern des Modells ermöglicht (siehe die Eigenschaft $rules
).
Als nächstes haben wir das public static $factory
-Array, das das FactoryMuff-Paket nutzt, um die Objekterstellung beim Testen zu unterstützen.
Sowohl Ardentx als auch FactoryMuff sind über Packagist und Composer erhältlich.
In unserem Post
-Modell haben wir über die Magic-Author
-Methode eine Beziehung zum User
-Modell.
Schließlich haben wir eine einfache Methode, die das Datum zurückgibt und als "day/month/year" formatiert ist.
// app/models/Post.php <?php use LaravelBook\Ardent\Ardent; class Post extends Ardent { /** * Table */ protected $table = 'posts'; /** * Ardent validation rules */ public static $rules = array( 'title' => 'required', // Post tittle 'slug' => 'required|alpha_dash', // Post Url 'content' => 'required', // Post content (Markdown) 'author_id' => 'required|numeric', // Author id ); /** * Array used by FactoryMuff to create Test objects */ public static $factory = array( 'title' => 'string', 'slug' => 'string', 'content' => 'text', 'author_id' => 'factory|User', // Will be the id of an existent User. ); /** * Belongs to user */ public function author() { return $this->belongsTo( 'User', 'author_id' ); } /** * Get formatted post date * * @return string */ public function postedAt() { $date_obj = $this->created_at; if (is_string($this->created_at)) $date_obj = DateTime::createFromFormat('Y-m-d H:i:s', $date_obj); return $date_obj->format('d/m/Y'); } }
Post-Tests
Um die Organisation zu gewährleisten, habe ich die Klasse mit den Post
-Modelltests in app/tests/models/PostTest.php
platziert. Wir werden alle Tests abschnittsweise durchgehen.
// app/tests/models/PostTest.php <?php use Zizaco\FactoryMuff\Facade\FactoryMuff; class PostTest extends TestCase {
Wir erweitern die TestCase
-Klasse, die für PHPUnit-Tests in Laravel erforderlich ist. Vergessen Sie auch nicht unsere prepareTests
-Methode, die vor jedem Test ausgeführt wird.
public function test_relation_with_author() { // Instantiate, fill with values, save and return $post = FactoryMuff::create('Post'); // Thanks to FactoryMuff, this $post have an author $this->assertEquals( $post->author_id, $post->author->id ); }
Dieser Test ist "optional". Wir testen, ob die Beziehung "Post
gehört zum User
". Der Zweck hier ist hauptsächlich, die Funktionalität von FactoryMuff zu demonstrieren.
Sobald die Post
-Klasse über das statische Array $factory
verfügt, das 'author_id' => 'factory| User'
enthält (beachten Sie den oben gezeigten Quellcode des Modells), instanziiert FactoryMuff einen neuen User
, füllt seine Attribute, speichert ihn in der Datenbank und kehrt schließlich zurück seine ID für das Attribut author_id
im Post
.
Damit dies möglich ist, muss das User
-Modell über ein $factory
-Array verfügen, das auch seine Felder beschreibt.
Beachten Sie, wie Sie über $post->author
auf die User
-Beziehung zugreifen können. Als Beispiel können wir auf den $post->author->username
oder ein anderes vorhandenes Benutzerattribut zugreifen.
Das FactoryMuff-Paket ermöglicht die schnelle Instanziierung konsistenter Objekte zum Testen, während alle erforderlichen Beziehungen berücksichtigt und instanziiert werden. In diesem Fall wird User
beim Erstellen eines Post
s mit FactoryMuff::create('Post')
ebenfalls vorbereitet und verfügbar gemacht.
public function test_posted_at() { // Instantiate, fill with values, save and return $post = FactoryMuff::create('Post'); // Regular expression that represents d/m/Y pattern $expected = '/\d{2}\/\d{2}\/\d{4}/'; // True if preg_match finds the pattern $matches = ( preg_match($expected, $post->postedAt()) ) ? true : false; $this->assertTrue( $matches ); } }
Zum Abschluss bestimmen wir, ob die von der Methode postedAt()
zurückgegebene Zeichenfolge dem Format "Tag / Monat / Jahr" entspricht. Für eine solche Überprüfung wird ein regulärer Ausdruck verwendet, um zu testen, ob das Muster \d{2}\/\d{2}\/\d{4}
("2 Zahlen" + "Balken" + "2 Zahlen" + "Balken)" + "4 Zahlen") wird gefunden.
Alternativ könnten wir den assertRegExp-Matcher von PHPUnit verwenden.
Zu diesem Zeitpunkt lautet die Datei app/tests/models/PostTest.php
wie folgt:
// app/tests/models/PostTest.php <?php use Zizaco\FactoryMuff\Facade\FactoryMuff; class PostTest extends TestCase { public function test_relation_with_author() { // Instantiate, fill with values, save and return $post = FactoryMuff::create('Post'); // Thanks to FactoryMuff this $post have an author $this->assertEquals( $post->author_id, $post->author->id ); } public function test_posted_at() { // Instantiate, fill with values, save and return $post = FactoryMuff::create('Post'); // Regular expression that represents d/m/Y pattern $expected = '/\d{2}\/\d{2}\/\d{4}/'; // True if preg_match finds the pattern $matches = ( preg_match($expected, $post->postedAt()) ) ? true : false; $this->assertTrue( $matches ); } }
PS: Ich habe mich entschieden, den Namen der Tests aus Gründen der Lesbarkeit nicht in CamelCase zu schreiben. PSR-1 vergib mir, aber
testRelationWithAuthor
ist nicht so lesbar, wie ich es persönlich vorziehen würde. Natürlich können Sie den Stil verwenden, den Sie am meisten bevorzugen.
Seitenmodell
Unser CMS benötigt ein Modell zur Darstellung statischer Seiten. Dieses Modell wird wie folgt implementiert:
<?php // app/models/Page.php use LaravelBook\Ardent\Ardent; class Page extends Ardent { /** * Table */ protected $table = 'pages'; /** * Ardent validation rules */ public static $rules = array( 'title' => 'required', // Page Title 'slug' => 'required|alpha_dash', // Slug (url) 'content' => 'required', // Content (markdown) 'author_id' => 'required|numeric', // Author id ); /** * Array used by FactoryMuff */ public static $factory = array( 'title' => 'string', 'slug' => 'string', 'content' => 'text', 'author_id' => 'factory|User', // Will be the id of an existent User. ); /** * Belongs to user */ public function author() { return $this->belongsTo( 'User', 'author_id' ); } /** * Renders the menu using cache * * @return string Html for page links. */ public static function renderMenu() { $pages = Cache::rememberForever('pages_for_menu', function() { return Page::select(array('title','slug'))->get()->toArray(); }); $result = ''; foreach( $pages as $page ) { $result .= HTML::action( 'PagesController@show', $page['title'], ['slug'=>$page['slug']] ).' | '; } return $result; } /** * Forget cache when saved */ public function afterSave( $success ) { if( $success ) Cache::forget('pages_for_menu'); } /** * Forget cache when deleted */ public function delete() { parent::delete(); Cache::forget('pages_for_menu'); } }
Wir können beobachten, dass die statische Methode renderMenu()
eine Reihe von Links für alle vorhandenen Seiten rendert. Dieser Wert wird im Cache-Schlüssel 'pages_for_menu'
gespeichert. Auf diese Weise muss bei zukünftigen Aufrufen von renderMenu()
nicht mehr auf die reale Datenbank zugegriffen werden. Dies kann die Leistung unserer Anwendung erheblich verbessern.
Wenn jedoch eine Page
gespeichert oder gelöscht wird (afterSave()
- und delete()
-Methoden), wird der Wert des Caches gelöscht, sodass renderMenu()
den neuen Status der Datenbank widerspiegelt. Wenn also der Name einer Seite geändert oder gelöscht wird, wird der key 'pages_for_menu'
aus dem Cache gelöscht. (Cache::forget('pages_for_menu');
)
HINWEIS: Die Methode
afterSave()
ist über das Ardent-Paket verfügbar. Andernfalls müsste die Methodesave()
implementiert werden, um den Cache zu bereinigen undparent::save()
aufzurufen.
Seitentests
In: app/tests/models/PageTest.php
schreiben wir die folgenden Tests:
<?php // app/tests/models/PageTest.php use Zizaco\FactoryMuff\Facade\FactoryMuff; class PageTest extends TestCase { public function test_get_author() { $page = FactoryMuff::create('Page'); $this->assertEquals( $page->author_id, $page->author->id ); }
Wir haben wieder einen "optionalen" Test, um die Beziehung zu bestätigen. Da Beziehungen in der Verantwortung von Illuminate\Database\Eloquent
liegen, das bereits von Laravels eigenen Tests abgedeckt wird, müssen wir keinen weiteren Test schreiben, um zu bestätigen, dass dieser Code wie erwartet funktioniert.
public function test_render_menu() { $pages = array(); for ($i=0; $i < 4; $i++) { $pages[] = FactoryMuff::create('Page'); } $result = Page::renderMenu(); foreach ($pages as $page) { // Check if each page slug(url) is present in the menu rendered. $this->assertGreaterThan(0, strpos($result, $page->slug)); } // Check if cache has been written $this->assertNotNull(Cache::get('pages_for_menu')); }
Dies ist einer der wichtigsten Tests für das Page
-Modell. Zunächst werden vier Seiten in der for
-Schleife erstellt. Anschließend wird das Ergebnis des Aufrufs renderMenu()
in der $result
-Variablen gespeichert. Diese Variable sollte eine HTML-Zeichenfolge enthalten, die Links zu den vorhandenen Seiten enthält.
Die foreach
-Schleife prüft, ob der Slug (URL) jeder Seite in $result
vorhanden ist. Dies ist ausreichend, da das genaue Format des HTML für unsere Anforderungen nicht relevant ist.
Schließlich stellen wir fest, ob im Cache-Schlüssel pages_for_menu
etwas gespeichert ist. Mit anderen Worten, hat der Aufruf von renderMenu()
tatsächlich einen Wert im Cache gespeichert?
public function test_clear_cache_after_save() { // An test value is saved in cache Cache::put('pages_for_menu','avalue', 5); // This should clean the value in cache $page = FactoryMuff::create('Page'); $this->assertNull(Cache::get('pages_for_menu')); }
Mit diesem Test soll überprüft werden, ob beim Speichern einer neuen Page
der Cache-Schlüssel 'pages_for_menu'
geleert wird. Der FactoryMuff::create('Page');
löst schließlich die save()
-Methode aus, sodass ausreichen sollte, damit der Schlüssel 'pages_for_menu'
gelöscht wird.
public function test_clear_cache_after_delete() { $page = FactoryMuff::create('Page'); // An test value is saved in cache Cache::put('pages_for_menu','value', 5); // This should clean the value in cache $page->delete(); $this->assertNull(Cache::get('pages_for_menu')); }
Ähnlich wie beim vorherigen Test wird hierdurch festgestellt, ob der Schlüssel 'pages_for_menu'
nach dem Löschen einer Page
ordnungsgemäß geleert wird.
Ihre PageTest.php
sollte folgendermaßen aussehen:
<?php // app/tests/models/PageTest.php use Zizaco\FactoryMuff\Facade\FactoryMuff; class PageTest extends TestCase { public function test_get_author() { $page = FactoryMuff::create('Page'); $this->assertEquals( $page->author_id, $page->author->id ); } public function test_render_menu() { $pages = array(); for ($i=0; $i < 4; $i++) { $pages[] = FactoryMuff::create('Page'); } $result = Page::renderMenu(); foreach ($pages as $page) { // Check if each page slug(url) is present in the menu rendered. $this->assertGreaterThan(0, strpos($result, $page->slug)); } // Check if cache has been written $this->assertNotNull(Cache::get('pages_for_menu')); } public function test_clear_cache_after_save() { // An test value is saved in cache Cache::put('pages_for_menu','avalue', 5); // This should clean the value in cache $page = FactoryMuff::create('Page'); $this->assertNull(Cache::get('pages_for_menu')); } public function test_clear_cache_after_delete() { $page = FactoryMuff::create('Page'); // An test value is saved in cache Cache::put('pages_for_menu','value', 5); // This should clean the value in cache $page->delete(); $this->assertNull(Cache::get('pages_for_menu')); } }
Benutzermodell
In Bezug auf die zuvor vorgestellten Modelle haben wir jetzt den User
. Hier ist der Code für dieses Modell:
<?php // app/models/User.php use Zizaco\Confide\ConfideUser; class User extends ConfideUser { // Array used in FactoryMuff public static $factory = array( 'username' => 'string', 'email' => 'email', 'password' => '123123', 'password_confirmation' => '123123', ); /** * Has many pages */ public function pages() { return $this->hasMany( 'Page', 'author_id' ); } /** * Has many posts */ public function posts() { return $this->hasMany( 'Post', 'author_id' ); } }
Dieses Modell fehlt an Tests.
Wir können beobachten, dass es mit Ausnahme von Beziehungen (die zum Testen hilfreich sein können) hier keine Methodenimplementierung gibt. Was ist mit der Authentifizierung? Nun, die Verwendung des Confide-Pakets bietet bereits die Implementierung und Tests dafür.
Die Tests für
Zizaco\Confide\ConfideUser
befinden sich in ConfideUserTest.php.
Es ist wichtig, die Klassenverantwortung zu bestimmen, bevor Sie Ihre Tests schreiben. Das Testen der Option zum "Zurücksetzen des Kennworts" eines User
s wäre überflüssig. Dies liegt daran, dass die ordnungsgemäße Verantwortung für diesen Test in Zizaco\Confide\ConfideUser
liegt; nicht im User
.
Gleiches gilt für Datenvalidierungstests. Da das Paket Ardent diese Verantwortung übernimmt, wäre es nicht sinnvoll, die Funktionalität erneut zu testen.
Kurz gesagt: Halten Sie Ihre Tests sauber und organisiert. Bestimmen Sie die ordnungsgemäße Verantwortung jeder Klasse und testen Sie nur, wofür sie ausschließlich verantwortlich ist.
Abschluss

Die Verwendung einer In-Memory-Datenbank ist eine gute Vorgehensweise, um Tests für eine Datenbank schnell auszuführen. Dank der Hilfe einiger Pakete wie Ardent, FactoryMuff und Confide können Sie die Codemenge in Ihren Modellen minimieren und gleichzeitig die Tests sauber und objektiv halten.
In der Fortsetzung dieses Artikels werden wir die Controller-Tests überprüfen. Bleib dran!
Beginnen wir noch mit Laravel 4 und lassen Sie sich das Wesentliche beibringen!
Subscribe below and we’ll send you a weekly email summary of all new Code tutorials. Never miss out on learning about the next big thing.
Update me weeklyEnvato Tuts+ tutorials are translated into other languages by our community members—you can be involved too!
Translate this post