Tester des applications Web avec HttpUnit

Dans une application d'entreprise typique, de nombreux domaines nécessitent des tests. En partant des composants, des classes les plus simples, les développeurs ou les développeurs de tests spécialisés doivent programmer des tests unitaires pour s'assurer que les plus petites unités de l'application se comportent correctement. Chaque composant peut potentiellement réussir seul les tests unitaires; cependant, les développeurs doivent s'assurer qu'ils travaillent ensemble comme prévu - dans le cadre d'un sous-système et dans le cadre de l'ensemble de l'application - par conséquent, des tests d'intégration doivent être effectués. Dans certains projets, les exigences de performance doivent être remplies, de sorte que les ingénieurs d'assurance qualité effectuent des tests de charge pour vérifier et documenter les performances de l'application dans diverses conditions. Pendant le développement d'applications, les ingénieurs d'assurance qualité effectuent des tests fonctionnels automatisés et manuelspour tester le comportement de l'application du point de vue de l'utilisateur. Lorsqu'un projet de développement atteint presque une étape spécifique, des tests d'acceptation peuvent être effectués pour vérifier que l'application remplit les conditions.

HttpUnit est un framework basé sur JUnit, qui permet la mise en œuvre de scripts de test automatisés pour les applications Web. Il est le mieux adapté à la mise en œuvre de tests fonctionnels automatisés ou de tests d'acceptation. Comme son nom l'indique, il peut être utilisé pour les tests unitaires; cependant, les composants de couche Web typiques tels que les pages JSP (JavaServer Pages), les servlets et les autres composants de modèle ne se prêtent pas aux tests unitaires. Quant aux divers composants basés sur le framework MVC (Model-View Controller), ils sont mieux adaptés pour les tests avec d'autres frameworks de test. Les actions Struts peuvent être testées unitairement avec StrutsUnit, et les actions WebWork 2 peuvent être testées unitairement sans conteneur Web, par exemple.

Cibles de test

Avant de passer aux détails de l'architecture et de l'implémentation, il est important de clarifier exactement ce que les scripts de test devront prouver à propos de l'application Web. Il est possible de simuler simplement le comportement d'un visiteur occasionnel du site Web en cliquant simplement sur des liens intéressants et en lisant des pages dans un ordre aléatoire, mais le résultat de ces scripts aléatoires ne décrirait pas l'exhaustivité et la qualité de l'application.

Une application Web d'entreprise typique (ou un site Web complexe) comporte plusieurs documents décrivant les exigences des différents utilisateurs ou responsables de l'application. Celles-ci peuvent inclure des spécifications de cas d'utilisation, des spécifications d'exigences non fonctionnelles, des spécifications de cas de test dérivées des autres artefacts, des documents de conception d'interface utilisateur, des maquettes, des profils d'acteurs et divers artefacts supplémentaires. Pour une application simple, la spécification entière pourrait éventuellement consister en un simple fichier texte avec une liste d'exigences.

À partir de ces documents, nous devons créer une liste organisée de cas de test. Chaque scénario de test décrit un scénario qui peut être réalisé par un visiteur Web via un navigateur Web. Une bonne pratique consiste à viser des scénarios de taille similaire - des scénarios plus grands peuvent être décomposés en morceaux plus petits. De nombreux excellents livres et articles traitent de la création de spécifications de cas de test. Pour cet article, supposons que vous disposez d'un ensemble d'éléments que vous souhaitez tester pour votre application Web, organisés en ensembles de scénarios de cas de test.

Il est temps de télécharger des trucs!

Bon, maintenant nous connaissons les trucs ennuyeux, téléchargeons des jouets sympas! Tout d'abord, nous avons besoin d'un SDK Java 2 installé pour compiler et exécuter nos tests. Ensuite, nous devons télécharger le framework HttpUnit - actuellement à la version 1.5.5. Le package binaire contient toutes les bibliothèques tierces requises. Nous aurons également besoin de l'outil de construction Ant pour exécuter les tests et générer des rapports automatiquement. Toute version assez récente de ces outils fonctionnerait probablement; Je préfère simplement utiliser la dernière et la meilleure version de tout.

Pour écrire et exécuter des tests, je recommande d'utiliser un IDE qui a un exécuteur de test JUnit intégré. J'utilise Eclipse 3.0M7 pour développer mes scripts de test, mais IntelliJ prend également en charge JUnit, tout comme les IDE les plus récents.

