vendredi 18 mars 2011

Tests unitaires, HTTP et Java

S'il vous est déjà arrivé d'écrire du code qui effectue des requêtes http, alors il vous est arrivé d'écrire des tests automatisés pour tester ce code (mais si, voyons !).

Mais alors comment faites-vous ?

Dans le cas d'un développement en Java (mais pas seulement), une solution est d'introduire, dans la classe testée, des méthodes non privées qui font effectivement les appels HTTP puis, dans les tests, de truquer ces méthodes (d'où la nécessité qu'elles ne soient pas privées) de façon à simuler différents retours du serveur web ou vérifier ce que votre code tente de transmettre au serveur.

C'est ce que je faisais dans les tests de soap-dust mais sans être vraiment satisfait de mon code.

Finalement j'ai choisi une méthode qui me parait plus élégante.

J'ai définit des urls au format test: qui simulent des urls http:. Dans mes tests, je configure le code que je veux tester pour qu'il utiliser des urls test: plutôt que des urls http:.

Dans une url test:, je suis directement capable de définir le statut HTTP lorsqu'on requête cette url, de même que les données qu'on obtient. Cerise sur le gâteau, à la fin du test, je peux récupérer les données qu'un client aurait tenté de transmettre à un serveur via cette url.

Exemple :


HttpURLConnection connection = (HttpURLConnection) new URL("test:status:500").openConnection();
assertEquals(500, connection.getResponseCode());

HttpURLConnection connection = (HttpURLConnection) new URL("test:status:200;file:hello.txt").openConnection();
assertStreamContent("Hello World !", connection.getInputStream);

byte[] written = new byte[] {1, 2, 3, 4};
HttpURLConnection connection = (HttpURLConnection) new URL("test:").openConnection();
OutputStream out = connection.getOutputStream();
out.write(written);
out.flush();
out.close();
assertTrue(Arrays.equals(written, Handler.saved.get("test:").toByteArray()));


Pour que les URLs test: soient reconnues par la jvm, il suffit de créer un classe test.Handler dans le package de votre choix puis de déclarer ce nouvel handler dans la jvm. Par exemple dans le cas des tests de soap-dust :


String handlers = System.getProperty("java.protocol.handler.pkgs");
if (handlers == null) handlers = "";
handlers += "|soapdust.urlhandler";
System.setProperty("java.protocol.handler.pkgs", handlers);


Grâce à cela, j'ai pu à nouveau rendre privée les méthodes que j'avais exposée uniquement pour les tests et ainsi j'ai eu plus de liberté dans la réorganisation de mon code.

Pour plus de détails, consultez :


Ce gestionnaire d'url test: est en version alpha et livré avec soap-dust.

4 commentaires:

  1. Salut Pascal,

    très jolie façon de tester tes requêtes HTTP. C'est un besoin très fréquent : je ne connais aucune base de code (ou presque) qui n'ai besoin de parler au monde extérieur via HTTP.
    Sur les 2 derniers projets sur lesquels j'ai travaillé, nous avons développé une classe MockHttpServer qui écoute vraiment sur une socket. Nous pouvons lui parler, lui donner des réponses type (200/OK, 500/KO etc) et même simuler des réponses plus complexes (en lui passant un TraiteurDeReponse), comme un streamer de vidéos ou un serveur de paiement.
    Je posterai prochainement un article sur ce sujet sur barreverte.fr. J'espère qu'on pourra ouvrir le code, car il me semble intéressant, mais a été écrit dans le cadre du travail.

    a +
    JP

    RépondreSupprimer
  2. Salut Jean-Philippe,

    J'ai également utilisé cette approche de déclencher un serveur bidon qui écoute véritablement sur une socket. Dans mon cas ce n'était pas http mais un protocole propriétaire et je n'ai pas trouvé d'autre alternative.

    Malheureusement les tests que j'ai écrit de cette façon sont assez instables : parfois la socket utilisée en écoute par le serveur bidon est mal libérée ou pas suffisamment vite au niveau système. Du coup le port ne peut pas être réutilisé par un autre serveur de test dans une autre suite de test. Lorsque cela se produit, certains tests échouent simplement parce que le serveur bidon n'arrive pas à démarrer :(

    Je ne rencontre pas ce type de problème avec la solution que j'ai mis en place dans soap-dust. Mais elle ne s'applique que dans un modèle où la connexion se fait via une URL prise en charge par la jvm.... tiens finalement j'ai peut-être moyen de faire ça aussi pour mon protocole propriétaire... faut que j'y réfléchisse.

    Bref ! Tu n'as pas rencontré ce type d'instabilité (socket mal libérée) dans vos tests ?

    Pascal.

    RépondreSupprimer
  3. Salut Pascal,

    nous avons rencontré certains cas de socket non libérée au début (époque c*nd*r), mais cela ne s'est jamais produit alors que nous utilisons cette technique intensément (sur *vsp) : serveur de paiement, de pub, de streaming... nous avons raffiné notre code au fil du temps.

    Nous nous en servons à la fois en tests unitaires junit classiques (avec arret demarrage en @BeforeClass/@AfterClass car c'est assez long) et dans nos tests d'acceptance (démarrés au début de la suite) sous Fitnesse.

    Effectivement, pour un protocole propriétaire, c'est une autre histoire.

    JP

    RépondreSupprimer
  4. Pour les inconditionnels, l'article promis par JP sur ce même sujet : http://www.barreverte.fr/comment-tester-les-interractions-avec-le-monde-exterieur-via-http

    RépondreSupprimer