Stubber des webservices avec JMS

Stubbing de webservices avec JMS

Mon équipe est amenée à concevoir et réaliser un ensemble de webservices REST. Ces services sont regroupés dans une webapp (alias “l’API”) qui constitue le point central de la plate-forme de production, exposant un ensemble de ressources à destination d’applications clientes, développées par d’autres équipes.

Situation initiale

Cette API en elle-même ne sert pas seulement de référentiel à des applications mobiles, elle s’interface aussi avec d’autres services internes ou externes au SI du client.

Parmi ceux-ci :

  • des API Apple
  • plusieurs brokers SMS
  • un opérateur de téléphonie
  • une plate-forme XMPP
  • plusieurs services internes
  • quelques réseaux sociaux

Une certaine continuité dans le fonctionnement du service est garantie au travers d’une batterie de tests. Outre les traditionnels tests unitaires, le projet comporte des tests d’acceptance. Ces derniers permettent d’exécuter de manière automatisée un ensemble de scénarios d’appels à l’API.

En pratique leur exécution est déclenchée soit manuellement par un développeur sur son poste, soit par le serveur d’intégration continue (Jenkins).

Élément perturbateur

Le problème qui se pose souvent est le suivant : cette API faisant souvent appel à d’autres APIs dont on ne maîtrise pas le comportement, comment tester des fonctionnalités qui mettent en jeu des appels à ces services externes sans être à la merci de lenteurs réseau ou de leurs éventuels downtimes?

Péripétie

Une solution basique, pour un service externe donné, est d’avoir deux versions du code qui permet de dialoguer avec lui :

  • une première destinée à la production, qui effectue de vrais appels au service en question
  • une implémentation “bouchon” qui ne fera rien ou se contenta de renvoyer des réponses prédéfinies
1
2
3
4
5
public class FacebookClientImpl {
    public String getFacebookStatus(String userId) {
        return "i'm a stubbed status";
    }
}

Un tel comportement est suffisant dans certains cas, mais pas toujours.
Pour prendre un exemple : comment vérifier que l’appel d’un client à notre API de découverte de contacts Facebook va effectivement déclencher une requête valide vers ce réseau social puis, par exemple, l’envoi du SMS “Le contact Facebook {nom_du_contact} a été ajouté”?

Péripétie(1)

Pour ce genre de cas de figure un peu plus avancé (et fréquent sur ce projet) on a eu besoin d’une méthode de stubbing un peu plus évoluée, qui permette:

  • d’intégrer aux scénarios de tests (donc à l’extérieur de la webapp testée) des assertions sur ce qui est envoyé aux différents services externes
  • de se substituer à ces mêmes services externes pour tester certains cas fonctionnels (ex: simuler la réussite ou l’échec d’un envoi de SMS)
  • d’être mise en oeuvre simplement aussi bien sur un poste de développeur que sur diverses plate-formes de tests.

Dénouement

Pour faire tout ça on a choisi d’utiliser JMS.

Le système s’appuie sur trois éléments :

  • un composant côté API branchable dans les clients, le Requester
  • un composant intégré aux tests d’acceptance qui en ont besoin, le Responder
  • un broker JMS (ici ActiveMQ) lancé à la demande via Maven ou tournant en permanence, en fonction du besoin.

Prenons l’exemple de la communication avec un service d’envoi de SMS : lorsque cela est nécessaire dans notre couche de services on va faire appel à un client implémentant SmsBrokerClient et qui possède une méthode sendSms().

1
2
3
public interface SmsBrokerClient {
void sendSms(String recipientPhoneNumber, String text);
}

L’implémentation stub simpliste ressemblait à:

1
2
3
4
5
public class SmsBrokerClientDumbStub implements SmsBrokerClient {
    public void sendSms(String recipientPhoneNumber, String text) {
        // nothing to do here
    }
}

Une nouvelle implémentation plus évoluée a été écrite :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class SmsBrokerClientJmsStub implements SmsBrokerClient {

    private final JmsRequester smsRequester;

    public SmsBrokerClientJmsStub(String jmsUrl) {
        smsRequester = new JmsRequester(jmsUrl, JmsSmsOperation.send);
    }

    public void sendSms(String recipientPhoneNumber, String text) {
        JmsContent jmsContent = new JmsContent(text);
        jmsContent.putParam(JmsSmsTags.TO, recipientPhoneNumber);
        smsRequester.sendRequest(jmsContent);
    }
}

Le JmsRequester est chargé de placer un message représentant une requête d’envoi de SMS dans une file JMS dédiée à ce type d’opération (identifiée par le membre d’enum JmsSmsOperation.send).

Voyons maintenant côté tests, un scénario d’acceptance parmi d’autres:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class PasswordResetAcceptanceTest {

    private final JmsResponder smsResponder = new JmsResponder("tcp://jms_broker_url", JmsSmsOperation.send);

    @Test
    public void should_send_reset_password_link_in_sms_when_api_called() {
        // ... début du scénario : création d’un client ...
        //... déclenchement de la procédure de réinitialisation de mots de passe ...

        JmsContent jmsContent = smsResponder.readRequest(JmsSmsTags.TO, "0612345678");
        String smsText = jmsContent.getObject();
        assertThat(smsText).isEqualTo("to reset you password, click [...]");
    }
}

