Tests unitaires utilisant une base de données : de nouvelles perspectives ?

L'utilisation d'une BD dans le cadre des tests unitaires est une chose qui est assez délicate, et ce pour plusieurs raisons. En effet, les tests utilisant les BD ont la réputation d'être lents, et il est difficile d'être sûr de rendre une BD dans le même état que lorsqu'on l'a prise, c'est à dire de trouver de bonnes manières de faire des opérations de setup et de teardown.

C'est pour cela qu'il est parfois pratique de partir d'une Golden Database, c'est à dire une base de données préparée pour les tests (par exemple, dans le cas de Magento, une base de données allégée, dans laquelle on a par exemple vidé les logs, les tables de cache, les commandes, et dans laquelle on laisse uniquement le minimum en terme de produits, clients,…).

PhpDbUnit

Pour les développeurs PHP, l'extension PHPDbUnit fournit des outils appréciables dans le cadre des tests unitaires utilisant une BD au travers de la classe PHPUnit_Extensions_Database_TestCase. Cette classe surcharge la classe PHPUnit_Framework_TestCase, en faisant les opérations de troncature et de remplissage des tables via les opérations de setUp et de tearDown. PHPDbUnit part du principe que l'état de la base est « inconnu », et qu'il faut nettoyer la BD et charger les données relatives au test. En pratique, il faut implémenter deux méthodes en surchargeant PHPUnit_Extensions_Database_TestCase :

  • getConnection() : cette méthode doit retourner une connection PDO vers la BD qui servira aux tests
  • getDataSet() : cette méthode doit retourner les données à charger pour les tests ; les données à retourner sont formalisées dans des structures de haut niveau, mais des implémentations spécialisées existent pour récupérer les données. Par exemple, depuis des fichiers XML, des fichiers de dump mysql, ou même des requêtes SQL

On comprend donc que grâce à ces deux éléments, il est possible de partir d'une Golden Database pour récupérer les informations de cette base modèle et les charger simplement dans une base de test.

Néanmoins, plusieurs problématiques se posent, dont notamment :

  • Les structures des données (tables au minimum) ne seraient pas créées par PHPDbUnit, ce qui veut dire qu'il faut utiliser un autre moyen pour préparer le « squelette » de la BD, comme par exemple un dump MySQL, ou la commande MySQLDbCopy. Cela signifie qu'il pourrait avoir lieu d'intégrer cette problématique par exemple dans une fixture annotée @beforeClass par exemple
  • phpdbunit ne tient pas compte des problématiques de clefs étrangères pour le chargement de données : il faut donc tenir compte de cela soi-même, et fournir les tables dans un ordre compatible avec les clefs étrangères
  • Dans le cadre de Magento, on a plusieurs centaines de tables contenant des données : idéalement, on souhaiterait pouvoir recharger toutes les données tout le temps, mais cela poserait évidemment des problématiques de performance.

De ce fait, il est peu envisageable de ne compter que sur PHPDbUnit pour initialiser toutes les données. Néanmoins, le système fourni par PHPDbUnit devrait permettre au minimum de remplacer les données qui ont été possiblement corrompues par les tests précédents au sein d'une même classe, voire de faire un nettoyage final dans un teardown annoté en @afterClass pour refournir une BD propre.

