Tests unitaires pour Google App Engine
Les tests unitaires des applications GAE posent les problèmes classiques des applications déployées dans des conteneurs : peut-on tester les classes hors de son contexte cible, peut-on simuler le conteneur, ou faut-il déployer l’application pour la tester ?
La première solution est celle qui s’approche le plus de l’unicité du test, il doit donc être privilégié autant que possible, c’est la technique qui est utilisée pour les POJOs sans contexte d’exécution. En revanche, c’est tout à fait impossible si notre classe a besoin d’une API fournie par le conteneur ou, pire encore, hérite d’une classe ou implémente une interface fournie par celui-ci. Ces cas sont classiques en JavaEE. Dans ce domaine, la tendance a été de remplacer le conteneur par des objets mock ou fake.
Nous allons étudier ce qui est proposé par Google pour son App Engine.
Contraintes des tests
Une application App Engine peut utiliser plusieurs types de services fournis par Google, comme le mail, la manipulation d’images ou la gestion de tâches. N’ayant encore aucun de services dans mon application, j’estime urgent d’attendre le besoin de ces techniques avant d’évaluer la façon de tester le code qui les utilise.
Bien plus important que ces services, mon application utilise la persistance GAE, avec JPA. Dans un billet précédent, sur la persistance dans Google App Engine, j’avais estimé que JPA était une bonne façon de commencer, mais que l’API bas niveau pouvait aussi être intéressante. Ce choix exclu d’entrée de remplacer le moteur JPA de Google par une autre implémentation. D’ailleurs, les spécificités du JPA façon GAE excluent presque systématiquement cette solution, à moins d’utiliser DataNucleus et Hbase ; pas sûr qu’on simplifie le problème…
Bref, pour tester mes classes persistantes, il va falloir que je reproduise un contexte de persistance dans mon environnement de développement. Enfin, quand je dis "je", ça signifie surtout que je compte sur Google pour me fournir le nécessaire dans son SDK.
Test simple
Avant d’écrire mes premiers tests, j’ai réussi à rompre avec certaines mauvaises habitudes, et j’ai lu la documentation sur les tests unitaires. Le début de la mise en place est classique, il faut installer jUnit ; un autre framework pourra certainement faire l’affaire, mais je ne vois pas l’intérêt dans mon cas. J’installe donc le jar de jUnit dans mon projet et je développe ma première classe de test.
@Test
public void testSave() {
Person person = new Person();
person.setName("Hassler");
person.setForname("Alexis");
dao.save(person);
}
Bien que la doc ne me dise pas de le faire, je ronge d’impatience d’essayer ce que ça donne. Évidemment, le résultat est sans appel : java.lang.NullPointerException: No API environment is registered for this thread. Au moins je saurai à quoi sert tout le bruit que Google me demande de faire autour du test : il faut initialiser l’environnement d’exécution.
Initialisation de l’environnement
Je reprends donc la lecture de la documentation. Elle me dit que je vais avoir besoin de classes installées dans appengine-testing.jar dans lib/testing/ et appengine-api.jar, appengine-api-labs.jar et appengine-api-stubs.jar du répertoire lib/impl/, dans le répertoire d’installation du SDK. Là, je ressens un soupçon de déception : si Google les a mis dans un répertoire impl, c’est qu’ils ne pensaient pas qu’on aurait à les utiliser, et donc qu’on ne ferait pas de test unitaire. Je rejette rapidement cette sombre pensée, indigne de notre époque, et je regarde ce que Google nous propose tout de même : il faut utiliser une classe helper qui initialise et clôt l’environnement pour nous.
private final LocalServiceTestHelper helper
= new LocalServiceTestHelper(new LocalDatastoreServiceTestConfig());
@Before
public void setUp() {
helper.setUp();
}
@After
public void tearDown() {
helper.tearDown();
}
Cet ajout suffit à faire fonctionner le test, je peux donc passer aux choses sérieuses : écrire du code de test des mes DAO.
Données persistantes
Par défaut, dans notre environnement de test, aucune donnée n’est stockée car la persistance est gérée uniquement en mémoire. Ce mode de fonctionnement peut s’avérer pratique puisque les tests sont ainsi réellement unitaires.
Dans certains cas de test, il pourrait être plus pertinent de partir d’un jeu de données stockées en fichier. Après étude de la javadoc, je découvre la possibilité de passer un objet LocalDatastoreServiceTestConfig au constructeur du helper, et les méthodes setNoStorage et setBackingStoreLocation sur cet objet de configuration ; ça ressemble tout à fait à ce que je voulais faire, c’est-à-dire rendre le stockage persistant. Je modifie ma méthode setUp en conséquence.
@Before
public void setUp() {
LocalDatastoreServiceTestConfig config
= new LocalDatastoreServiceTestConfig();
config.setBackingStoreLocation("data/local_db.bin");
config.setNoStorage(false);
helper = new LocalServiceTestHelper(config);
helper.setUp();
}
Le fichier local_db.bin est bien créé, mais les données n’y sont pas stockées ! Lorsque je consulte les traces, je constate qu’il y a des load, mais pas de persist. Soit je n’ai rien compris à l’affaire, soit il y a d’autres paramètres à modifier, et comme je suis d’humeur optimiste, j’opte pour cette dernière solution. Je me replonge donc dans la javadoc et je trouve d’autres propriétés à modifier : un délai de stockage, une durée de vie de transaction et une durée de vie de requête. Par contre, aucune explication sur l’utilisation de ces propriétés, je vais donc devoir tâtonner.
Lorsque je positionne StoreDelayMs à une valeur faible (100 ms), je vois bien apparaître des traces de persist, mais au final mon fichier ne grossit pas. En revanche, si je mets une valeur plus grande (500 ms), je retrouve bien les valeurs insérée… sauf les dernières. Bref, choisir une valeur pertinente va ressembler à la quadrature du cercle. Ou alors, en essayant de mieux comprendre le mécanisme de persistance locale, j’arriverai peut-être à avancer. Je vous passe les détails de mes investigations, mais le résultat est le suivant : la méthode tearDown du helper fait le ménage dans le datastore, si le délai de stockage est important, ma méthode de recherche passe avant le déclenchement de la persistance finale, celle qui fait le ménage.
Bref, la solution serait d’une part, de ne pas faire de tearDown, ou de restaurer un fichier de stockage local dans le setup, et d’autre part d’ajouter un délai d’attente après chaque sauvegarde de donnée afin d’être certain qu’elles seront bien dans le fichier.
Conclusion
Écrire des tests unitaires pour une application Google App Engine semble assez simple et pratique. Ça se complique si on veut travailler avec des données réellement persistantes, point sur lequel j’ai plus posé le problème que trouvé une solution viable.
En ayant identifié les possibilités et contraintes, je peux commencer à développer et à tester, et c’était bien là l’objectif. Ah non, il faut que j’étudie l’intégration avec Spring et en particulier la gestion des transactions au préalable.