Skip to content

Symfony 4 & TDD - PHPUnit - Vue.js & Jest - (dockerized workshop)

License

Notifications You must be signed in to change notification settings

jprivet-dev/symfony-tdd

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Symfony 4 & TDD (dockerized workshop)

master Build Status Coverage Status Codacy code quality
dev Build Status Coverage Status Codacy code quality

Makefile GitHub

Sommaire

1. Présentation

Ce dépôt Git fraîchement créé est un atelier où je concentre, teste et partage des cas pratiques rencontrés (projets clients, R&D, …​) autour des tests automatiques, du TDD (Test-Driven Development) et du framework Symfony.

J’ai démarré cet atelier à partir du dépôt https://github.com/dunglas/symfony-docker de Kevin Dunglas.

2. Prérequis technique : avoir Docker CE et Docker Compose installés

3. Installer le projet

3.1. Cloner

$ git clone [email protected]:jprivet-dev/symfony-tdd.git

3.2. Installer

$ cd symfony-tdd
$ make install
...
...
...
READY!
  Website:    http://localhost
  API:        http://localhost/api
  phpMyAdmin: http://localhost:8088

APP_ENV=dev
💡

Des commandes Makefile sont à votre disposition (voir Le Makefile du projet). Cependant, si vous n’avez pas un outil du type GNU Make disponible, exécutez à la place de $ make install l’alias suivant :

$ . .bash_aliases
symfony-tdd: aliases loaded

$ project-install

Pour plus d’informations, voir Les alias du projet.

3.3. Accéder au site

A la fin de l’installation, vous pourrez avoir accès aux URLs suivantes :

4. Le Makefile du projet

Si vous avez un outil du type GNU Make disponible sur votre poste, vous pouvez acceder à toutes les commandes du fichier Makefile :

$ make

Liste des commandes disponibles :

PROJECT
  start                          Project: Start the current project.
  start.one                      Project: Stop all containers & start the current project.
  stop                           Project: Stop the current project.
  sh                             Project: app sh access.

  install                        Project: Install all (dependencies, data, assets, ...) according to the current environment (APP_ENV).
  install.dev                    Project: Force the installation for the "dev" environment.
  install.prod                   Project: Force the installation for the "prod" environment.

  dependencies                   Project: Install the dependencies (only if there have been changes).
  assets                         Project: Generate all assets according to the current environment (APP_ENV).
  assets.dev                     Project: Generate all assets (webpack Encore, ...) for the "dev" environment.
  assets.prod                    Project: Generate all assets (webpack Encore, ...) for the "prod" environment.
  data                           Project: Install the data (db).
  fixtures                       Project: Load all fixtures.

  check                          Project: Launch of install / Composer, Security and DB validations / Tests
  tests                          Project: Launch all tests.
  coverage                       Project: Generate & open all code coverage reports.

  cc                             Project: Clear all caches.
  clean                          Project: [PROMPT yN] Remove build, vendor & node_modules folders.

ENVIRONMENT
  env.app                        Environment: Print current APP_ENV in Makefile.
  env.local.dev                  Environment: Alias of `env.local.clean`.
  env.local.prod                 Environment: [PROMPT yN] Copy '.env.local.prod.dist' into '.env.local' (APP_ENV=prod)
  env.local.test                 Environment: [PROMPT yN] Copy '.env.local.test.dist' into '.env.local' (APP_ENV=test)
  env.local.clean                Environment: [PROMPT yN] Remove '.env.local' and use default vars & environment of '.env' (APP_ENV=dev)

COMPOSER
  composer.install               Composer: Read the composer.json/composer.lock file from the current directory, resolve the dependencies, and install them into vendor.
  composer.install.prod          Composer: Idem `composer.install` without dev elements.
  composer.update                Composer: Get the latest versions of the dependencies and update the composer.lock file.
  composer.licenses              Composer: List the name, version and license of every package installed.
  composer.validate              Composer: Check if your composer.json is valid. | https://getcomposer.org/doc/03-cli.md#validate
  composer.dumpenv.prod          Composer: Dump .env files for "prod".

YARN
  yarn.install                   Yarn: Install all dependencies.
  yarn.upgrade                   Yarn: Upgrade packages to their latest version based on the specified range.

