Blog Arolla

Linq provider : un essai… partie 6

Jammin’ Jammin’

Le mois dernier, j’ai eu l’opportunité d’obtenir du feedback sur mon provider Linq, de la part @cyriux et Jérôme, au cours de la Jam de code mensuelle d’@ArollaFr. A cette occasion, nous avons également fait d’importants changements dans le code.

Le concept d’un Jam de code est simple : nous nous rencontrons une fois par mois pour mettre en pratique nos compétences en programmation, en mettant l’accent sur des pratiques telles que TDD et le pair-programming.

Pour plus de détails : http://www.arolla.fr/evenements-2/jams-de-code/

Le thème du mois de juin était “Tester des choses qui sont difficiles à tester”. La session a démarré sur un “Lightning talk” par @cyriux, qui nous a décrit un certain nombre de situations dans lesquelles la mise en place de tests est relativement compliquée, comme par exemple :

  • Lorsque le temps est impliqué
  • Lorsqu’on traite de parallélisation et de multi-threading
  • Lorsque l’architecture fait partie du test (bande passante du réseau, systèmes tolérants aux pannes…)
  • Tester la sécurité (injection SQL, diverses sources d’exploits)
  • Lorsque l’hôte de l’application est difficile à émuler (par exemple: comment tester un plug-in eclipse ?)
  • Tester des entrées/sorties non triviales : images, sons, documents, rapports, messages de taille importante…
  • Tester avec des problèmes de configuration vaste : index de recherche réaliste, algorithmes sociaux, séries temporelles pour des analyses financières…

Après cette introduction, nous avons recherché un exemple pratique d’une telle situation, et j’ai suggéré que nous jettions un œil à mon implémentation de ce provider Linq. Je ne suis pas allé très loin dans cette implémentation à ce jour, mais l’objectif de ce provider “Linq to web service” est de récupérer des données filtrées par un service en se basant sur une requête Linq.

Cela signifie que je souhaite que le filtrage des données intervienne au niveau du web service, et non au niveau du client de ce service. Et bien sûr, il s’agit d’un comportement que je veux pouvoir tester ! Le mécanisme du fonctionnement du provider est résumé sur le schéma suivant :

Après une courte discussion, @cyriux m’a fait remarqué qu’il y avait plusieurs fausses assomptions dans la façon dont je testais ces points. J’étais parti du principe que j’allais tester le provider face à un service que j’aurais implémenté moi-même pour l’occasion… mais en réalité je n’avais aucun besoin d’une implémentation réelle du service ! Le service en lui-même, et le provider correspondant, sont deux fonctionnalités liées, mais distinctes, et doivent être testées unitairement indépendamment l’une de l’autre !

Je n’ai pas encore complètement abandonné l’idée d’implémenter ce service, mais il s’agit à présent d’un objectif séparé, qui n’a plus rien à voir avec l’implémentation du provider. Et le fait que le service est en fait un web service est juste un détail d’implémentation. Cette réflexion m’a amené à revoir l’organisation du projet. Ma vision de cette organisation est désormais la suivante :

La solution est dorénavant composée de 3 assemblies :

  • PeopleFinder, qui définit l’interface du service (le contrat) et quelques classes utilisées comme simples Data Transfer Objects,
  • LinqToService, qui contient l’implémentation du Linq provider, dont traitent les billets précédents,
  • UnitTests, qui contient les tests unitaires, ainsi qu’une implémentation “lambda-based” de l’interface PeopleFinder. Cette implémentation est utilisée pour les tests à la place du web service.

Cette organisation nous permet de tester uniquement le comportement du provider, en fournissant notre propre implémentation du service. Pour permettre l’injection de notre implémentation du service, nous avons introduit une classe PeopleFinderLocator.

