- 1. Présentation
- 2. Prérequis technique : avoir Docker CE et Docker Compose installés
- 3. Installer le projet
- 4. Le Makefile du projet
- 5. Les alias du projet
- 6. PHPUnit : gérer les tests automatiques PHP
- 7. Sujets traités
- 7.1. Nomenclature
- 7.2. [x] Fibonacci : 4 implémentations, 1 seul test
- 7.3. [x] Injecter un repository au lieu de l’entity manager
- 7.4. [x] Créer et tester un repository
- 7.5. [x] Créer et tester une classe abstraite
- 7.6. [!] Créer et tester un custom validator
- 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
- 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, …)
- 8. Tips
- 8.1. [!] Makefile : En local, utiliser les fichiers .env de Symfony
- 8.2. [x] Makefile : Script d’attente de disponibilité de la base de données
- 8.3. [ ] Tests unitaires : Utiliser les yield
- 8.4. [!] Contrôleurs : doit-on les tester unitairement ?
- 8.5. [x] Docker : libérer la mémoire
- 8.6. [!] Fixtures : comment, dans son test, en récupérer une avec son id ?
- 8.7. [ ] Symfony Panther : activer la couverture de code
- 9. Autres ressources
- 10. Licence
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.
$ cd symfony-tdd
$ make install
...
...
...
READY!
Website: http://localhost
API: http://localhost/api
phpMyAdmin: http://localhost:8088
APP_ENV=dev
💡
|
Des commandes
Pour plus d’informations, voir Les alias du projet. |
A la fin de l’installation, vous pourrez avoir accès aux URLs suivantes :
-
Site web : https://localhost
-
API : https://localhost/api
-
phpMyAdmin: http://localhost:8088
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.
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.
|
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 :
|
📎
|
La commande $ make tests charge les fixtures et lance tous les tests disponibles.
|
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 :
|
|
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 Xdebug ⇒ 1.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 Xdebug ⇒ 153 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 :
|
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
...
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.
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.
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.
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
.
Le principe est de pouvoir vérifier les requêtes d’un repository, en les testant directement sur la base de données.
Le repository NewsRepository
permet de traiter des news. Nous voulons vérifier les points suivants :
-
Récupérer toutes les news.
-
Récupérer uniquement celles qui sont publiées.
-
Récupérer par son slug une news publiée.
-
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. |
Le principe est de pouvoir tester unitairement les méthodes concrètes d’une classe abstraite.
Le premier exemple est réalisé avec une classe abstraite très simple AbstractClass
,
pour présenter 3 méthodes de tests élémentaires :
-
Avec
getMockForAbstractClass()
. -
Avec une classe anonyme
new class()
. -
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.
Le principe est de gérer et de tester facilement tous les cas limites auxquels pourrait-être exposé notre custom validator.
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.
Le principe est de pouvoir tester fonctionnellement une page dans laquelle est utilisé du JavaScript.
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 .
|
💡
|
Docker : Bien intégrer le binaire |
|
Panther ne permet pas de générer une couverture de code pour le moment. Voir : |
File | Scope | Environment | Commited |
---|---|---|---|
|
all machines |
all |
yes |
|
machine-specific |
all |
should not be committed |
|
all machines |
<env> |
yes |
|
machine-specific |
<env> |
should not be committed |
-
https://www.gnu.org/software/make/manual/html_node/Environment.html
-
https://symfony.com/doc/current/configuration.html#managing-multiple-env-files
-
https://symfony.com/doc/current/configuration.html#configuring-environment-variables-in-production
-
https://symfony.com/blog/new-in-symfony-4-2-define-env-vars-per-environment
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.
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)"
On peut facilement être saturé de plusieurs dizaines de Go de données créées par Docker.
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. |
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
.
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
|
$ cat /etc/docker/daemon.json
{"data-root": "/data/home/jprivet/docker"}
$ sudo service docker start
Au prochain $ docker-compose up
, les éléments seront créés dans le nouveau dossier /data/home/jprivet/docker
.
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());
}
}
-
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();
/* ... */
}
}
-
Récupération d’une fixture typée.
https://github.com/jprivet-dev/symfony-tdd est publié sous licence MIT.