ENCORE
  encore.compile                 Webpack Encore: Compile assets once.
  encore.watch                   Webpack Encore: Recompile assets automatically when files change.
  encore.deploy                  Webpack Encore: On deploy, create a production build.

SYMFONY
  symfony.cc                     Symfony: Clear cache (current env).
  symfony.ccp                    Symfony: Clear cache (prod).
  symfony.cchard                 Symfony: Remove all in `var/cache` folder.
  symfony.routes                 Symfony: Display current routes.

  symfony.about                  Symfony: Display information about the current project (Symfony, Kernel, PHP, Environment, ...).
  symfony.env.vars               Symfony: List defined environment variables. | https://symfony.com/doc/current/configuration.html#configuration-based-on-environment-variables

  symfony.security.check         Symfony: Check security of your dependencies. | https://github.com/sensiolabs/security-checker

ALICE BUNDLE
  alice.fixtures.load            AliceBundle: load fixtures.

PHPUNIT
  phpunit                        PHPUnit: Launch all tests (unit, functional, ...).
  phpunit.coverage               PHPUnit: Generate code coverage report in HTML format.
  phpunit.coverage.clover        PHPUnit: Generate code clover style coverage report.
  phpunit.coverage.open          PHPUnit: Open code coverage report.

  phpunit.unit                   PHPUnit: Launch unit tests.
  phpunit.unit.coverage          PHPUnit: Generate code coverage report in HTML format for unit tests.
  phpunit.functional             PHPUnit: Launch functional tests.
  phpunit.functional.coverage    PHPUnit: Generate code coverage report in HTML format for functional tests.

  phpunit.watch                  PHPUnit Watcher: Rerun automatically tests whenever you change some code. | https://github.com/spatie/phpunit-watcher
  phpunit.watch.unit             PHPUnit Watcher: Rerun only unit tests.
  phpunit.watch.functional       PHPUnit Watcher: Rerun only functional tests.

XDEBUG
  xdebug.on                      Xdebug: Enable the module.
  xdebug.off                     Xdebug: Disable the module.

QUALITY ASSURANCE - STATIC ANALYZERS
  qa.phpmetrics                  PHPMetrics: Provide tons of metric (complexity / volume / object oriented / maintainability). | http://www.phpmetrics.org
  qa.codesniffer                 PHP_CodeSniffer: Tokenize PHP, JavaScript and CSS files and detect violations... | https://github.com/squizlabs/PHP_CodeSniffer
  qa.codesniffer.diff            PHP_CodeSniffer: Printing a diff report
  qa.codesniffer.fix             PHP_CodeSniffer: Fixing errors automatically
  qa.messdetector                PHP Mess Detector: Scan PHP source code and look for potential problems... | http://phpmd.org/

DATABASE
  db.create                      Database: Creates the configured database & Executes the SQL needed to generate the database schema.
  db.create.force                Database: Drop & create.
  db.drop                        Database: Drop.
  db.update                      Database: Generate & execute a Doctrine migration.

  db.validate                    Database: Validate the mapping files.
  db.entities                    Database: List mapped entities.
  db.bash                        Database: Bash access.
  db.mysql                       Database: MySQL access (mysql> ...).

DOCTRINE
  doctrine.database.create       Doctrine: Create the configured database.
  doctrine.database.create.force Doctrine: Drop & create the configured database.
  doctrine.database.drop         Doctrine: Drop the configured database.

  doctrine.schema.validate       Doctrine: Validate the mapping files.
  doctrine.mapping.info          Doctrine: List mapped entities.

  doctrine.migrations.diff       Doctrine: Generate a migration by comparing your current database to your mapping information.
  doctrine.migrations.migrate    Doctrine: Execute a migration to the latest available version.
  doctrine.migrations.migrate.nointeract Doctrine: Execute a migration to the latest available version (no interaction).