HttpUnit: le simulateur de client HTTP

Comme nous voulons tester des applications Web, idéalement, l'outil de test doit se comporter exactement comme les navigateurs Web des utilisateurs. Notre application (la cible de test) ne doit pas être consciente de toute différence lors de la diffusion de pages vers un navigateur Web ou l'outil de test. C'est exactement ce que fournit HttpUnit: il simule les requêtes GET et POST d'un navigateur normal, et fournit un joli modèle d'objet avec lequel coder nos tests.

Consultez le guide détaillé de l'API pour le reste des classes et méthodes; La figure 1 donne juste un bref aperçu des classes que j'utilise le plus fréquemment. Une session utilisateur (une séquence d'interactions avec l'application Web) est encapsulée avec un fichier WebConversation. Nous construisons des WebRequests, configurant généralement l'URL et les paramètres, puis nous l'envoyons via le WebConversation. Le framework retourne ensuite a WebResponse, contenant la page retournée et les attributs du serveur.

Voici un exemple de cas de test HttpUnit de la documentation HttpUnit:

/ ** * Vérifie que la soumission du formulaire de connexion avec le nom "maître" aboutit * à une page contenant le texte "Top Secret" ** / public void testGoodLogin () jette Exception {WebConversation conversation = new WebConversation (); WebRequest request = new GetMethodWebRequest ("//www.meterware.com/servlet/TopSecret"); Réponse WebResponse = conversation.getResponse (requête); WebForm loginForm = response.getForms () [0]; request = loginForm.getRequest (); request.setParameter ("nom", "maître"); response = conversation.getResponse (demande); assertTrue ("Connexion non acceptée", response.getText (). indexOf ("Vous l'avez fait!")! = -1); assertEquals ("Titre de la page", "Top Secret", response.getTitle ()); }

Considérations architecturales

Remarquez comment l'exemple Java ci-dessus contient le nom de domaine du serveur exécutant l'application. Lors du développement d'un nouveau système, l'application réside sur plusieurs serveurs et les serveurs peuvent exécuter plusieurs versions. C'est évidemment une mauvaise idée de conserver le nom du serveur dans l'implémentation Java - pour chaque nouveau serveur, nous devons recompiler nos sources. D'autres éléments ne doivent pas se trouver dans les fichiers source, tels que les noms d'utilisateur et les mots de passe, qui doivent être configurables pour le déploiement spécifique. D'un autre côté, nous ne devons pas sur-architecturer une simple implémentation de cas de test. Normalement, la spécification du cas de test contient déjà la plupart de l'état du système et des descriptions de paramètres spécifiques pour notre scénario, il est donc inutile de tout paramétrer dans l'implémentation.

Pendant le codage, vous vous rendrez compte que de nombreuses sections de code apparaissent dans plus d'une implémentation de cas de test (potentiellement dans tous les cas de test). Si vous êtes un développeur orienté objet expérimenté, vous serez tenté de créer des hiérarchies de classes et des classes communes. Dans certains cas, cela a beaucoup de sens - par exemple, la procédure de connexion doit être une méthode commune disponible pour tous les cas de test. Cependant, vous devez prendre un peu de recul et vous rendre compte que vous ne construisez pas un nouveau système de production au-dessus de l'application cible de test - ces classes Java ne sont que des scripts de test pour valider la sortie du site Web. Faites preuve de bon sens et visez des scripts de test simples, séquentiels et autonomes.

Les cas de test sont généralement fragiles. Si un développeur modifie une URL, réorganise la mise en page

sera un script séquentiel simple

La traçabilité est cruciale pour nos cas de test. Si quelque chose se passe KA-BOOM, ou, par exemple, un résultat de calcul est faux, il est important de pointer le développeur vers la spécification de cas de test correspondante et la spécification de cas d'utilisation pour une résolution rapide des bogues. Par conséquent, annotez votre implémentation avec des références aux documents de spécification d'origine. L'inclusion du numéro de version de ces documents est également utile. Cela peut être juste un simple commentaire de code ou un mécanisme complexe où les rapports de test eux-mêmes sont liés aux documents; l'important est d'avoir la référence dans le code et de garder la traçabilité.