Le JmsResponder ci-dessus est le pendant du JmsRequester: il permet de lire les messages JMS créés par ce dernier, en les filtrant sur un attribut (ici le numéro de téléphone du destinataire).

Avec ce système en place, voici comment se déroule une exécution des tests d’acceptance:

  • le projet maven des scénarios d’acceptance démarre sa propre instance de l’API en mode “bouchonné JMS” (activé via une variable d’environnement)
  • les versions “bouchonnées JMS” des différents clients sont instanciées et injectées via Spring
  • le projet maven lance une instance d’ActiveMQ
  • il joue chacun des tests d’acceptance.

Un second usage du Responder – d’où vient son nom – est de prendre la place des services externes en fournissant des réponses factices à l’API.
Pour ce faire, dans un test donné les réponses factices sont positionnées dans des files JMS au début de l’éxécution de chaque test.

Exemple :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class PasswordResetAcceptanceTest {

    private final JmsResponder smsResponder = new JmsResponder("tcp://jms_broker_url", JmsSmsOperation.send);

    @Test
    public void should_send_reset_password_link_in_sms_when_api_called() {
        // ... début du scénario : création d’un client ...

        // simule une réponse positive de la part du provider de SMS
        smsResponder.sendResponse(JmsSmsTags.TO, "SMS sent successfully");

        //... déclenchement de la procédure de réinitialisation de mots de passe ...

        JmsContent jmsContent = smsResponder.readRequest(JmsSmsTags.TO, "0612345678");
        String smsText = jmsContent.getObject();
        assertThat(smsText).isEqualTo("to reset you password, click [...]");
    }
}

Parmi les inconvénients de cette approche :
– il faut faire bien attention à ce que les sélecteurs utilisés pour filtrer les messages JMS d’une file soient uniques (risque de téléscopage entre plusieurs tests)
– dans le code d’un test, on est obligés de “répondre” (sendResponse) avant de lire la requête, ce qui n’est pas des plus plus intuitifs

Situation finale

Ce système a commencé à être mis en place il y a près d’un an et s’est révélé d’une grande utilité:

  • En apportant plus de souplesse dans le comportement des API externes il a permis d’élargir l’éventail des cas testables
  • En exposant à l’extérieur de l’API le contenu des pseudo-requêtes aux services externes, il offre plus de visibilité aux testeurs chargés de faire régner la Qualité sur le produit
  • En évitant toute communication avec les véritables services externes, il a permis de s’affranchir des éventuelles lenteurs ou défaillances de ces derniers qui faisaient régulièrement échouer les tests.
VN:R_U [1.9.22_1171]
Rating: 0 (from 0 votes)
Share
Ce contenu a été publié dans Java. Vous pouvez le mettre en favoris avec ce permalien.

3 réponses à Stubber des webservices avec JMS

  1. Benjamin B. dit :

    Un procédé intéressant, mais un point reste obscur pour moi : Quel est l’avantage de cette méthode par rapport à l’utilisation de framework de bouchonnage tels que Mockito ou EasyMock ?

    VN:R_U [1.9.22_1171]
    Rating: 0 (from 0 votes)
    • rkasbach@excilys.com dit :

      On utilise effectivement Mockito mais dans des tests unitaires.
      Ca n’est peut-être pas très bien mis en avant mais la méthode de bouchonnage que décrit l’article concerne des tests d’acceptance.
      Ce tests sont de plus haut niveau et ont pour but de valider les fonctionnalités de la webapp “complète” et non d’une de ses classes.
      En l’occurence on la déploie dans un tomcat et on l’attaque en HTTP en nous mettant dans la peau d’un client (ex : application mobile).

      VN:R_U [1.9.22_1171]
      Rating: 0 (from 0 votes)
  2. mgrenonville dit :

    J’aime beaucoup l’idée de pouvoir paramétrer le comportement. Ça pourrait s’étendre aux bouchons en général (je me souviens avoir travaillé sur un projet où nous avions construit un bouchon paramétrable avec play pour répondre en fonction des inputs : prochain appel avec ce montant et ce type de question, on renvoie un échec, puis un OK.).

    Un Responder en node.js (ou autre langage de script rapide à écrire) qui se connecte sur un broker et hop, ça fait un bouchon pour l’appli dont on peut changer le comportement, et qui permet d’aller plus loin que les bouchons dont le comportement est plus ou moins “hard-codé”.

    En revanche, je remarque que ce genre de bouchonnage ne teste pas le code de communication avec l’API externe : J’imagine que c’est testé à un autre moment. Je me pose aussi la question de la pertinence du test d’acceptance sachant que le contexte spring est modifié. Je suis plus partisan du remplacement de l’url d’appel par exemple. Bref, à creuser ;-)

    VN:R_U [1.9.22_1171]
    Rating: 0 (from 0 votes)

Laisser un commentaire