DOCKER
  docker.start                   Docker: Build, (re)create, start, and attache to containers for a service (detached mode). | https://docs.docker.com/compose/reference/up/
  docker.start.one               Docker: Stop all projects running containers & Start current project.
  docker.build                   Docker: Same `docker.start` command + build images before starting containers (detached mode). | https://docs.docker.com/compose/reference/up/
  docker.build.force             Docker: Stop, remove & rebuild current containers.
  docker.stop                    Docker: Stop running containers without removing them. | https://docs.docker.com/compose/reference/stop/
  docker.stop.all                Docker: Stop all projects running containers without removing them. | https://docs.docker.com/compose/reference/stop/
  docker.down                    Docker: [PROMPT yN] Stop containers and remove containers, networks, volumes, and images created by up. | https://docs.docker.com/compose/reference/down/

  docker.list                    Docker: List containers. | https://docs.docker.com/engine/reference/commandline/ps/
  docker.list.stopped            Docker: List all stopped containers.
  docker.remove                  Docker: [PROMPT yN] Stop & Remove service containers (only current project). | https://docs.docker.com/compose/reference/rm/
  docker.remove.all              Docker: [PROMPT yN] Remove all stopped service containers. | https://docs.docker.com/compose/reference/rm/
  docker.images                  Docker: List images. | https://docs.docker.com/engine/reference/commandline/images/
  docker.images.remove.all       Docker: [PROMPT yN] Remove all unused images (for all projects!).
  docker.clean                   Docker: [PROMPT yN] Remove unused data. | https://docs.docker.com/engine/reference/commandline/system_prune/

  docker.env                     Docker: Show environment variables.
  docker.ip                      Docker: Get ip Gateway.
  docker.ip.all                  Docker: List all containers ip.
  docker.networks                Docker: list networks. | https://docs.docker.com/engine/reference/commandline/network/
  docker.logs                    Docker: Show logs.

UTIL
  util.chown.fix                 Util (Permissions): Editing permissions on Linux. | https://github.com/dunglas/symfony-docker#editing-permissions-on-linux
  util.readme.update             Util (Readme.adoc): Retrieve and insert the latest makefile commands & aliases in the Readme.adoc.
  util.php.strict                Util (PHP): Insert `<?php declare(strict_types=1);` instead of `<?php` in all PHP files in src/ & tests/ folders.
  util.ide.phpstorm.templates    Util (PHPStorm): Copy templates from .ide/PHPStorm/fileTemplates folder in .idea/fileTemplates folder. | https://www.jetbrains.com/help/phpstorm/using-file-and-code-templates.html

MAKEFILE
  help                           Makefile: Print self-documented Makefile.
  list                           Makefile: List all included files.

5. Les alias du projet

Le fichier .bash_aliases propose quelques raccourcis (php, composer, yarn, sf, …​) :

alias reload=". .bash_aliases"

alias app="docker-compose exec app"
alias composer="app composer"
alias yarn="app yarn"
alias php="app php"
alias phpunit="app ./vendor/bin/simple-phpunit"
alias phpunit-watch="app ./vendor/bin/phpunit-watcher watch"
alias symfony="php bin/console"

alias cc="symfony cache:clear"
alias ccp="symfony cache:clear --env=prod"

alias tests="phpunit --stop-on-error --stop-on-failure --stop-on-warning"
alias tests-no-stop="phpunit"
alias tests-coverage="phpunit --coverage-html build/phpunit/coverage"
alias tests-watch="phpunit-watch"
alias open-coverage="gio open build/phpunit/coverage/index.html"

alias m="make"
alias sf="symfony"
alias t="tests"
alias tnostop="tests-no-stop"
alias tc="
tests-coverage;
open-coverage;
"
alias tw="tests-watch"
alias ut="make unit-tests"
alias ft="make functional-tests"

alias chownfix="docker-compose run --rm app chown -R $(id -u):$(id -g) ."

alias project-install="
docker-compose up --remove-orphans -d;
docker-compose exec app composer install --verbose;
docker-compose exec app yarn install;
docker-compose exec app php bin/console doctrine:database:drop --if-exists --force;
docker-compose exec app php bin/console doctrine:database:create;
docker-compose exec app php bin/console doctrine:schema:create;
"

Charger les alias du projet :

$ . .bash_aliases
Le fichier .bash_aliases ne peut être chargé automatiquement à la commande start du Makefile.

6. PHPUnit : gérer les tests automatiques PHP

6.1. Lancer les tests avec PHPUnit

