Unter den Diskussionen in der Community höre ich oft die Meinung, dass Unit-Tests in Laravel falsch und kompliziert sind und die Tests selbst lang sind und keinen Nutzen bringen. Aus diesem Grund schreiben nur wenige diese Tests und beschränken sich nur auf Funktionstests, und die Verwendung von Komponententests tendiert zu 0.
Das habe ich auch einmal gedacht, aber einmal habe ich darüber nachgedacht und mich gefragt - vielleicht weiß ich nicht, wie ich sie kochen soll?
Für einige Zeit verstand ich und am Ausgang hatte ich ein neues Verständnis für Unit-Tests, und die Tests wurden klar, freundlich, schnell und begannen mir zu helfen.
Ich möchte mein Verständnis mit der Community teilen und dieses Thema noch besser verstehen, um meine Tests noch besser zu machen.
Ein bisschen Philosophie und Grenzen
Laravel ist an einigen Stellen eine Art Rahmen. Besonders in Bezug auf Fassaden und Eloquent. Ich werde nicht auf Diskussionen oder Verurteilungen dieser Punkte eingehen, aber ich werde zeigen, wie ich sie mit Unit-Tests kombiniere.
Ich schreibe Tests, nachdem ich (oder gleichzeitig) den Hauptcode geschrieben habe. Möglicherweise ist mein Ansatz nicht mit dem TDD-Ansatz kompatibel oder erfordert teilweise Anpassungen.
Die wichtigste Frage, die ich mir vor dem Schreiben eines Tests stelle, lautet: „Was genau möchte ich testen?“. Dies ist ein wichtiges Thema. Es war diese Idee, die es mir ermöglichte, meine Ansichten zum Schreiben von Komponententests und zum Projektcode selbst zu überdenken.
Die Tests sollten stabil und minimal von der Umgebung abhängig sein. Wenn Ihre Tests bei Mutationen fehlschlagen, sind sie höchstwahrscheinlich gut. Umgekehrt sind sie wahrscheinlich nicht sehr gut, wenn sie nicht fallen.
Standardmäßig unterstützt Laravel drei Arten von Tests:
Ich werde hauptsächlich über Unit-Tests sprechen.
Ich teste nicht den gesamten Code durch Unit-Tests (möglicherweise ist dies nicht korrekt). Ich teste überhaupt keinen Code (mehr dazu weiter unten).
Wenn in den Tests Moques verwendet werden, vergessen Sie nicht, Mockery :: close () für tearDown auszuführen.
Einige Beispieltests werden "aus dem Internet entnommen".
Wie ich teste
Im Folgenden werde ich Testbeispiele nach Klassengruppen gruppieren und versuchen, Testbeispiele für jede Klassengruppe anzugeben. Für die meisten Klassengruppen werde ich keine Beispiele für den Code selbst geben.
Middleware
Für den Middleware-Unit-Test erstelle ich ein Objekt der Request-Klasse, ein Objekt der gewünschten Middleware, rufe dann die Handle-Methode auf und führe die erforderlichen Asserts aus. Middleware kann entsprechend den durchgeführten Aktionen in 3 Gruppen unterteilt werden:
- Ändern des Anforderungsobjekts (Ändern der Textanforderung oder der Sitzungen)
- Weiterleiten (Ändern des Antwortstatus)
- nichts mit dem Anforderungsobjekt tun
Versuchen wir, für jede Gruppe ein Beispiel für einen Test zu geben:
Angenommen, wir haben die folgende Middleware, deren Aufgabe es ist, das Titelfeld zu ändern:
class TitlecaseMiddleware { public function handle($request, Closure $next) { if ($request->title) { $request->merge([ 'title' => title_case($request->title) ]); } return $next($request); } }
Ein Test für eine ähnliche Middleware könnte folgendermaßen aussehen:
public function testChangeTitleToTitlecase() { $request = new Request; $request->merge([ 'title' => 'Title is in mixed CASE' ]); $middleware = new TitlecaseMiddleware; $middleware->handle($request, function ($req) { $this->assertEquals('Title Is In Mixed Case', $req->title); }); }
Tests für die Gruppen 2 und 3 werden von einem solchen Plan sein:
$response = $middleware->handle($request, function () {}); $this->assertEquals($response->getStatusCode(), 302);
Klasse anfordern
Die Hauptaufgabe dieser Klassengruppe ist die Autorisierung und Validierung von Anforderungen.
Ich teste diese Klassen nicht durch Komponententests (ich gebe zu, dass dies möglicherweise nicht der Fall ist), sondern nur durch Funktionstests. Meiner Meinung nach sind Unit-Tests für diese Klassen überflüssig, aber ich habe einige interessante Beispiele dafür gefunden, wie dies getan werden kann. Vielleicht helfen sie Ihnen, wenn Sie sich entscheiden, Ihre Anforderungseinheitsklasse mit Tests zu testen:
Controller
Ich teste Controller auch nicht durch Unit-Tests. Beim Testen verwende ich jedoch eine Funktion, über die ich sprechen möchte.
Controller sollten meiner Meinung nach leicht sein. Ihre Aufgabe ist es, die richtige Anfrage zu erhalten, die erforderlichen Dienste und Repositories aufzurufen (da diese beiden Begriffe für Laravel „fremd“ sind, werde ich meine Terminologie unten erläutern) und die Antwort zurückzugeben. Lösen Sie manchmal ein Ereignis, einen Job usw. aus.
Dementsprechend müssen wir beim Testen durch Funktionstests nicht nur den Controller mit den erforderlichen Parametern aufrufen und die Antwort überprüfen, sondern auch die erforderlichen Dienste sperren und überprüfen, ob sie tatsächlich aufgerufen werden (oder nicht aufgerufen werden). Manchmal - erstellen Sie einen Datensatz in der Datenbank.
Ein Beispiel für einen Controller-Test mit einem Serviceklassen-Mock:
public function testProductCategorySync() { $service = Mockery::mock(\App\Services\Product::class); app()->instance(\App\Services\Product::class, $service); $service->shouldReceive('sync')->once(); $response = $this->post('/api/v1/sync/eventsCallback', [ "eventType" => "PRODUCT_SYNC" ]); $response->assertStatus(200); }
Ein Beispiel für einen Controller-Test mit einem Fassadenmodell (in unserem Fall ein Ereignis, das jedoch analog für andere Laravel-Fassaden durchgeführt wird):
public function testChangeCart() { Event::fake(); $user = factory(User::class)->create(); Passport::actingAs( $user ); $response = $this->post('/api/v1/cart/update', [ 'products' => [ [
Service und Repositories
Diese Arten von Klassen sind sofort einsatzbereit. Ich versuche, die Controller dünn zu halten, also stelle ich die ganze zusätzliche Arbeit in eine dieser Klassengruppen.
Ich habe den Unterschied zwischen ihnen wie folgt festgestellt:
- Wenn ich eine Geschäftslogik implementieren muss, füge ich diese in die entsprechende Service-Schicht (Klasse) ein.
- In allen anderen Fällen habe ich dies in die Repository-Klassengruppe eingefügt. In der Regel geht die Funktion mit Eloquent dorthin. Ich verstehe, dass dies nicht ganz die richtige Definition der Repository-Ebene ist. Ich habe auch gehört, dass einige alles aushalten, was mit Eloquent im Modell zu tun hat. Mein Ansatz ist meiner Meinung nach eine Art Kompromiss, obwohl "akademisch" nicht ganz richtig ist.
Für Repository-Klassen schreibe ich fast keine Tests.
Testbeispiel für Serviceklassen unten:
public function testUpdateCart() { Event::fake(); $cartService = resolve(CartService::class); $cartRepo = resolve(CartRepository::class); $user = factory(User::class)->make(); $cart = $cartRepo->getCart($user);
Event-Listener, Jobs
Diese Klassen werden fast nach dem allgemeinen Prinzip getestet - wir bereiten die zum Testen erforderlichen Daten vor; Wir rufen die gewünschte Klasse aus dem Framework auf und überprüfen das Ergebnis.
Beispiel für Listener:
public function testHandle() { $user = factory(User::class)->create(); $cart = Cart::create([ 'userId' => $user->id,
Konsole Konsole
Ich betrachte Konsolenbefehle als eine Art Controller, der zusätzlich Daten ausgeben (und komplexere Manipulationen mit den in der Dokumentation beschriebenen Konsolen-Eingabe / Ausgabe durchführen kann). Dementsprechend ähneln die Tests dem Controller: Wir überprüfen, ob die erforderlichen Servicemethoden aufgerufen werden, Ereignisse ausgelöst werden (oder nicht) und wir überprüfen auch die Interaktion mit der Konsole (Ausgabe oder Datenanforderung).
Ein Beispiel für einen ähnlichen Test:
public function testSendCartSyncDataEmptyJobs() { $service = m::mock(CartJobsRepository::class); app()->instance(CartJobsRepository::class, $service); $service->shouldReceive('getAll') ->once()->andReturn(collect([])); $this->artisan('sync:cart') ->expectsOutput('Get all jobs for sending...') ->expectsOutput('All count for sending: 0') ->expectsOutput('Empty jobs') ->assertExitCode(0); }
Separate externe Bibliotheken
Wenn separate Bibliotheken über Funktionen für Komponententests verfügen, werden diese in der Regel in der Dokumentation beschrieben. In anderen Fällen wird die Arbeit mit diesem Code ähnlich wie auf der Serviceschicht getestet. Es macht keinen Sinn, die Bibliotheken selbst mit Tests abzudecken (nur wenn Sie PR an diese Bibliothek senden möchten), und Sie sollten sie als eine Art Black Box betrachten.
Bei vielen Projekten muss ich über die API mit anderen Diensten interagieren. Laravel verwendet häufig die Guzzle-Bibliothek für diese Zwecke. Es erschien mir zweckmäßig, die gesamte Arbeit mit anderen Diensten in eine separate Klasse des NetworkService-Dienstes einzuteilen. Dies erleichterte mir das Schreiben und Testen des Hauptcodes und half, die Antworten und die Fehlerbehandlung zu standardisieren.
Ich gebe Beispiele für verschiedene Tests für meine NetworkService-Klasse:
public function testSuccessfulSendNetworkService() { $mockHandler = new MockHandler([ new Response(200), ]); $handler = HandlerStack::create($mockHandler); $client = new Client(['handler' => $handler]); app()->instance(\GuzzleHttp\Client::class, $client); $networkService = resolve(NetworkService::class); $response = $networkService->sendRequestToSite('GET', '/'); $this->assertEquals('200', $response->getStatusCode()); } public function testUnsupportedMethodSendNetworkService() { $networkService = resolve(NetworkService::class); $this->expectException('\InvalidArgumentException'); $networkService->sendRequestToSite('PUT', '/'); } public function testUnsetConfigUrlNetworkService() { $networkService = resolve(NetworkService::class); Config::shouldReceive('get') ->once() ->with('app.api_url') ->andReturn(''); Config::shouldReceive('get') ->once() ->with('app.api_token') ->andReturn('token'); $this->expectException('\InvalidArgumentException'); $networkService->sendRequestToApi('GET', '/'); }
Schlussfolgerungen
Dieser Ansatz ermöglicht es mir, besseren und verständlicheren Code zu schreiben, um beim Schreiben von Code die Vorteile von SOLID- und SRP-Ansätzen zu nutzen. Meine Tests wurden schneller und vor allem - sie kamen mir zugute.
Durch aktives Refactoring beim Erweitern oder Ändern der Funktionalität sehen wir sofort, was genau abfällt, und können Fehler schnell und genau korrigieren, ohne sie aus der lokalen Umgebung freizugeben. Dies macht die Fehlerkorrektur so billig wie möglich.
Ich hoffe, dass die von mir beschriebenen Prinzipien und Ansätze Ihnen helfen werden, mit Unit-Tests in Laravel umzugehen und Unit-Tests zu Ihren Assistenten bei der Code-Entwicklung zu machen.
Schreiben Sie Ihre Ergänzungen und Kommentare.