XI. Terminer les tests de l'application▲
Nous avons fini le moteur de blog que nous voulions créer dans ce tutoriel. Toutefois, le projet n'est pas encore terminé. Pour que nous ayons une plus grande confiance dans notre code, il faut ajouter de nouveaux projets.
Bien sûr, nous avons déjà écrit des tests unitaires pour tester les fonctionnalités du modèle de données. Cela nous assure également que les fonctionnalités du cœur de notre application sont aussi testées. Mais une application web n'est pas uniquement constituée de cette partie. Nous avons besoin de nous assurer que l'interface visuelle fonctionne tel qu'attendu. Cela signifie qu'il faut également tester la couche des contrôleurs. Nous allons également avoir besoin de tester la partie graphique elle-même, ce qui inclue entre autre le code Javascript.
XI-A. Test de la couche contrôleur▲
Play offre un moyen de tester directement la couche de contrôleur d'une application grâce à JUnit. Il s'agit de tests fonctionnels, car nous voulons ici tester des fonctionnalités complètes de notre application web.
D'un point de vue technique, un test fonctionnel va appeler directement ActionInvoker, qui a pour but de simuler une requête HTTP. De fait, nous lui fournissons une méthode HTTP, une URI ainsi que des paramètres. Play routera ensuite cette requête, invoquera l'action correspondante, et retournera la réponse complète. Nous pourrons alors l'analyser et vérifier qu'elle contient ce que nous attendons.
Ecrivons un premier test fonctionnel. Ouvrons le fichier /yabe/test/ApplicationTest.java de test unitaire :
import
org.junit.*;
import
play.test.*;
import
play.mvc.*;
import
play.mvc.Http.*;
import
models.*;
public
class
ApplicationTest extends
FunctionalTest {
@Test
public
void
testThatIndexPageWorks
(
) {
Response response =
GET
(
"/"
);
assertIsOk
(
response);
assertContentType
(
"text/html"
, response);
assertCharset
(
"utf-8"
, response);
}
}
Dans l'état actuel, cela ressemble beaucoup à un test JUnit classique. Notez que ce test hérite de la classe FunctionalTest afin de disposer des bonnes méthodes. Ce test est correct et va simplement vérifier que notre page d'accueil (ici l'URL "/") nous retourne un statut 200 (qui correspond à une réponse HTML signifiant que tout s'est bien passé).
Nous allons maintenant tester que la sécurité de l'interface d'administration fonctionne correctement. Ajoutons ce test au fichier ApplicationTest.java :
...
@Test
public
void
testAdminSecurity
(
) {
Response response =
GET
(
"/admin"
);
assertStatus
(
302
, response);
assertHeaderEquals
(
"Location"
, "http://localhost/login"
, response);
}
...
Maintenant, lançons l'application yabe grâce à la commande play test, puis ouvrons la page http://localhost:9000/@tests. Sélectionnons ensuite le test ApplicationTest.java, lançons-le et vérifions que tout est OK :
Nous pourrions continuer à tester le reste des fonctionnalités de notre application de la même manière, mais ce n'est pas forcément la façon la plus simple de tester les applications web basées sur du HTML. Comme le blog est consultable via un navigateur web, l'idéal serait de le tester directement au sein de ce navigateur. C'est exactement ce que font les tests Selenium de Play.
Les tests fonctionnels basés sur JUnit restent toutefois utiles, par exemple dans le cas où nous aurions à tester un Web Service qui retourne une réponse qui n'est pas du HTML comme du JSON ou du XML...
XI-B. Ecriture des tests Selenium▲
Selenium est un outil dédié aux tests des applications web. L'un des ses intérêts est qu'il est possible d'exécuter les tests au sein de n'importe quel navigateur existant. Comme il n'utilise pas de "simulateur de navigateur", nous pouvons être sûr que ce qu'il teste est bien ce que dont les utilisateurs auront accès lors de leur visite sur notre site.
Un test Selenium est généralement écrit dans un fichier HTML. La syntaxe HTML utilisée par Selenium est un petit peu ennuyeuse à écrire (l'utilisation des tables HTML pour le formatage n'est pas très agréable par exemple). La bonne nouvelle, c'est que Play peut nous aider à générer ces tests grâce à un moteur de templates ainsi qu'un ensemble de tags qui simplifient la syntaxe de Selenium. Un effet de bord intéressant consécutif à l'utilisation de ces templates est que nous ne sommes plus cloisonnés aux scénarios statiques, et que nous pouvons dès lors utiliser les fonctionnalités de Play (boucles, blocs conditionnels...) pour écrire des tests plus complexes.
Il est toutefois possible de se limiter à la seule syntaxe Selenium pour écrire ces tests. Il devient alors très intéressant d'utiliser des outils de génération de tests tels que SeleniumIDE.
Lorsqu'on crée une nouvelle application avec Play, un test Selenium est également créé. Ouvrons donc le fichier /yabe/test/Application.test.html :
*{ You can use plain selenium commands using the selenium tag }*
#{selenium}
// Open the home page, and check that no error occurred
open('/')
waitForPageToLoad(1000)
assertNotTitle('Application error')
#{/selenium}
Ce test devrait s'exécuter sans problème dans notre application courante. Il ouvre simplement la page d'accueil et vérifie que cette page n'est pas titrée "Application error".
Toutefois, comme n'importe quel test complexe, nous avons besoin d'un certain nombre de données de test avant de se lancer plus avant. Nous allons évidemment réutiliser le mécanisme de Fixtures et le fichier /yabe/test/data.yml pour cela. Afin d'importer ces données avant l'exécution des tests, nous allons utiliser la balise #{fixture /} :
#{fixture delete:'all', load:'data.yml' /}
#{selenium}
// Open the home page, and check that no error occurred
open('/')
waitForPageToLoad(1000)
assertNotTitle('Application error')
#{/selenium}
Un autre point important à surveiller est de s'assurer que nous démarrons le test avec une session utilisateur toute neuve. Etant donné que les informations propres à une session sont conservées parmi les cookies du navigateur, nous pourrions malencontreusement conserver cette session au cours de tests successifs.
Démarrons les tests par une commande spécifique pour pallier ce problème :
#{fixture delete:'all', load:'data.yml' /}
#{selenium}
clearSession()
// Open the home page, and check that no error occurred
open('/')
waitForPageToLoad(1000)
assertNotTitle('Application error')
#{/selenium}
Exécutons ce test pour nous assurer que tout est toujours correct. Ce devrait être le cas !
Maintenant, écrivons un test plus spécifique. Ouvrons la page d'accueil et vérifions que les billets par défaut sont bien présents :
#{fixture delete:'all', load:'data.yml' /}
#{selenium 'Check home page'}
clearSession()
// Open the home page
open('/')
// Check that the front post is present
assertTextPresent('About the model layer')
assertTextPresent('by Bob, 14 Jun 09')
assertTextPresent('2 comments , latest by Guest')
assertTextPresent('It is the domain-specific representation')
// Check older posts
assertTextPresent('The MVC application')
assertTextPresent('Just a test of YABE')
#{/selenium}
Nous utilisons ici la syntaxe standard de Selenium, appelée Selenese.
Exécutons ce nouveau test :
Nous allons à présent tester le formulaire de commentaires. Ajoutons une nouvelle balise #{selenium /} à notre template :
#{selenium 'Test comments'}
// Click on 'The MVC application post'
clickAndWait('link=The MVC application')
assertTextPresent('The MVC application')
assertTextPresent('no comments')
// Post a new comment
type('content', 'Hello')
clickAndWait('css=input[type=submit]')
// Should get an error
assertTextPresent('no comments')
assertTextPresent('Author is required')
type('author', 'Me')
clickAndWait('css=input[type=submit]')
// Check
assertTextPresent('Thanks for posting Me')
assertTextPresent('1 comment')
assertTextPresent('Hello')
#{/selenium}
Exécutons ce test. Ah, cette fois-ci nous avons un échec, et même un sérieux problème :
Nous ne pouvons pas véritablement tester le mécanisme du captcha, nous devons du coup le feinter. En mode de test, nous considérons comme valide n'importe quelle valeur pour ce captcha. Nous savons que nous nous trouvons dans le mode test de Play quand l'ID du framework est égal à test. Ainsi, modifions notre action postComment() du fichier /yabe/app/controllers/Application.java pour considérer l'ID du framework :
...
if
(!
Play.id.equals
(
"test"
)) {
validation.equals
(
code, Cache.get
(
randomID)).message
(
"Invalid code. Please type it again"
);
}
...
Terminons par corriger notre code de test pour remplir le champ du captcha par une valeur fictive :
...
type('author', 'Me')
type('code', 'XXXXX')
clickAndWait('css=input[type=submit]')
...
Nous pouvons à nouveau relancer le test, qui, cette fois-ci, devrait passer.
XI-C. Mesurer la couverture de test▲
Bien entendu, nous n'avons pas écrit tous les tests requis pour cette application. Cependant, nous nous arrêterons là pour ce tutoriel. Dans un monde réel, comment pouvons-nous nous assurer que nous avons écrit suffisamment de cas de tests ? Nous avons pour cela besoin de mesurer la couverture de test.
Play propose un module de calcul de couverture de test basé sur l'outil Cobertura. Nous allons devoir activer ce module, mais uniquement pour le mode de test. Ajoutons ainsi cette ligne dans le fichier application.conf, puis redémarrons l'application en mode test :
# Import the cobertura module in test mode
%test.module.cobertura=${play.path}/modules/cobertura
Rouvrons notre navigateur, et rendons-nous sur la page http://localhost:9000/@tests. Sélectionnons l'ensemble des tests, et exécutons-les. Normalement, tous les indicateurs devraient être au vert.
Maintenant que tous les tests sont passés, stoppons l'application. Cobertura va alors générer ses rapports de couverture de test que nous pourrons retrouver dans le fichier /yabe/test-result/code-coverage/index.html :
Nous constatons que nous sommes encore loin d'une couverture complète de notre application. Une bonne suite de tests devrait nous permettre d'approcher les 100%, bien que cette valeur soit généralement quasiment impossible à atteindre (c'est là un autre débat ;) ).