2012-01-23. 2012-03-07. 2012-03-13. 2013-08-27. 2013-09-26. 2017-03-04. 2020-12-12.
-----
NEW ce 2020-12-12: Si une valeur doit être multiligne dans la base de données,
et sachant que le multiligne n'est pas supporté par Spyc et Yaml, nous avons
introduit la convention '\r\n': on remplace les sauts de ligne par cette séquence
dans le fichier _fixt.yml (en faisant à la main...) et application\third_party\CIUnit\libraries\Fixture.php
a été adapté pour restituer les vrais CRLF, CR ou LF avant d'insérer la valeur en base de données.
-----
Ce répertoire accueille les jeux de données ("data fixtures" ou "fixtures")
que nous générons à des fins d'utilisation dans les tests unitaires.
Son emplacement est figé dans le code et si cet emplacement devait changer pour une raison ou une autre
(l'emplacement actuel est aux cotés des tests unitaires PHPUnit et des tests d'acceptance Behat),
il faut modifier le code dans les fichiers suivants en recherchant le mot-cle "TESTSPATH":
application\third_party\CIUnit\libraries\CIUnitTestCase.php
features\implementations\BehatFixtureLoader.php
tests\fixtures\Controller_fixt.php
tests\fixtures\Controller_fixt2.php
tests\fixtures\view_fixt2.php
Dans le futur, on pourrait aussi rendre ce chemin paramétré depuis un endroit unique.
Ces fichiers sont au format YAML et sont générés avec l'aide de l'utilitaire
en ligne de commande generate_populated_fixtures.php (ou occasionnellement generate_empty_fixtures.php) (*).
Depuis le 2013-08-26, nous mettons ici indifférement les jeux de données pour Behat
avec le même format YAML. Behat et my-ciunit partagent donc désormais le même format
de data fixtures et nous n'avons pas à maintenir deux formats de données de test.
QUICK-QUICK-START
Génération données: I.1. QUICK-START
Utilisation dans tests: II.1. QUICK-START
I. GENERATION DE DONNEES DE TEST
================================
I.1. QUICK-START
++++++++++++++++
Faire:
$ php generate_populated_fixtures.php fixtures -f
Explications:
Pour créer un jeu de données sous forme de fichier YAML que nos tests unitaires savent charger facilement:
1. Dans la base de données _test et dans la concernée,
préparer un jeu de données adapté au test que l'on se prépare à exécuter.
2. Sauvegarder le jeu de données dans database\test\ pour minimiser
les risques de mauvaises manipulations dans la suite de la prodédure.
De préférence donner au fichier un nom parlant vis à vis du/des test(s) qu'il servira.
Lui donner également le suffixe _data puisqu'il s'agit d'un fichier de données.
Exemple: database\test\tauxinterests_banque1_data.sql.
3. Si un fichier précédent nommé tests\fixtures\_fixt.yml existe,
le renommer/sauvegarder car il sera écrasé SANS avertisement.
4. Depuis le répertoire tests\ et en ligne de commande, générer le jeu de données YAML:
> php generate_populated_fixtures.php fixtures -f
*** ATTENTION : Ne pas oublier le "-f" sous peine d'écraser non pas un mais TOUS LES FICHIERS
_fixt.yml pour lesquels correspond au nom d'une table de la base ! ***
*** ATTENTION: 2017/03/03. 2017/11/02.
{529DAD26-0F9F-4c7d-A449-6EECFBD3FAAD} Nécessité de pointer manuellement sur phpunit.phar:
Pour que cette génération marche, il faut vérifier que application/third_party/CIUnit/bootstrap_phpunit.php
pointe bien sur phpunit.phar avec un require_once(). Ce pointage est inutile pour les tests unitaires
en eux-mêmes mais ne leur nuit pas. On peut donc le laisser en place pour faciliter la génération de jeux de données.
5. Ouvrir immédiatement dans Notepad++ le fichier tests\fixtures\_fixt.yml généré et:
a. Le CONVERTIR en *ANSI*:
Notepad++ > Encodage > Convertir en ANSI // Et non pas "Encoder en ANSI"
Ceci évite les problèmes occasionnés par les caractères étendus lors des chargements de données.
b. NEW ce 2020-12-12: Si une valeur doit être multiligne dans la base de données,
et sachant que le multiligne n'est pas supporté par Spyc et Yaml, nous avons
introduit la convention '\r\n': on remplace les sauts de ligne par cette séquence
dans le fichier _fixt.yml (en faisant à la main...) et application\third_party\CIUnit\libraries\Fixture.php
a été adapté pour restituer les vrais CRLF, CR ou LF avant d'insérer la valeur en base de données.
OLD: 2017/10/11, 2017/11/26. LES MULTILIGNES NE SONT PAS ACCEPTEES.
Si le contenu d'un champ comporte des sauts de ligne, alors la génération produira aussi du multiligne.
C'est le cas notamment si la colonne est de type text (au lieu de varchar) et que la saisie a pu profiter des sauts de ligne.
C'est probablement le cas aussi si le texte est long et a été copié-collé d'ailleurs, tel que depuis MS Word.
Nous pourrions enfin être tentés de modifier les lignes à la main et introduire des lignes multiples
pour une même et unique valeur, par exemple dans le but d'aérer le texte et faciliter la lecture...:
=> Cela n'est pas bien lu par la bibliothèque Spyc qui rend un array mal formé
et qui à son tour PLANTE le chargement des fixtures fait par Fixture::load()
=> DONC VERIFIER AU CAS OU !!!!
6. Renommer immédiatement le fichier tests\fixtures\_fixt.yml en un nom plus parlant
vis à vis du/des test(s) qu'il servira. En principe c'est le même nom que celui du fichier
de sauvegarde _data.sql que l'on a créé précédemment.
Exemple: tests\fixtures\tauxinterests_banque1_fixt.yml.
Astuce: Il est très utile de comparer un fichier YAML précédent avec le nouveau
(avec Araxis Merge par exemple) pour bien vérifier qu'on a généré les données comme on le voulait.
I.2 DETAILS GENERATION DE DONNEES DE TEST
+++++++++++++++++++++++++++++++++++++++++++
Lire attentivement ce qui suit au moins une fois pour éviter les pièges de la génération
de données de test, potentiellement destructeur pour des jeux de données existants,
soit par écrasement soit par corruption des données (problèmes de caractères étendus...).
-----
Répertoires concernés par la génération
1. \tests // Utilitaires de génération de doonnées de test
2. \tests\fixtures // Données de test transformées au format YAML
3. \database\test // Optionnel. Eventuelles données sources au format .SQL
-----
Prérequis: une base de données de test dédiée, pas de DRI :
-----------------------------------------------------------
1. Une base de données nommée _test
Notre framework ne sait pas charger directement des jeux de données SQL
mais plutôt des données au format YAML (*). Des mécanismes sont fournis
par ce framework pour que ces fichiers YAML soient générables à partir
d'une base de données.
Or, nous avons toujours deux bases de données dans nos applications:
l'une nommee %database%_dev pour les tests humains et l'autre nommee
%database%_test pour les tests unitaires.
C'est cette deuxième base de données %database%_test que notre
générateur de jeu de données s'attend à trouver OBLIGATOIREMENT.
2. Pas d'itégrité référentielle déclarative (DRI) entre les tables
Les mécanismes de chargement de données de test ainsi que TRUNCATE TABLE
ne supportent pas bien les intégrités référentielles déclaratives car celles-ci
empêchent souvent de vider les tables. Il ne FAUT donc pas mettre en place ces DRI,
du moins pendant toute la période de développement, sous peine d'avoir des tests
qui ne peuvent définir leurs données correctement.
La génération des données de test
-----------------------------------------
1. Créer/modifier les jeux de données de tests.
On peut s'aider pour cela de jeux de données constituées dans
le répertoire database\test en les chargeant préalablement et
en les modifiant/adaptant ensuite, ou encore en modifiant une
copie du script xxx_data.sql et le chargeant ensuite.
Exemple:
* Copier le fichier database\test\tauxinterets_data.sql vers tauxinterets_banque1_data.sql
* Modifier/adapter les données de tauxinterets_banque1_data.sql à sa convenance.
* Tronquer la table et charger les données adaptées:
SQLyog: USE
SQLyog: TRUNCATE TABLE ;
SQLyog: DB | Restore From SQL Dump... | File to execute | database\test\tauxinterets_banque1_data.sql | Execute
2. Générer le jeu de données avec la commande fournie.
*** ATTENTION à ne pas oublier le "-f" sous peine d'écraser
tous les fixtures correspondant à une table en db ! ***:
\tests> php generate_populated_fixtures.php fixtures -f %matable%
Le fichier %matable%_fixt.yml correspondant sera généré dans \tests\fixtures (ici).
ATTENTION 1: Si on ne fournit pas le "-f %matable%" les jeux de données
de *toutes* les tables seront générés, en *écrasant* les fichiers précédents.
Très dangereux ; voir ci-après.
ATTENTION 2: Cette commande ECRASE le fixture précédent !
Comme il n'y a pas d'option pour indiquer un nom alternatif pour le fichier YAML,
veiller donc à faire une sauvegarde préalable du fichier antérieur et utiliser
un differ pour comparer ce qui est sauvegardé avec ce qui a été généré.
ATTENTION 3: Contrairement à ce que dit la documentation originale de l'outil
(my-ciunit, http://d.hatena.ne.jp/Kenji_s/20120118/1326848578), on n'indique plus
le nombre de lignes à générer. L'utilitaire génèrera toutes les lignes de la table
jusqu'à la limite de 200 enregistrements (**).
ALTERNATIVE: On peut aussi générer un fichier YAML correctement structuré
mais vierge de données comme suit. De plus cette commande n'écrase pas les
fichiers existants, y compris si on omet l'option "-f %matable%":
\tests> php generate_empty_fixtures.php fixtures -f %matable%
NOTE: Ce n'est pas la peine de générer des jeux de données de la sorte
pour les tables de référence. En effet, les données de ces tables ne bougent
pas pendant les tests et il y a donc lieu de les ternir pré-chargées,
sans les mouvementer inutilement.
3. Ouvrir le fichier YAML généré et *si* celui-ci est en "UTF-8 (sans BOM)" le convertir en ANSI .
Sans cette action, les données re-chargées dans la table seront tronquées au premier caractère
étendu rencontré et il sera très difficile de déceler un tel problème sur les données:
Ouvrir tests\fixtures\%matable%_fixt.yml
Notepad++ > Encodage > Convertir en ANSI (à ne *pas* confondre avec "Encoder en ANSI").
EXPLICATIONS: Si SQLyog voit que les données d'une table contient des caractères étendus,
il va générer un fichier .SQL au format UTF-8 sans BOM au lieu de générer habituellement
un fichier ANSI.
Conséquement, si SQLyog(?) voit un fichier .SQL au format UTF-8 sans BOM en entrée,
les données ayant des caractères étendus seront tronquées à la première rencontre avec
un tel caractère. Pour y rémédier: Après génération des fichiers .YAML, ouvrir ceux-ci
dans Notepad++ et si l'encodage est du UTF-8 sans BOM, aller dans le menu Encodage et
choisir "Convertir en ANSI" pour le ramener dans le droit chemin...
Noter bien que c'est contre-intuitif car nous avons l'habitude de passer les fichiers
vers de l'UTF-8, mais c'est ainsi dans ce cas particulier.
4. Si certaines des colonnes de la table générée font plus que le classique VARCHAR(255)
en taille, toujours en ayant le fichier YAML ouvert, vérifier visuellement qu'aucune
donnée n'est renvoyée à la ligne intempestivement par le générateur (***).
Si tel est le cas, remettre la valeur sur une seule et unique ligne ;
sans quoi la ligne ne sera pas chargée correctement et les tests seront
difficiles à déboguer (ou peut-être que le chargement lui-même plantera).
II. UTILISATION LES DONNEES DANS LES TESTS UNITAIRES
====================================================
II.1. QUICK-START
++++++++++++++++++
2017/03/28.
* Ne pas utiliser la variable magique $tables, lui préférer la méthode $this->dbfixt(array(...));
* Mettre dans setUp() ce qui doit etre exécuté au début de *chaque* test
* Mettre dans tearDown() ce qui doit etre exécuté au début de *chaque* test
* Si on a des initialisations partagées par tous et que l'on peut utiliser
public static function setUpBeforeClass() {...} alors privilégier cette méthode.
Sinon créer une pseudo-méthode nommée testSetupOnce() où on fera les initialisations.
et déclarer dans les commentaires en-tête de chaque test "@depends testSetupOnce".
FAIRE TRES ATTENTION à cette technique car elle est caduque dès que l'on agit
en modification sur la base de données (insert, update, delete).
SITUATION LEGITIME: lorsqu'on charge des données en lecture seule et que
l'on ne désire pas les recharger au début de chaque test individuel.
Les cas d'utilisation types mélangent chargements et troncatures,
et mélangent également chargements initiaux et chargements au cas par cas.
La meilleure pratique que nous avons identifée est donc la suivante:
class SomeTest extends CIUnit_TestCase {
// On fait les chargements et les troncatures dont chaque méthode de test a besoin
// au début de son exécution. Pour rappel, setUp() est exécuté pour chaque test
// (chaque méthode)
public function setUp(): void
{
// N'est utile que si on a utilise la variable magique $tables
// qui sera alors parcourue pour tronquer-charger les tables listees.
// Voir les details dans CIUnitTestCase::setUp().
// parent::setUp();
// Charger les données requises au début de chaque test (chaque méthode).
// Les tables sont tronquées puis chargées.
$this->dbfixt(array(
"%matable1%" => "%monfixture1%",
"%matable2%" => "%monfixture2%"
)
);
// Purger les autres tables qui doivent être vierges au début de chaque méthode.
$CI = &get_instance();
$Query = $CI->db->query("TRUNCATE TABLE %matable3%");
$Query = $CI->db->query("TRUNCATE TABLE %matable4%");
}
// On peut se discipliner à purger les tables à la fin de chaque test,
// ce qui laisse un contexte propre pour les tests suivants ;
// mais qui peut aussi servir d'indicateur comme quoi les tests
// se sont exécutés jusqu'au bout.
public function tearDown(): void
{
// N'est utile que si on a utilise la variable magique $tables
// qui sera alors parcourue pour tronquer les tables listees.
// Voir les details dans CIUnitTestCase::tearDown().
// parent::tearDown();
$CI = &get_instance();
$Query = $CI->db->query("TRUNCATE TABLE %matable1%");
$Query = $CI->db->query("TRUNCATE TABLE %matable2%");
$Query = $CI->db->query("TRUNCATE TABLE %matable3%");
$Query = $CI->db->query("TRUNCATE TABLE %matable4%");
}
// On peut initier un contexte à tout moment,
// à tout endroit, et de multiples fois si nécessaire.
// C'est donc la méthode pour des jeux de données propres à un test spécifique.
public function testUnTestQuiSeDonneSonPropreContexte() {
$this->dbfixt(array(
'%matable1%' => '%monfixture1%',
'%matable2%' => '%monfixture2%',
)
);
// Do test...
// No need to truncate in the end if tearDown() takes care of final clean-up.
}
// Other tests
}
II.2. DISCUSSIONS SUR LES MEILLEURES PRATIQUES
++++++++++++++++++++++++++++++++++++++++++++++
1. METHODE 1. DECONSEILLEE. Utiliser la liste magique $tables offerte par my-ciunit
--------------------------------------------------------------------------------
Dans l'esprit de CodeIgniter, my-ciunit autorise une variable spéciale
nommée $tables (avec "s") que l'on créera au niveau de la classe et
à laquelle on donnera une portée protected.
On fournira à cette variable la liste des jeux de données à charger comme suit:
class SomeTest extends CIUnit_TestCase {
// Déclarer les données requises pour le test, à charger automatiquement
protected $tables = array(
"%matable1%" => "%monfixture1%", // Signifiant: "Pour la table %matable1% => charger %monfixture1%_fixt.yml"
"%matable2%" => "%monfixture2%", // Signifiant: "Pour la table %matable2% => charger %monfixture2%_fixt.yml"
);
public function setUp(): void
{
parent::setUp(); // OBLIGATOIRE pour que la variable magique $tables soit prise en compte.
// ...
}
public function tearDown(): void
{
parent::tearDown(); // Facultative: Activer si on veut que les tables soient vidées en fin de tests.
// ...
}
// ...
}
On remarquera qu'il faut OBLIGATOIREMENT appeler la méthode parent::setUp(),
et le cas échéant parent::tearDown(). C'est un inconvénient majeure qui nous
conduit à abandonner l'usage de cette fonctionnalité.
Explications: Pour rappel, les tests dans notre framework maison étendent CIUnit_TestCase
et non plus le traditionnel PHPUnit_Framework_TestCase. Or cette nouvelle classe de base,
exécute systématiquement deux méthodes de son cru, setUp() et tearDown() et dans la première,
la variable magique $tables et traitée pour **tronquer puis charger** les données des tables listées .
Note: Contrairement à ce que dit la documentation de my-ciunit cette technique PEUT s'utiliser
depuis n'importe quel type de test du moment qu'il hérite de CIUnit_TestCase, qu'il concerne
les modèles, les controleurs, les helpers, ou autres (http://d.hatena.ne.jp/Kenji_s/20120118/1326848578).
2. METHODE 2. PREFEREE. Utiliser la méthode de chargement explicite dbfxit()
---------------------------------------------------------------
Si la premiere technique suffit pour les cas simples, la technique suivante s'avère plus pratique
car elle permet le setup de ses données depuis n'importe quel endroit du code, et notamment dans
la méthode de test elle-même.
Utiliser un méthode de chargement appelée dbfixt(), soit au moment même où
on a besoin des données (spécifique à ce test précis), soit dans le setUp()
de la classe (global dans ce cas aux tests de la classe):
// Dans le setUp() ou juste avant le besoin
$this->dbfixt(array(
'%matable1%' => '%monfixture1%',
'%matable2%' => '%monfixture2%',
)
);
Nous avons aussi adapté cette méthode avec succès pour le chargement de données pour Behat.
Voir plus loin cette adaptation.
3. Tronquer les tables au début [et à la fin] d'un test
----------------------------------------------------
A d'autres moments, ce n'est pas de chargements de données dont nous avons besoin,
mais d'une troncature pour avoir des tables vierges. Nous pourrions recourir à des
fichiers de données _fixt.yml vides et utiliser l'une des deux syntaxes précédentes,
mais il est plus simple de recourir aux fonctionnalités de CodeIgniter:
public function setUp(): void
{
// Purger les tables pour partir d'une situation connue
$CI = &get_instance();
$Query = $CI->db->query("TRUNCATE TABLE %matable1%");
$Query = $CI->db->query("TRUNCATE TABLE %matable2%");
}
public function tearDown(): void
{
// [Optionnel] Purger une nouvelle fois les tables afin de laisser la base dans l’état où nous l’avons trouvée
$CI = &get_instance();
$Query = $CI->db->query("TRUNCATE TABLE %matable1%");
$Query = $CI->db->query("TRUNCATE TABLE %matable2%");
}
4. Dans la pratique nous avons trouvé que l'utilisation de $tables était trop limitative:
----------
* D'une part la liste n'est pas prise en compte si on n'exécute pas obligatoirement
la méthode parent::setUp(); et le vidage automatique ne se fera pas si n'exécute
pas la méthode parent::tearDown(); => Ceci est error-prone car il est facile d'oublier
d'exécuter ces méthodes.
* D'autre part le chargement se fait pour toute la classe, c'est à dire pour tout le jeu de tests,
limitant la possibilité de se donner différents contextes pour chacun des tests inclus.
* Enfin, l'éclatement des lieux d'initialisation entre $tables et setUp()/tearDown()
rend le code moins clair, comparé à l'utilisation de setUp()/tearDown() directement.
Cela peut être géner le développeur débutant ou inattentionné.
=> Nous préférons donc dbfixt() et n'utilisons pas $tables.
V. UTILISER LES DONNES DANS LES SPECIFICATIONS BEHAT
=====================================================
En étant dans un fichier %SomeFeature%Context.php qui a besoin de charger des données,
il suffit de charger l'utilitaire BehatFixtureLoader et dans le constructeur
de faire appel à la méthode LoadFixture() pour charger les fichiers YAML appropriés:
// L'utilitaire de chargement de fixtures adaptés de my-ciunit.
require_once "BehatFixtureLoader.php";
class %SomeFeature%Context extends BehatContext
{
public function __construct(array $parameters)
{
// ...
// Charger les données requises pour le test
$DataLoader = new BehatFixtureLoader();
$DataLoader->LoadFixture(array(
'%matable1%' => '%monfixture1%',
'%matable2%' => '%monfixture2%',
)
);
}
// ...
}
V. FICHIERS A NE PAS ENLEVER
=============================
Les fichiers suivants ne doivent pas être supprimés de ce sous-répertoire
car outre le présent fichier readme-ibonia.txt, ce sont des utilitaires requis
par my-ciunit pour la génération des jeux de données:
Controller_fixt.php
Controller_fixt.php
view_fixt.php
view_fixt2.php
VI. FOOTNOTES
============
(*) Initialement ces fichiers s'appelaient respectivement "generate.php"
pour la génération populée avec écrasement des fichiers antérieurs et
"generate" (du PHP également) pour la génération vide sans écrasement.
Nous avons renommé ces deux fichiers en generate_populated_fixtures.php
et generate_empty_fixtures.php pour lever les ambiguïtés et éviter les
accidents sur les données.
(**) Documentation initiale: http://d.hatena.ne.jp/Kenji_s/20120118/1326848578
(***) Le générateur de données YAML de my-ciunit voudrait faire un word-wrap
par souci de lisibilité pour l'humain. Mais ce wordwrap s'est montré problématique
pour le bon chargement des données de test. Nous avons donc modifié le fichier
application\third_party\CIUnit\libraries\spyc\spyc.php vers la ligne 222 afin
d'augmenter le seuil de wrapping: "$this->_dumpWordWrap = 259; // 40 originellement."
NOTE: Comparer périodiquement en comparant d'éventuelles évolutions
concernant la gestion des données de test qui seraient notés par
inadvertance dans \database\readme-ibonia.txt.