Le projet utilise le PHPUnit Bridge de Symfony (https://symfony.com/doc/current/testing.html).

Pour lancer les tests, chargez d’abord les fixtures :

$ make fixtures

Exécutez ensuite les tests :

$ make phpunit
...
...
...
Testing
.........................................                         41 / 41 (100%)

Time: 2.25 seconds, Memory: 24.00 MB

OK (41 tests, 91 assertions)
💡

Si vous n’avez pas un outil du type GNU Make disponible, lancer les tests avec les commandes suivantes :

$ docker-compose exec app php bin/console hautelook:fixtures:load
$ docker-compose exec app ./vendor/bin/simple-phpunit
📎
La commande $ make tests charge les fixtures et lance tous les tests disponibles.

6.2. PHPUnit Watcher : relance automatiquement des tests après modification d’un fichier

Le projet utilise PHPUnit Watcher (https://github.com/spatie/phpunit-watcher) que vous pouvez lancer avec la commande suivante :

$ make phpunit.watch
💡

Si vous n’avez pas un outil du type GNU Make disponible, lancer le watcher avec la commande suivante :

$ docker-compose exec app ./vendor/bin/phpunit-watcher watch

6.3. Xdebug & Performance : activation et désactivation à chaud du module

⚠️
Xdebug est nécessaire pour générer la couverture de code, mais augmente considérablement (x10) le temps d’exécution des tests.

Exécution avec Xdebug1.52 secondes :

$ docker-compose exec app ./vendor/bin/simple-phpunit
stty: standard input
PHPUnit 8.4.1 by Sebastian Bergmann and contributors.

Testing
................................                                  32 / 32 (100%)

Time: 1.52 seconds, Memory: 24.00 MB

OK (32 tests, 74 assertions)

Exécution sans Xdebug153 ms :

$ docker-compose exec app ./vendor/bin/simple-phpunit
stty: standard input
PHPUnit 8.4.1 by Sebastian Bergmann and contributors.

Error:         No code coverage driver is available

Testing
................................                                  32 / 32 (100%)

Time: 153 ms, Memory: 18.00 MB

OK (32 tests, 74 assertions)
💡

Xdebug peut être activé et désactivé à chaud avec les commandes suivantes :

$ make xdebug.on
$ make xdebug.off

Xdebug est automatiquement désactivé pour les tests qui ne nécessitent pas de couverture de code et réactivé dans le cas contraire.

Exemple de commandes avec Xdebug désactivé automatiquement :

$ make phpunit
$ make phpunit.unit
$ make phpunit.functional
$ make phpunit.watch
...

Exemple de commandes avec Xdebug activé automatiquement :

$ make phpunit.coverage
$ make phpunit.coverage.clover
$ make phpunit.unit.coverage
$ make phpunit.functional.coverage
...

7. Sujets traités

7.1. Nomenclature

  1. [ ] A faire

  2. [!] En cours

  3. [x] Fait

7.2. [x] Fibonacci : 4 implémentations, 1 seul test

Principe

Le principe est de montrer que 4 implémentations différentes d’une même fonctionnalité peuvent passer correctement le même test unitaire.

Ce premier cas simple permet d’illustrer ce que permettent les tests automatiques : garantir le code.

Qu’importe la stratégie d’implémentation choisie par le développeur (en fonction du contexte, de ses facilités, du temps qui lui ait imparti, …​), ce dernier peut garantir au client que son implémentation répond bien aux besoins dans le scope testé, et que la fonctionnalité réagit bien dans les cas limites retenus.

Exemple

Pour une application de Planning Poker, nous avons besoins d’une méthode qui puisse nous retourner les 12 premiers termes de la suite de Fibonacci.

Ces termes (1, 2, 3, 5, …​, 55, 89, 144) seront les valeurs de nos cartes agiles.

7.3. [x] Injecter un repository au lieu de l’entity manager

Principe

Au lieu d’injecter dans un premier temps l’entity manager pour récupérer dans un deuxième temps les repositories dont nous avons besoin, nous pouvons injecter directement les repositories concernés.

Exemple

Pour récupérer et traiter les news enregistrées en base de données, le NewsService.php de l’exemple suivant importe et utilise NewsRepository.php.

7.4. [x] Créer et tester un repository

Principe

Le principe est de pouvoir vérifier les requêtes d’un repository, en les testant directement sur la base de données.

Exemple

Le repository NewsRepository permet de traiter des news. Nous voulons vérifier les points suivants :

  1. Récupérer toutes les news.

  2. Récupérer uniquement celles qui sont publiées.

  3. Récupérer par son slug une news publiée.

  4. Retourner une valeur null si le slug est inconnu, ou si la news n’est pas publiée.

💡
Nous devons injecter des fixtures dans la base de données pour réaliser ces tests. Voir PHPUnit : gérer les tests automatiques PHP.

7.5. [x] Créer et tester une classe abstraite

Principe

Le principe est de pouvoir tester unitairement les méthodes concrètes d’une classe abstraite.

Exemples

Le premier exemple est réalisé avec une classe abstraite très simple AbstractClass, pour présenter 3 méthodes de tests élémentaires :

  1. Avec getMockForAbstractClass().

  2. Avec une classe anonyme new class().

  3. Avec une simple classe Dummy.

Le deuxième exemple est réalisé avec la classe abstraite AbstractRepository, utiliser dans [x] Injecter un repository au lieu de l’entity manager.

7.7. [ ] Créer et tester un custom type

7.8. [ ] Créer et tester une fonction Twig

7.9. [!] Créer un smoke testing

Principe

Le principe de ce premier niveau de test fonctionnel est d’appeler chaque page de l’application pour vérifier qu’aucune d’entre elles ne retournent d’erreur.

Exemple

…​

7.10. [ ] Créer, dispatcher et tester un event

7.11. [ ] Mocker le temps

7.12. [ ] Créer et tester une commande Symfony

7.13. [ ] Créer et tester une requête ajax

7.14. [ ] Travailler avec le workflow component

7.15. [ ] Travailler avec le process bundle

7.16. [ ] Travailler avec des collections d’objets typés

7.17. [ ] La performance avec Doctrine

7.18. [ ] Créer et tester un trait

7.19. [ ] Créer et tester un composant Vue.js

7.20. [ ] Créer et tester des éléments asynchrones PHP

7.21. [x] Symfony Panther : les bases (Vue.js, screenshots, …​)

Principe

Le principe est de pouvoir tester fonctionnellement une page dans laquelle est utilisé du JavaScript.

Exemple

Nous testons fonctionnellement une page qui affiche une news, dont les commentaires sont récupérés et affichés dynamiquement avec un composant Vue.js.

📎
Retrouvez les screenshots réalisés automatiquement par ces tests dans le dossier build/tests/screenshots.

Autres informations

💡

Docker : Bien intégrer le binaire chromedriver avec une image alpine. Voir :

⚠️

Panther ne permet pas de générer une couverture de code pour le moment. Voir :

8. Tips

8.1. [!] Makefile : En local, utiliser les fichiers .env de Symfony

File Scope Environment Commited

.env

all machines

all

yes

.env.local

machine-specific

all

should not be committed

.env.<env>

all machines

<env>

yes

.env.<env>.local

machine-specific

<env>

should not be committed

8.2. [x] Makefile : Script d’attente de disponibilité de la base de données

Problématique rencontrée

Après avoir démarré les conteneurs avec, par exemple, $ make install :

Starting symfony_tdd_db_service    ... done
Starting symfony_tdd_app_service ... done
Starting symfony_tdd_nginx_service      ... done
Starting symfony_tdd_phpmyadmin_service ... done
Starting symfony_tdd_h2_proxy_service   ... done

Vous pouvez avoir, tout juste après, l’erreur suivante qui s’affiche au moment de la création de la base :

ERROR 2002 (HY000): Can't connect to local MySQL server through socket '/var/run/mysqld/mysqld.sock' (2)

C’est une erreur qui apparait, en particulier, à la toute première installation et qui vous stoppera toute la procédure : le symfony_tdd_db_service est bien done, mais l’initialisation de MySQL n’est qu’en à lui pas encore finie.

Solution

C’est pour cela qu’il existe la commande db.wait suivante :

PHONY: db.wait
db.wait: # Database: Wait database...
	@$(PHP) -r 'echo "\e[0;43mWait database $(DATABASE_HOST):$(DATABASE_PORT)...\e[0m\n"; \
	set_time_limit(15); for(;;) { if(@fsockopen($(DATABASE_HOST), $(DATABASE_PORT))) { break; }}; echo "\e[0;42mDatabase ready!\e[0m\n";'

Cette commande peut être couplée à toutes les commandes Makefile ayant une action avec la base. Comme dans le cas suivant par exemple, où l’on attend que la base soit disponible avant de vouloir s’y connecter avec le terminal :

PHONY: db.mysql
db.mysql: db.wait ## Database: MySQL access (mysql> ...).
	$(EXEC_DB) bash -c "mysql -u $(DATABASE_USER) $(DATABASE_NAME)"

8.3. [ ] Tests unitaires : Utiliser les yield

8.5. [x] Docker : libérer la mémoire

On peut facilement être saturé de plusieurs dizaines de Go de données créées par Docker.

Astuce 1 : Supprimer les données non utilisées

Dans un premier temps, il est possible de supprimer tout ce qui n’est plus utilisé par Docker :

$ docker system prune --volumes
💡
Retrouvez dans la documentation plus de commandes de suppression sur Le Makefile du projet.

Astuce 2 : Changer le dossier de travail de Docker

Pour une gestion à long terme, il est préférable d’orienter Docker vers un espace de travail plus volumineux sur votre machine, avec le fichier de configuration daemon.json.

1) Stopper Docker :
$ sudo service docker stop
2) Créer le nouveau dossier de destination :
$ sudo mkdir /data/home/jprivet/docker
3) Vérifier si daemon.json existe :
$ ls /etc/docker
key.json
4) Si daemon.json n’existe pas, le créer :
$ sudo touch /etc/docker/daemon.json
4 bis) Injecter l’option "data-root": "/data/home/jprivet/docker" dans le nouveau fichier daemon.json :
$ sudo -- sh -c "echo '{\"data-root\": \"/data/home/jprivet/docker\"}' >> /etc/docker/daemon.json"
📎