Quand dois-je écrire du code?

Maintenant que vous connaissez les exigences (documentation de cas d'utilisation et spécifications de cas de test correspondantes), comprenez les bases du framework et disposez d'un ensemble de directives architecturales, mettons-nous au travail.

Pour le développement des implémentations de cas de test, je préfère travailler dans Eclipse. Tout d'abord, il a un bon lanceur de test JUnit. Vous pouvez sélectionner une classe Java et, dans le menu Exécuter, vous pouvez l'exécuter en tant que test unitaire JUnit. Le runner affiche la liste des méthodes de test reconnues et le résultat de l'exécution. Lorsque tout va bien pendant le test, cela donne une belle ligne verte. Si une exception ou un échec d'assertion se produit, il affiche une ligne rouge inquiétante. Je pense que le retour visuel est vraiment important - il offre un sentiment d'accomplissement, en particulier lors de l'écriture de tests unitaires pour votre propre code. J'aime aussi utiliser Eclipse pour ses capacités de refactoring. Si je réalise que dans une classe de cas de test, je dois copier et coller des sections de code, je peux simplement utiliser le menu Refactoring pour créer une méthode à partir de la section de code à la place.Si je réalise que de nombreux cas de test utiliseront la même méthode, je peux utiliser le menu pour afficher ma méthode dans ma classe de base.

Based on the architectural requirements above, for each project, I typically create a base test-case class, which extends the JUnit TestCase class. I call it ConfigurableTestCase. Each test-case implementation extends this class, see Figure 2.

ConfigurableTestCase typically contains the common methods and initialization code for the test case. I use a property file to store the server name, the application context, various login names for each role, and some additional settings.

The specific test-case implementations contain one test method per test-case scenario (from the test-case specification document). Each method typically logs in with a specific role and then executes the interaction with the Web application. Most test cases do not need a specific user to accomplish the activities; they typically require a user in a specific role, like Administrator, or Visitor, or Registered User. I always create a LoginMode enum, which contains the available roles. I use the Jakarta Commons ValuedEnum package to create enums for the roles. When a specific test method in a test-case implementation logs in, it must specify which login role is required for that particular test scenario. Of course, the ability to log in with a specific user should also be possible, for example, to verify the Registered User use case.

After each request and response cycle, we typically need to verify if the returned page contains an error, and we need to verify our assertions about what content the response should contain. We must be careful here as well; we should only verify items that are not variable and not too fragile in the application. For example, if we assert specific page titles, our tests will probably not run if the language is selectable in the application and we want to verify a different language deployment. Similarly, there's little point in checking an item on the page based on its position within a table layout; table-based designs change frequently, so we should strive to identify elements based on their IDs. In case some important elements on the page don't have IDs or names, we should just ask the developers to add them, rather than trying to work around them.

JUnit assertions offer a poor approach for checking if the look and feel, layout, and page design comply with the requirements. It is possible, given an infinite amount of time for the test development, but a good human tester can assess these things more efficiently. So concentrate on verifying the Web application's functionality, rather than checking everything possible on the page.

Here's an updated test scenario based on our test-case architecture. The class extends ConfigurableTestCase, and the login details are handled in the base class:

 /** * Verifies that submitting the login form with the name "master" results * in a page containing the text "Top Secret" **/ public void testGoodLogin() throws Exception { WebConversation conversation = new WebConversation(); WebResponse response = login(conversation, LoginMode.ADMIN_MODE); assertTrue( "Login not accepted", response.getText().indexOf( "You made it!" ) != -1 ); assertEquals( "Page title", "Top Secret", response.getTitle() ); } 

Tips and tricks

Most scenarios can be handled quite easily by setting WebForm parameters and then looking for specific elements with results in the WebResponse pages, but there are always some challenging test cases.





structure, ou modifie l'ID d'un élément de formulaire, le visiteur ne verra probablement aucune différence, mais vos scripts de test seront soufflés. Attendez-vous à beaucoup de retouches et de changements pour chaque implémentation de cas de test. La conception orientée objet pourrait réduire l'effort de retouche des parties communes dans les cas de test, mais du point de vue d'un ingénieur ou d'un testeur d'assurance qualité, je suis sûr qu'un qui interagit avec un site Web est plus facile à maintenir et à corriger. #####