Bonjour la famille et bienvenue dans ce tutoriel.
Aujourd'hui nous allons voir en ensemble le TDD avec Laravel.
Il y a une citation célèbre de James Grenning, l'un des pionniers des méthodologies de développement TDD et Agile :
<<If you're not doing test-driven development, you're doing debug-later development>>
Développement piloté par les tests
Test Driven Development (TDD) est une approche de développement logiciel dans laquelle des cas de test sont développés pour spécifier et valider ce que le code va faire. En termes simples, les cas de test pour chaque fonctionnalité sont créés et testés en premier et si le test échoue, le nouveau code est écrit afin de réussir le test et de rendre le code simple et sans bogue.
Le développement piloté par les tests commence par la conception et le développement de tests pour chaque petite fonctionnalité d'une application. TDD demande aux développeurs d'écrire un nouveau code uniquement si un test automatisé a échoué. Cela évite la duplication de code. La forme complète de TDD est le développement piloté par les tests.
Le concept simple de TDD est d'écrire et de corriger les tests qui ont échoué avant d'écrire un nouveau code (avant le développement). Cela permet d'éviter la duplication du code lorsque nous écrivons une petite quantité de code à la fois afin de réussir les tests. (Les tests ne sont rien d'autre que des conditions d'exigence que nous devons tester pour les remplir).
Le développement piloté par les tests est un processus de développement et d'exécution de tests automatisés avant le développement réel de l'application. Par conséquent, TDD est parfois également appelé Test First Development.
Comment effectuer un test TDD
Les étapes suivantes définissent comment effectuer le test TDD,
- Ajoutez un test.
- Exécutez tous les tests et voyez si un nouveau test échoue.
- Écrivez du code.
- Exécutez des tests et du code Refactor.
- Répéter.
Nous allons apprendre de manière pratique, alors allons-y et créez un nouveau projet Laravel (au moment de la rédaction de ce document, la dernière version de Laravel est 8). Laravel inclut PHPUnit prêt à l'emploi, bien que vous souhaitiez peut-être l'installer globalement sur votre machine en exécutant :
$ composer global require phpunit/phpunit
Le dossier à la racine Test contient les dossiers et fichiers suivants:
- tests
- Feature
- ExampleTest.php
- Unit
- ExampleTest.php
- CreatesApplication.php
- TestCase.php
Le CreatesApplication.php
trait amorce une application Laravel pour nos tests et est utilisé dans la TestCase.php
classe.
La TestCase.php
est la classe de base à partir de laquelle tous vos tests Laravel s'étendent. Cette classe s'étend d'une autre classe Laravel, qui à son tour s'étend de la TestCase
classe de PHPUnit .
Les deux dossiers (Feature et Unit) sont utilisés pour stocker respectivement les tests de fonctionnalités et les tests unitaires. Il n'y a pas de réelle différence entre les deux. Vous devriez utiliser des tests unitaires pour tester des unités individuelles (par exemple une seule classe) et des tests de fonctionnalités pour tester des choses plus complexes.
Avant de continuer, assurez-vous que PHPUnit fonctionne et que les deux exemples de tests réussissent. Exécutez vendor/bin/phpunit
(ou juste phpunit
si vous l'avez installé globalement) à partir du dossier racine de votre projet Laravel. Vous devriez voir quelque chose comme ceci:
$ Mes articles\Article\tdd-crud-article> vendor/bin/phpunit
PHPUnit 9.4.3 by Sebastian Bergmann and contributors.
.. 2 / 2 (100%)
Time: 00:00.207, Memory: 20.00 MB
OK (2 tests, 2 assertions)
Notez qu'avec le temps, le nombre de tests dans votre projet augmentera considérablement et l'exécution de toute la suite de tests à chaque fois sera assez lente et inutile. Au lieu de cela, vous pouvez exécuter une seule classe de test ou une méthode de test. Exécutez simplement phpunit –filter
suivi d'un nom de classe ou de méthode de test, par exemple phpunit --filter ExampleTest
ou phpunit --filter testExample
.
Très bien, maintenant allez-y et supprimez les deux ExampleTest.php
fichiers, nous n'en aurons plus besoin.
Préparation de la base de données
Nous allons développer un module CRUD de gestion des articles simple afin de démontrer le flux de travail TDD. Mais avant cela, préparons notre base de données. Créez un nouveau modèle avec une migration correspondante en exécutant:
$ php artisan make:model Articles -m
Notre article aura un titre, un corps et une image principale affichés en haut de la page.
Voici à quoi ressemble la migration:
Schema::create('article', function (Blueprint $table) {
$table->id();
$table->string('title');
$table->string('body');
$table->string('image_path')->nullable();
$table->timestamps();
});
Créez maintenant votre premier test pour tester la création de nouveaux articles:
$ php artisan make:test CreateNewsTest
Cela créera un nouveau fichier tests/Feature/CreateNewsTest.php
. Il contient un exemple de test à l'intérieur et si vous l'exécutez, phpunit
il passera.
Chaque fichier de test et nom de classe doivent se terminer par "Test". Chaque nom de méthode de test doit commencer par "test". Si vous souhaitez appeler votre méthode différemment, écrivez une /** @test */
annotation avant la déclaration de méthode. C'est ce que nous allons faire pour donner aux méthodes des noms plus descriptifs.
Ouvrez le fichier de test. Vous remarquerez qu'il importe le use Illuminate\Foundation\Testing\RefreshDatabase;
. Allez-y et utilisez-le dans notre classe de test en ajoutant use RefreshDatabase;
au début de la classe.
class CreateNewsTest extends TestCase
{
use RefreshDatabase;
...
}
Cette ligne annule toutes les modifications apportées à la base de données lors de l'exécution d'une méthode de test, de sorte qu'au moment où la méthode de test suivante est exécutée, la base de données est à son état initial. Cela garantit que les données d'un test n'interfèrent pas avec les résultats d'autres tests et que l'ordre dans lequel vous effectuez les tests n'a pas d'importance.
Si vous exécutez vos tests maintenant, cela échouera car nous n'avons pas encore configuré de connexion à la base de données dans le fichier .env
. Au lieu de cela, nous allons le configurer dans le fichier phpunit.xml
. Ouvrez le fichier et défiler vers le bas jusqu'à ce que vous voyiez la section <php>
, c'est là que les variables env
pour l'environnement de test sont définies et celles-ci ne prendront effet que lorsque vous exécutez des tests PHPUnit. Ici, vous pouvez remplacer toutes les valeurs de votre fichier .env
.
Nous allons utiliser la base de données SQLite en mémoire pour nos tests. La base de données en mémoire est stockée dans la RAM, par conséquent, elle est très rapide (par rapport, par exemple, à MySQL). Toutes les données sont détruites une fois les tests terminés, et comme il s'agit d'une base de données séparée, il n'y aura aucune interférence avec votre base de données principale et vos données.
Le seul inconvénient ici est que la syntaxe et le comportement de SQLite sont légèrement différents de la syntaxe et du comportement des autres bases de données que vous allez utiliser, que ce soit MySQL, PostgreSQL ou une autre base de données SQL. La plupart du temps, tout fonctionnera correctement, mais vous rencontrerez parfois des situations où vos tests échouent, même si le code réel fonctionne parfaitement. C'est quelque chose à garder à l'esprit et il existe des solutions de contournement pour certains cas.
D'accord, ajoutez maintenant ces lignes ou commenter les si elle existe à l'intérieur du php
bloc du phpunit.xml
comme suit:
<php>
...
<env name="DB_CONNECTION" value="sqlite"/>
<env name="DB_DATABASE" value=":memory:"/>
</php>
Si vous exécutez phpunit
maintenant, le test réussira, sauf si vous n'avez pas installé SQLite sur votre système, dont vous devrez vous occuper vous même, il suffit de rechercher sur Google. Nous sommes maintenant prêts à commencer le développement (test) proprement dit.
Jetons d'abord un coup d'œil à la méthode testExample
dans le fichier CreateNewsTest.php
que nous avons créé . Il y a une ligne de code à l'intérieur qui contient une affirmation:
public function testExample()
{
$response = $this->get('/');
$response->assertStatus(200);
}
PHPUnit teste notre code via différentes assertions. Nous affirmons que certaines conditions ont été remplies après que certaines actions aient été effectuées. Dans ce cas, nous affirmons que Status
est 200
et bien sûr que c'est le cas - donc le test réussit.
Plusieurs affirmations peuvent vous aider à tester les résultats de toutes sortes d'appels dans vos applications. Parfois, vous devez être un peu plus créatif pour tester une fonctionnalité plus complexe, mais les assertions fournies par PHPUnit couvrent la majorité des cas que vous souhaitez tester. Voici une liste des plus courants que vous utiliserez dans vos tests:
- AssertTrue: Vérifiez l'entrée pour vérifier qu'elle est égale à true.
- AssertFalse: Vérifiez l'entrée pour vérifier qu'elle est égale à la valeur fausse.
- AssertEquals: Comparer le résultat avec une autre entrée pour une correspondance.
- AssertArrayHasKey (): Signale une erreur si le tableau n'a pas la clé.
- AssertGreaterThan: Vérifiez le résultat pour voir s'il est supérieur à une valeur.
- AssertContains: Vérifie que l'entrée contient une certaine valeur.
- AssertType: Vérifie qu'une variable est d'un certain type.
- AssertNull: Vérifier qu'une variable est nulle.
- AssertFileExists: Vérifier qu'un fichier existe.
- AssertRegExp: Vérifie l'entrée par rapport à une expression régulière.
CRUD ARTICLE
Commençons par écrire notre première vraie méthode de test (supprimez la méthode par défaut testExample
).
class CreateNewsTest extends TestCase
{
use RefreshDatabase;
/** @test */
public function authenticated_users_can_create_new_articles()
{
//
}
}
Alors, à quoi ressemble le processus de création d'un nouvel article ? Eh bien, nous avons probablement des données de formulaire. Nous devons envoyer ces données de formulaire à un point de terminaison et nous nous attendons à ce qu'un article soit créé et stocké dans la base de données en réponse. C'est exactement ce que nous allons tester. Ne vous inquiétez pas du mot <<authentifié>> dans le nom de la méthode - nous nous en occuperons un peu plus tard.
Juste pour vérifier la cohérence, affirmons qu'il n'y a pas de articles dans notre base de données avant d'effectuer des actions. C'est une bonne pratique pour être sûr à 100%, même si c'est à vous de décider si vous êtes vraiment sûr que la base de données est vide.
/** @test */
public function authenticated_users_can_create_new_articles()
{
$this->assertEquals(0, Articles::count());
}
assertEquals()
affirme que deux valeurs sont égales. Mettez toujours la valeur attendue comme premier paramètre. Nous affirmons simplement que le nombre de tous les nouveaux articles de la base de données est égal à 0
. D'accord, maintenant, chaque article de notre site a un titre, un corps et une image facultative. Préparons les données, bien que sans image pour l'instant.
$data = [
'title' => 'Coder Autrement.',
'body' => 'Apprend deja a faire du TDD'
];
Nous devons maintenant envoyer une demande au serveur pour conserver ces données. J'aime utiliser des routes nommées, c'est donc ce que nous allons utiliser ici aussi. Nous créons un nouvel enregistrement de base de données, donc la méthode de requête devrait être POST
. Laravel facilite grandement la création de requêtes internes (API) dans les tests:
$this->postJson(route('articles.store'), $data)
->assertStatus(201);
Nous envoyons une demande postJson()
pour indiquer au serveur que nous attendons une réponse JSON. Cela facilite les choses lorsque vous souhaitez tester que votre serveur renvoie l'objet nouvellement créé en réponse ou lorsque vous développez une API. Vous pouvez enchaîner plusieurs méthodes ici. assertStatus(201)
est une assertion spécifique à Laravel qui affirme que la réponse a été renvoyée avec un statut correct. Il ne peut être utilisé que sur un objet Response
.
Nous devons maintenant nous assurer que les nouveaux articles ont été conservés dans la base de données. Nous n'avions aucun enregistrement de base de données auparavant, alors maintenant nous devrions en avoir exactement un, ce qui est notre nouvel article.
$this->assertEquals(1, Article::count());
$articles= Article::first();
Enfin, vérifions que les champs de l'article ont été correctement remplis. Et nous en avons terminé avec le test, voici toute la méthode:
/** @test */
public function authenticated_users_can_create_new_articles()
{
$this->withoutExceptionHandling();
$this->assertEquals(0, Article::count());
$data = [
'title' => 'Coder Autrement.',
'body' => 'Apprend deja a faire du TDD'
];
$this->json('POST', '/articles',$data) ->assertStatus(201);
$this->assertEquals(1, Article::count());
$articles= Article::first();
$this->assertEquals($data['title'], $articles->title);
$this->assertEquals($data['body'], $articles->body);
}
Nous pouvons maintenant démarrer des itérations pour écrire le code back-end. Exécutez le test en exécutant
$ phpunit --filter authenticated_users_can_create_new_articles
et vous devriez voir une erreur comme ceci :
InvalidArgumentException: Route [articles.store] not defined
En effet, nous n'avons pas défini cette route. Ouvrez le fichier routes/web.php
et définissez-le comme ceci :
Route::post('/articles','App\Http\Controllers\ArticlesController@store');
Exécutez à nouveau le test et vous verrez une erreur différente :
Expected status code 201 but received 500.
Failed asserting that false is true.
Ce n'est pas vraiment utile, hein? 500 est une erreur de serveur générique qui ne révèle aucune information spécifique. C'est le mécanisme de gestion des exceptions de Laravel qui nous cache la véritable racine du problème. Corrigeons ça en ajoutant ce code tout en haut notre method test comme ceci:
$this->withoutExceptionHandling();
Relancer le test:
ReflectionException: Class App\Http\Controllers\ArticlesController does not exist
Ah! Il s'avère que nous devons créer le fichier ArticlesController
. Exécutez
$ php artisan make:controller ArticlesController
puis réexécutez le test. Maintenant, vous aurez:
BadMethodCallException: Method App\Http\Controllers\ArticlesController::store does not exist.
Allez-y et créez cette méthode à l'intérieur du ArticlesController
contrôleur:
public function store()
{
//
}
En passant, c'est ainsi que vous développez votre back-end avec TDD en procédant étape par étape, les étapes étant définies par le test que vous avez écrit Ok, relancez le test.
Expected status code 201 but received 200.
La réponse d'action par défaut du contrôleur a le statut 200. Écrivons une solution rapide pour le moment.
public function store()
{
return request()->wantsJson()
? response()->json([], 201)
: null;
}
Si la demande attend une réponse JSON, renvoyez une réponse JSON. Sinon, retournez quelque chose d'autre probablement une redirection ou une vue, vous pouvez vous en occuper lorsque vous développerez votre front-end. Pour l'instant, cela n'a pas d'importance. Relancez le test :
Failed asserting that 0 matches expected 1.
Nous n'avons rien stocké dans la base de données, c'est pourquoi cette assertion $this->assertEquals(1, News::count());
échoue. Créons un nouvel article dans la méthode du contrôleur, puis retournons le nouvel objet dans la réponse :
$articles = Articles::create([
'title' => request('title'),
'body' => request('body'),
]);
return request()->wantsJson()
? response()->json($articles, 201)
: null;
De plus, vous devez rendre les champs assignables en masse en ajoutant cette ligne dans le modèle Articles.php
:
protected $guarded = [];
Exécutez à nouveau votre test et il passera !
$ Mes articles\Article\tdd-crud-article> vendor/bin/phpunit
PHPUnit 9.4.3 by Sebastian Bergmann and contributors.
.. 2 / 2 (100%)
Time: 00:00.429, Memory: 24.00 MB
OK (2 tests, 6 assertions)
C'est rapide et sale, nous n'avons pas de vérification d'authentification ni de validation en place, mais au moins nous avons un prototype brut qui fonctionne dans les conditions parfaites.
Vous pourriez écrire des tests pour tester la validation de chaque champ, mais imaginez si vous aviez un formulaire complexe avec beaucoup de champs ? Ce serait fastidieux d'écrire tous les tests, même si cela garantit une qualité encore meilleure de votre projet. Je pense qu'il n'y a pas de limite avec les tests - vous pouvez être aussi scrupuleux ou aussi superficiel que vous le souhaitez, en testant les performances générales ou chaque petit détail.
Nous allons implémenter la validation sans écrire de tests pour raccourcir ce tutoriel. Ajoutez ceci au début de votre méthode store
dans le contrôleur :
request()->validate([
'title' => 'required|string|max:255',
'body' => 'required|string',
'image' => 'mimes:jpeg,png,gif|nullable'
]);
C'est un standard de Laravel et nous allons simplement supposer que cela fonctionne parfaitement. Le test devrait toujours réussir puisque nous avons fourni des données valides.
Mots finaux
Ces notions (les validations, l'authentification, le téléchargement de l'image d'un article, ...) pouvaient être abordées. Pour ne pas être très long, je vais conclure ce tutoriel sur cette note. Nous avons vue en ensemble comment démarrer le TDD avec Laravel et vous êtes maintenant prêt à partir seul(e). J'espère que vous utiliserez désormais l'approche TDD dans vos projets réels.
Merci d'avoir lu ce tutoriel et à bientôt!
Participe à la discussion