Si le fichier daemon.json existe déjà, le modifier directement :

$ sudo vim /etc/docker/daemon.json
5) Vérifier le contenu du fichier daemon.json :
$ cat /etc/docker/daemon.json
{"data-root": "/data/home/jprivet/docker"}
6) Redémarrer Docker :
$ sudo service docker start

Au prochain $ docker-compose up, les éléments seront créés dans le nouveau dossier /data/home/jprivet/docker.

8.6. [!] Fixtures : comment, dans son test, en récupérer une avec son id ?

Exemple : récupérer le slug d’une news

Nous avons des fixtures dans le fichier news.yaml suivant :

App\Entity\News:
  news_published_1:
    slug: 'week-601'
    title: 'A week of symfony #601 (2-8 July 2018)'
    body: '...'
  news_published_2:
    slug: 'symfony-live-usa-2018'
    title: 'Join us at SymfonyLive USA 2018!'
    body: '...'
  news_not_published_1:
    slug: 'not-published-news'
    title: 'Not published news'
    body: '...'

Dans le test NewsRepositoryTest, il est possible d’avoir accès par défaut à la liste des fixtures chargées et de pointer la news news_published_1 :

class NewsRepositoryTest extends RepositoryWebTestCase
{
    public function testFindOnePublishedBySlug()
    {
        // Arrange
        $news = self::$fixtures['news_published_1']; // (1)
        $slug = $news->getSlug();

        // Act
        $news = $this->repository->findOnePublishedBySlug($slug);

        // Assert
        $this->assertInstanceOf(News::class, $news);
        $this->assertSame($slug, $news->getSlug());
    }
}
  1. Accès par défaut au tableau des fixtures (sans typage de la donnée récupérée).

Avec le fichier tests/Shared/Fixtures/FixturesDecorator.php de ce repo, il est possible de récupérer directement une fixture typée, ce qui facilite l’autocomplétion dans votre IDE :

class NewsRepositoryTest extends RepositoryWebTestCase
{
    public function testFindOnePublishedBySlug()
    {
        // Arrange
        $news = $this->fixtures()->news('news_published_1'); // (1)
        $slug = $news->getSlug();

        /* ... */
    }
}
  1. Récupération d’une fixture typée.

8.7. [ ] Symfony Panther : activer la couverture de code