Pour une expression Linq donnée, le but du provider Linq est uniquement de fournir au service les bons paramètres, de récupérer les données transmises par le service, et d’exécuter le travail additionnel que le service ne peut pas (ou ne sait pas encore) gérer. Dans le cas présent, le service sait comment filtrer des personnes en fonction de leur âge. Considérons donc la requête suivante :

var query = new QueryableServiceData<Person>()
    .Where(p => p.Age >= 36);

Lorsqu’on l’exécute, le provider doit appeler le service avec le paramètre approprié, et doit renvoyer les résultats à l’appelant, sans effectuer aucun autre travail additionnel. La situation devrait donc être la suivante :

Voici le test que j’ai mis en place, correspondant à cette situation précise :

[TestMethod]
public void GivenAProviderWhenIFilterOnAgeGT36ThenItFilters()
{
    LambdaBasedPeopleFinder peopleFinder =
        new LambdaBasedPeopleFinder(p => p.Age >= 36);
    PeopleFinderLocator.Instance = peopleFinder;

    QueryableServiceData<Person> people =
        new QueryableServiceData<Person>();

    var query = people
        .Where(p => p.Age >= 36);

    var results = query.ToList();

    Assert.IsTrue(results.All(p => p.Age >= 36));
    Assert.AreEqual(
        new IntCriterion()
            {
                FilterType = NumericFilterType.IsGreaterThan,
                Value = 36
            },
            peopleFinder.PassedCriteria.Age);
}

Dans le test précédent, on utilise l’implémentation du service LambdaBasedPeopleFinder, qui effectue le filtrage des données auquel on s’attend, basé sur un prédicat. D’une certaine façon, on dépend de cette implémentation, car nous testons que les résultats sont correctement filtrés. Mais nous pouvons aller plus loin, et tester que le provider ne fait pas le travail qu’il doit déléguer au service !

Si nous adaptons légèrement le test précédent en remplaçant le lambda fourni au service par p => true, les résultats en retour du service ne seront pas filtrés, et nous pouvons vérifier que le provider délègue bien son travail, et ne filtre pas les données.

[TestMethod]
public void WhenIFilterOnAgeGT36ThenTheProviderDoesntFilter()
{
    LambdaBasedPeopleFinder peopleFinder =
        new LambdaBasedPeopleFinder(p => true);
    PeopleFinderLocator.Instance = peopleFinder;

    QueryableServiceData<Person> people =
        new QueryableServiceData<Person>();

    var query = people
        .Where(p => p.Age >= 36);

    var results = query.ToList();

    Assert.IsFalse(results.All(p => p.Age >= 36));
    Assert.AreEqual(
        new IntCriterion()
        {
            FilterType = NumericFilterType.IsGreaterThan,
            Value = 36
        },
        peopleFinder.PassedCriteria.Age);
}

Finalement, tester le provider n’était pas un problème compliqué (pour des cas aussi simples). Mon problème n’était pas “comment tester”, comme je le pensais au début, mais plutôt “quoi tester”.

Pour finir, voici la liste des prochaines choses que je souhaite faire sur ce projet (l’ordre peut changer) :

  • Mettre en œuvre SpecFlow pour transformer tous mes tests et utiliser la syntaxe Gherkin,
  • Prendre en charge des requêtes plus complexes, lister tous les types de nœuds qui peuvent être rencontrés et tous les gérer, en s’assurant que les données retournées sont toujours correctes. Si nécessaire, tout nœud non géré pourrait entrainer l’utilisation d’un critère de recherche vide, et la réalisation de toutes les opérations de la requête en utilisant Linq to Objects, au détriment de la performance,
  • Implémenter le service, même si ce n’est pas nécessaire, juste pour la satisfaction de le faire,
  • Rester coincé sur la manipulation des arbres d’expression sans savoir comment m’y prendre,
  • Publier la solution sur un répository Github,
  • Obtenir de l’aide de la part de toute personne motivée.

@pirrmann

Laisser un commentaire

Votre adresse de messagerie ne sera pas publiée. Les champs obligatoires sont indiqués avec *