La limitation des données corrompues pourrait par exemple se faire en limitant les droits de Magento au niveau de l'accès à la BD, en préparant des comptes qui ne peuvent accéder en écriture qu'à un nombre de tables limité (et l'on pourra notamment forcer Magento à se recharger en utilisant Mage::reset, qui fait effectivement le travail de reset au niveau des structures de Magento, ce qui forcera la reprise en compte des sources de configuration).

Btrfs

Il faut trouver autre chose pour le chargement initial des données : c'est là que peut entrer en jeu un système de fichiers relativement nouveau : Btrfs. Btrfs (prononcé « Butter FS »), est un système de fichiers implémentant le Copy On Write : en fonction de la configuration de MySQL, MySQL peut tenir dans un faible nombre de fichiers, mais des fichiers à taille éventuellement conséquente.

En copiant tout d'abord l'ensemble des fichiers sur un FS Btrfs, on peut ensuite faire une copie qui va prendre peu de place via le paramètre –reflink de la commande cp : les données ne vont pas être dupliquées tout de suite : seuls les blocs de données qui changent seront dupliqués. Ceci laisse donc espérer un net gain d'espace disque et de performances par rapport à la copie des données brutes d'une instance MySQL. Néanmoins, d'autres problématiques apparaissent avec ce nouveau type de système de fichiers : par exemple il est nettement plus ardu de savoir quelle est la véritable quantité d'espace disque encore disponible. Un petit test sous une Ubuntu 14 LTS, simple à faire :


dd if=/dev/zero bs=1G count=3 of=btrfs.img
losetup /dev/loop0 btrfs.img
mkfs.btrfs /dev/loop0
mount /dev/loop0 /mnt/disk
service mysql stop
cp -dpRf /var/lib/mysql /mnt/disk
cp --reflink -Rp mysql mysql2

Ce test consiste donc à se créer un fichier vide (pour se réserver de la place sur le disque dur), de mapper le fichier sur un périphérique de loop, créer une partition btrfs dessus, copier les données de mysql (mysql arrêté), puis de créer une copie de ces données de référence avec le paramètre –reflink de cp. On peut ensuite chercher à voir la place utilisée / libre :


du -chs /mnt/disk
1,1G /mnt/disk
1,1G total


df -m /mnt/disk
Sys. de fichiers blocks de 1M Utilisé Disponible Uti% Monté sur
/dev/loop0 3072 568 2178 21% /mnt/disk


btrfs filesystem show /mnt/disk
Label: none uuid: 5a7af669-ca8e-4358-b3b0-493d680544af
Total devices 1 FS bytes used 563.41MiB
devid 1 size 3.00GiB used 983.12MiB path /dev/loop0


Btrfs v3.12


btrfs filesystem df /mnt/disk
Data, single: total=648.00MiB, used=559.01MiB
System, DUP: total=8.00MiB, used=16.00KiB
System, single: total=4.00MiB, used=0.00
Metadata, DUP: total=153.56MiB, used=4.39MiB
Metadata, single: total=8.00MiB, used=0.00

On constate que les informations sont divergentes et sont bien plus difficilement exploitables. On ne peut pas vraiment prédire à quel moment le disque dur va être plein. Ceci dit, la version de btrfs que j'ai utilisé est une version 3.12, livrée avec Ubuntu, et les développeurs de Btrfs sembleraient avoir pris conscience du problème de la présentation de l'information, d'après ce que j'ai pu lire lors de mes recherches.

Instances multiples de MySQL

Les données de MySQL copiées (je n'ai pas montré toutes les opérations de copie), on peut ensuite chercher à démarrer MySQL en utilisant ses données : comme MySQL peut prendre la plupart de ses données de configuration depuis la ligne de commande, on pourrait éventuellement envisager de préparer des scripts de clonage d'instances MySQL tournant sur un compte non privilégié (et chaque instance aurait évidemment un port spécifique, les ports utilisables automatiquement par Linux par exemple comme port source étant spécifiés dans /proc/sys/net/ipv4/ip_local_port_range, les autres étant à la disposition de l'administrateur système ou de l'utilisateur).

Ainsi, en utilisant l'affichage fourni par mysqld –verbose –help, on arrive à trouver les paramètres spécifiques à modifier pour pouvoir lancer plusieurs instances simultanément. Il est à noter que les paramètres peuvent diverger en fonction des configurations prévues par les distributions, où il y a aura alors plus ou moins de travail à fournir. Dans mon cas, j'ai par exemple réussi à démarrer une instance via :


mysqld --pid-file=/mnt/disk/mysql/mysql.pid --socket=/mnt/disk/mysql/mysqli.sock --port 3307 --basedir=usr --datadir=/mnt/disk/mysql2 --bind-address=127.0.0.1 --log-bin=/mnt/disk/log_mysql2/mariadb-bin –log-bin-index=/mnt/disk/log_mysql2/mariadb-bin.index

L'utilisation conjointe de Btrfs et d'instances multiples de MySQL semble donc être un couplage offrant de belles perspectives, qui pourrait par exemple permettre de lancer plusieurs sessions de tests simultanément par plusieurs développeurs, en proposant un temps de mise en œuvre relativement réduit, et une solution capable de tourner sans être super-utilisateur (du moment qu'on a une partition en Btrfs).

Néanmoins, il ne faut pas oublier que les tests unitaires intégrant des BD restent des environnements et des tests complexes, car si l'on utilise une Golden Database, il s'agit par exemple de la maintenir au fur et à mesure de l'évolution des développements, et connaître intimement son code pour essayer de limiter aux moindres privilèges les comptes utilisés pour accéder aux BD de tests.

De plus, les tests utilisant des BD, donc les tests portant sur des états, peuvent nécessiter des approches qui les rendent complexes à mettre en œuvre. Par exemple inclure des vérifications sur des préconditions (guard assertions) qui servent à vérifier si l'on est bien lors du setup dans une situation compatible avec le test que l'on veut faire, utiliser des vérifications différentielles (par exemple, nombre d'enregistrements avant et après opération), ou bien, écrire du code de teardown « le plus robuste possible».

Laisser un commentaire

Votre adresse de messagerie ne sera pas publiée. Les champs obligatoires sont indiqués avec *

Captcha *