Windows Phone 7 et les tests unitaires asynchrones avec RestSharp

En plein développement d’un prototype Windows Phone 7 (WP7), j’ai cherché à optimiser et tester des tâches comme la récupération de données issues de requêtes HTTP et pouvant générer des output de différents types (XML, JSON, etc..); non seulement réaliser ces tâches, mais également écrire un ensemble de tests unitaires (afin de découvrir un peu leur fonctionnement sous WP7). Nous verrons quels outils pratiques utiliser afin de pouvoir y parvenir et comment écrire des tests asynchrones avec d’autres librairies spécifiques pour Windows Phone 7.

RestSharp

Dans un contexte de requêtes orienté “récupération de flux” (entendons par flux, données issues de requêtes Http avec des mots clés comme REST, JSON, etc..), il est fréquent d’utiliser naturellement la classe native WebClient. L’utilisation de cette classe a ses avantages (natif au framework Microsoft .NET) et ses inconvénients, l’exécution de requêtes asynchrones en étant un exemple (inconvénient). Après quelques petites recherches, je suis tombé par hasard sur la librairie dont je vais brièvement vous parler : RestSharp.

Quel avantage propose cette librairie face à l’utilisation classique d’un webclient ?

Pour reprendre ce que disait son créateur sur StackOverFlow ici :

RestSharp removes the following pain points:

* Quirks in .NET’s HTTP classes (basic authentication is broken, error handling for non-200 responses, etc)
* Automatic deserialization from response data to POCOs
* Simplified API (request.AddParameter(name, value) instead of manually compiling request bodies
* Simplified request/response semantics, especially for async (however, it’s opinionated for async and may not meet everyone’s needs, in which case I would also suggest evaluating Hammock)

Deserialization is probably the biggest gain since for most APIs you don’t have to do very much to get the XML or JSON into your C# objects.

Surpris et curieux de la tester, je me suis mis à l’utiliser et je l’ai intégré dans mes développements très rapidement. L’avantage de cette librairie est qu’elle supporte Windows Phone 7, et que cela permet de réduire le nombre de ligne de code et de traitements considérablement.

Voici un exemple d’utilisation (récupéré sur leur site) :

var client = new RestClient("http://example.com");
var request = new RestRequest("resource", Method.POST);

client.ExecuteAsync(request, (response) => {
      var resource = response.Data;
});

Exécution de requêtes asynchrones et tests unitaires

Arrivé à l’écriture de certains de mes tests unitaires, je me suis retrouvé face à plusieurs problématiques et erreurs. Pourquoi ?
Simplement parce que la plupart des requêtes pouvant être exécutées avec RestSharp peuvent l’être de façon asynchrone, et c’est le mode de communication que j’ai choisi par défaut pour le prototype WP7.

Pourquoi vouloir faire des tests unitaires sur des requêtes Http?

Peut-être vous posez-vous cette question ?
Je vous répondrai que tout est testable d’une façon ou d’une autre, et que l’intérêt peut dépendre du contexte.

Mais pour que vous compreniez davantage, l’application en question fonctionne avec 2 fichiers de configuration : un fichier de configuration distant et un fichier de configuration local. (le fichier de configuration distant étant facultatif).

Le fichier local est créé par défaut, et pourra être mis à jour par le fichier de configuration distant.
Pourquoi ? Simplement parce que, les liens auxquels je fais référence dans ce fichier seront amenés à évoluer à plusieurs reprises. Il fallait pouvoir les mettre à jour le plus facilement possible, éviter de publier plusieurs fois l’application Windows Phone.

Voici à quoi ressemble ce fichier XML.

Bien entendu, au lieu du fichier XML, nous aurions pu tout à faire récupérer les données qui pourraient provenir d’un Web Service. Toutefois, je suis lié à des contraintes techniques imposées par les clients de l’application.

Les tests de connectivité

Je n’ai pas écrit de tests unitaires concernant la connectivité dans mon application car je n’en ai pas vu l’utilité. A chaque lancement de l’application, si la connexion est disponible, on lira le fichier de configuration distant s’il est disponible. Si la date de dernière mise à jour requise dans ce fichier est supérieure à celle qui se trouve dans notre fichier local, on récupère la version distante et on remplace la versions locale. Si le fichier distant n’existe pas, ou ne peut pas être lu, c’est toujours le fichier local qui sera lu.

La seule chose dont j’ai besoin, c’est de pouvoir réaliser les tests à condition uniquement que ma connexion soit disponible. Il est possible de le vérifier avec cette l’énumération NetworkInterface.

   (NetworkInterface.NetworkInterfaceType != NetworkInterfaceType.None;)

Toutefois, attention à son utilisation qui peut bloquer les appels réseaux sur votre téléphone. Je vous recommande la lecture de cet article qui parle de comment résoudre ce problème.

Les méthodes à tester

Dans une classe de base me permettant de gérer la configuration de mon application, j’ai les blocs de code suivants (qui sont les blocs que je vais tester) :

public BaseConfiguration ReadDistantConfiguration()
{
     var client = this.RestClient();
     var request = this.RestRequest();
     Configuration = null;
     DownloadedRequestEstablished = false;
     client.ExecuteAsync(request, (response) =>
     {
             DownloadedRequestEstablished = true;
             if (response.ResponseStatus == ResponseStatus.Completed) {
                   var statusCode = response.StatusCode;
                   if (statusCode == System.Net.HttpStatusCode.OK
                         || statusCode == System.Net.HttpStatusCode.NotModified) {
                         var xml = new XmlDeserializer();
                         xml.DateFormat = CONFIG_DATE_FORMAT;
                         Configuration = xml.Deserialize(response);
                   }
                   else if (statusCode == System.Net.HttpStatusCode.NotFound) {
                       //
                       // TODO When File Not Found
                       //
                   } else {
                       //
                       // TODO When status code not expected
                   }
             }
             else {
                  //
                  // TODO When response status is not completed
                 //
             }
     });
     return Configuration;
}

Concernant les propriétés Configuration et DownloadedRequestEstablished, ce sont des propriétés courantes :

///
/// Base Configuration
///
public BaseConfiguration Configuration
{
    get { return _Configuration; }
    set { _Configuration = value; }
}
///
/// Determines if a request has been established
///
public bool DownloadedRequestEstablished
{
     get { return _DownloadedRequestEstablished; }
     set {
             if (_DownloadedRequestEstablished != value)
            {
                 _DownloadedRequestEstablished = value;
                 RaisePropertyChanged("DownloadedRequestEstablished");
             }
       }
}

Écriture des tests unitaires

Afin de pouvoir tester non seulement toutes mes ViewModels (et aussi certaines autres classes de bases), j’utilise le framework de tests de Silverlight 3 de Jeff Wilcox (Silverlight 3 étant la version actuelle de Windows Phone 7 avec un “peu” de différences…). En tout cas, c’est le seul recommandé (compatible) actuellement pour Windows Phone. Vous pouvez le retrouver ici.

Il est relativement simple d’utilisation, et les exemples d’utilisation sont nombreux.

Voici un premier test, qui me permet de tester la lecture de mon fichier en local :

[TestMethod]
[Tag("Configuration")]
[Description("Teste la lecture du fichier de configuration en local")]
public void Can_Get_Local_Configuration_File()
{
      var bcHelper = BaseConfigurationHelper.Instance;
      BaseConfiguration config = null;
      bcHelper.ResetBaseConfigFile();
      config = bcHelper.ReadLocalConfiguration();
      Assert.IsNotNull(config, "Config is null");
}

La méthode ;ReadLoadConfiguration est une méthode que j’ai créé et qui ne fait que lire des fichiers dans l’espace réservé à l’application sur le mobile (l’IsolatedStorage). Il n’y a pas encore de lecture asynchrone, là, tout va bien.

Voici maintenant une première version de mon test qui échoue. Je vous expliquerai pourquoi.

[TestMethod]
[Asynchronous]
[Description("Essaie de lire mon fichier de configuration depuis le site example.com")]
[Tag("Configuration")]
public void Can_Get_Distant_Configuration_File()
{
     DateTime startTime = DateTime.Now;
     var bcHelper = BaseConfigurationHelper.Instance;
     BaseConfiguration config = null;

     bcHelper.SetableClient = new RestClient("http://www.example.com/");
     bcHelper.SetableRequest = new RestRequest("BaseConfiguration.xml", Method.GET);
     config = bcHelper.ReadDistantConfiguration();

     Assert.IsNotNull(spfConf.Configuration, "Config is null");
}

Ce test va échouer car, rappelez vous la méthode ReadDistantConfiguration juste en haut. Elle fait appelé à des fonctions de notre librairie RestSharp, méthode exécutant un traitement asynchrone. Le mode de fonctionnement des tests unitaires avec des traitements asynchrones est expliqué par Jeff Wilcox toujours ici. Une de ses phrases que l’on pourrait faire remonter est celle ci :

The AsynchronousAttribute from the Microsoft.Silverlight.Testing namespace, and in the assembly of the same name, tells the test framework:

Hello! I’m going to do some stuff now. And when this method returns, well, I’m going to continue doing work. So don’t report any results yet…

… In fact, wait for me to send you a ‘test complete’ message – or throw an unhandled exception – to make a call, OK?

This means that the entire methods executes, and then any queued up work items are run afterwards, each in due time.

Notez le flag au dessus de la méthode (Asynchronous) pour indiquer qu’un de nos traitements est asynchrone et qu’il faudra prendre cela en considération. Mais, ce n’est pas tout. Dans le billet de Jeff, il suggère l’utilisation des méthodes EnqueueConditional, EnqueueCallback et EnqueueTestComplete pour préparer l’exécution de notre test en attendant que la requête soit complètement exécutée.

Voici à quoi ressemble notre test maintenant :


[TestMethod]
[Asynchronous]
[Description("Essaie de lire mon fichier de configuration depuis le site example.com")]
[Tag("Configuration")]
public void Can_Get_Distant_Configuration_File()
{
     DateTime startTime = DateTime.Now;
     var bcHelper = BaseConfiguration.Instance;
     BaseConfiguration config = null;

     bcHelper.SetableClient = new RestClient("http://example.com/");
     bcHelper.SetableRequest = new RestRequest("BaseConfiguration.xml", Method.GET);
     config = bcHelper.ReadDistantConfiguration();

     EnqueueConditional(() => bcHelper.DownloadedRequestEstablished); //attendra que le fichier ait été récupéré
     EnqueueCallback(() => { Assert.IsNotNull(bcHelper.Configuration, "Config is null"); });
     EnqueueTestComplete();
}

On va d’abord exécuter notre fonction de lecture de notre fichier de configuration à distance. L’ensemble des conditions de notre test est empilé. La condition dans EnqueueConditional est le point final qui permettra l’exécution de mon test seulement si la propriété DownloadedRequestEstablished est vraie (c’est le cas uniquement quand un téléchargement a eu lieu).

Attention : Évitez d’encapsuler votre méthode asynchrone dans un bloc EnqueueCallback car cela fonctionne très mal et pertubera vos tests. Le test n’attendra pas que la méthode ReadDistantConfiguration soit exécutée pour finir son traitement. Elle sera exécutée une première fois (cela correspondant à son appel), puis une seconde fois (correspondant à son exécution par le EnqueueCallBack).

J’espère que cela vous aurez un peu plus éclairé si vous devez être confrontés à ce genre de cas pratiques.

Bon courage et à bientôt.

Nombre de vue : 49

COMMENTAIRES 6 commentaires

  1. […] des services REST l’excellent RestSharp. J’en avais déjà parlé par ailleurs dans cet article. L’avantage en utilisant RestSharp est le suivant : If there is a network transport error […]

  2. David POULIN dit :

    Merci beaucoup pour ce lien Paul! J’ai regardé et c’est vrai que ça a l’air pas mal (même si Spring.Rest ne gère pas encore OAuth, le téléchargement multiple de fichiers, et que l’authentification basique n’est pas si basique que cela :D).
    Par contre, un reproche à cette API (Spring.Rest), c’est que le nom des méthodes et leurs fonctionnalités ne sont pas suffisamment explicite. On est loin d’imaginer que RestTemplate.Exchange(…) permet d’ajouter des headers et de lire les réponses de chaque opération. Et je t’avouerai que je préfère mille fois davantage request.AddHeader ou request.AddParameter etc… C’est plus évocateur, plus simple et plus familier ! Je suis en train d’écrire d’ailleurs un article très complet sur RestSharp. Tu me diras ce que tu en penses si tu le désires une fois publié.

    A bientôt !

  3. Paul dit :

    D’après la doc :

    Booking requestBody = // create booking request content
    HttpEntity entity = new HttpEntity(requestBody);
    entity.Headers[“MyRequestHeader”] = “MyValue”;
    // ou entity.Headers.Add(“MyRequestHeader”, “MyValue”);

    template.PostForLocation(“http://example.com/hotels/{id}/bookings”, entity, 1);

  4. […] tâches, mais également écrire un ensemble de tests unitaires (afin de découvrir un peu … Lire la suite chez Soat. Windows Phone ← TFS, les clients et un changement de domaine C# – Un FtpHelper […]

  5. […] des services REST l’excellent RestSharp. J’en avais déjà parlé par ailleurs dans cet article. L’avantage en utilisant RestSharp est le suivant : If there is a network transport error […]

AJOUTER UN COMMENTAIRE