Parmi les discussions dans la communauté, j'entends souvent l'opinion que les tests unitaires à Laravel sont faux, compliqués, et les tests eux-mêmes sont longs et ne donnent aucun avantage. Pour cette raison, peu écrivent ces tests, se limitant uniquement aux tests de fonctionnalités, et l'utilisation de tests unitaires tend à 0.
J'ai aussi pensé une fois, mais une fois que j'y ai pensé et que je me suis demandé - peut-être que je ne sais pas comment les cuisiner?
Pendant un certain temps, j'ai compris et à la sortie, j'ai eu une nouvelle compréhension des tests unitaires, et les tests sont devenus clairs, amicaux, rapides et ont commencé à m'aider.
Je veux partager ma compréhension avec la communauté, et encore mieux comprendre ce sujet, rendre mes tests encore meilleurs.
Un peu de philosophie et de limites
Laravel est une sorte de cadre à certains endroits. Surtout en termes de façades et d'éloquent. Je n'aborderai pas les discussions ou les condamnations de ces points, mais je montrerai comment je les combine avec des tests unitaires.
J'écris des tests après (ou en même temps) l'écriture du code principal. Peut-être que mon approche ne sera pas compatible avec l'approche TDD ou nécessitera des ajustements partiels.
La question la plus importante que je me pose avant de passer un test est "qu'est-ce que je veux tester exactement?". C'est un problème important. C'est cette idée qui m'a permis de reconsidérer mon point de vue sur l'écriture de tests unitaires et le code du projet lui-même.
Les tests doivent être stables et dépendre au minimum de l'environnement. Si, lorsque vous effectuez des mutations, vos tests échouent, ils sont très probablement bons. A l'inverse, s'ils ne tombent pas, ils ne sont probablement pas très bons.
Prêt à l'emploi, Laravel prend en charge 3 types de tests:
- Navigateur
- Fonctionnalité
- Unité
Je parlerai principalement des tests unitaires.
Je ne teste pas tout le code via des tests unitaires (ce n'est peut-être pas correct). Je ne teste pas du tout le code (plus de détails ci-dessous).
Si des moques sont utilisés dans les tests, n'oubliez pas de faire Mockery :: close () sur tearDown.
Certains exemples de tests sont «extraits d'Internet».
Comment je teste
Ci-dessous, je vais regrouper les exemples de test par groupe de classe et essayer de donner des exemples de test pour chaque groupe de classe. Pour la plupart des groupes de classe, je ne donnerai pas d'exemples du code lui-même.
Middleware
Pour le test unitaire du middleware, je crée un objet de la classe Request, un objet du middleware souhaité, puis j'appelle la méthode handle et exécute les assertions nécessaires. Le middleware en fonction des actions effectuées peut être divisé en 3 groupes:
- modification de l'objet de demande (modification de la demande de corps ou des sessions)
- redirection (modification du statut de la réponse)
- ne rien faire avec l'objet de requête
Essayons de donner un exemple de test pour chaque groupe:
Supposons que nous ayons le middleware suivant, dont la tâche consiste à modifier le champ de titre:
class TitlecaseMiddleware { public function handle($request, Closure $next) { if ($request->title) { $request->merge([ 'title' => title_case($request->title) ]); } return $next($request); } }
Un test pour un middleware similaire pourrait ressembler à ceci:
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); }); }
Les tests pour les groupes 2 et 3 seront d'un tel plan, respectivement:
$response = $middleware->handle($request, function () {}); $this->assertEquals($response->getStatusCode(), 302);
Classe de demande
La tâche principale de ce groupe de classe est l'autorisation et la validation des demandes.
Je ne teste pas ces classes à travers des tests unitaires (j'avoue que ce n'est peut-être pas vrai), seulement à travers des tests de fonctionnalités. À mon avis, les tests unitaires sont redondants pour ces classes, mais j'ai trouvé quelques exemples intéressants de la façon dont cela peut être fait. Peut-être qu'ils vous aideront si vous décidez de tester votre classe d'unité de demande avec des tests:
Contrôleur
Je ne teste pas non plus les contrôleurs via des tests unitaires. Mais lorsque je les teste, j'utilise une fonctionnalité dont je voudrais parler.
Les contrôleurs, à mon avis, devraient être légers. Leur tâche est d'obtenir la bonne demande, d'appeler les services et référentiels nécessaires (puisque ces deux termes sont «étrangers» à Laravel, je vais expliquer ma terminologie ci-dessous), renvoyez la réponse. Déclenche parfois un événement, un travail, etc.
Par conséquent, lors des tests par le biais de tests de fonctionnalités, nous devons non seulement appeler le contrôleur avec les paramètres nécessaires et vérifier la réponse, mais également verrouiller les services nécessaires et vérifier qu'ils sont réellement appelés (ou non). Parfois - créez un enregistrement dans la base de données.
Un exemple de test de contrôleur avec une maquette de classe de service:
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); }
Un exemple de test de contrôleur avec une maquette de façade (dans notre cas, un événement, mais par analogie se fait pour d'autres façades Laravel):
public function testChangeCart() { Event::fake(); $user = factory(User::class)->create(); Passport::actingAs( $user ); $response = $this->post('/api/v1/cart/update', [ 'products' => [ [
Service et référentiels
Ces types de classes sont prêts à l'emploi. J'essaie de garder les contrôleurs minces, alors j'ai mis tout le travail supplémentaire dans l'un de ces groupes de classe.
J'ai déterminé la différence entre eux comme suit:
- Si j'ai besoin d'implémenter une logique métier, je la mets dans la couche de service (classe) appropriée.
- Dans tous les autres cas, je l'ai mis dans le groupe de classes de référentiel. En règle générale, le fonctionnel avec Eloquent va là-bas. Je comprends que ce n'est pas tout à fait la définition correcte du niveau de référentiel. J'ai également entendu dire que certains endurent tout ce qui concerne Eloquent dans le modèle. Mon approche est une sorte de compromis, à mon avis, bien que "académiquement" ne soit pas tout à fait vrai.
Pour les classes de référentiel, je n'écris presque pas de tests.
Exemple de test de classe de service ci-dessous:
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
Ces classes sont testées presque selon le principe général - nous préparons les données nécessaires aux tests; Nous appelons la classe souhaitée depuis le framework et vérifions le résultat.
Exemple pour l'auditeur:
public function testHandle() { $user = factory(User::class)->create(); $cart = Cart::create([ 'userId' => $user->id,
Console console
Je considère les commandes de la console comme une sorte de contrôleur qui peut en outre produire (et effectuer des manipulations plus complexes avec les entrées-sorties de la console décrites dans la documentation). En conséquence, les tests sont similaires au contrôleur: nous vérifions que les méthodes de service nécessaires sont appelées, les événements sont déclenchés (ou non), et nous vérifions également l'interaction avec la console (sortie ou demande de données).
Un exemple d'un test similaire:
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); }
Bibliothèques externes séparées
En règle générale, si des bibliothèques distinctes ont des fonctionnalités pour les tests unitaires, elles sont décrites dans la documentation. Dans d'autres cas, le travail avec ce code est testé de la même manière que la couche service. Cela n'a aucun sens de couvrir les bibliothèques elles-mêmes avec des tests (uniquement si vous voulez envoyer des RP à cette bibliothèque) et vous devez les considérer comme une sorte de boîte noire.
Sur de nombreux projets, je dois interagir via l'API avec d'autres services. Laravel utilise souvent la bibliothèque Guzzle à ces fins. Il m'a semblé commode de regrouper tout le travail avec d'autres services dans une classe distincte du service NetworkService. Cela m'a facilité l'écriture et le test du code principal, et a aidé à standardiser les réponses et la gestion des erreurs.
Je donne des exemples de plusieurs tests pour ma classe NetworkService:
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', '/'); }
Conclusions
Cette approche me permet d'écrire du code meilleur et plus compréhensible, de tirer parti des approches SOLID et SRP lors de l'écriture de code. Mes tests sont devenus plus rapides et, surtout, ils ont commencé à me profiter.
Avec une refactorisation active lors de l'extension ou de la modification des fonctionnalités, nous voyons immédiatement ce qui tombe exactement et nous pouvons corriger rapidement et précisément les erreurs sans les libérer de l'environnement local. Cela rend la correction d'erreurs aussi bon marché que possible.
J'espère que les principes et les approches que j'ai décrits vous aideront à gérer les tests unitaires dans Laravel et à faire des tests unitaires vos assistants dans le développement de code.
Écrivez vos ajouts et commentaires.