Tag Archives: tests unitaires

Quoi de neuf dans FakeItEasy 3.0.0 ?

FakeItEasy est un framework de mocking populaire pour .NET, avec une API intuitive et facile à utiliser. Depuis environ un an, je suis un des principaux développeurs de FakeItEasy, avec Adam Ralph and Blair Conrad. Ça a été un vrai plaisir de travailler avec eux, et je me suis éclaté !

Aujourd’hui j’ai le plaisir d’annoncer la sortie de FakeItEasy 3.0.0, avec le support de .NET Core et quelques fonctionnalités utiles.

Voyons ce que cette nouvelle version apporte !

Support de .NET Core

En plus de .NET 4+, FakeItEasy supporte maintenant .NET Standard 1.6, vous pouvez donc l’utiliser dans vos projets .NET Core.

Notez qu’en raison de certaines limitations de .NET Standard 1.x, il y a quelques différences mineures avec la version .NET 4 de FakeItEasy :

  • Les fakes ne sont pas sérialisables avec la sérialisation binaire
  • Les fakes "auto-initialisés" (self-initializing fakes) ne sont pas supportés (c’est-à-dire fakeService = A.Fake<IService>(options => options.Wrapping(realService).RecordedBy(recorder))).

Un immense merci aux personnes qui ont rendu possible le support de .NET Core :

  • Jonathon Rossi, qui maintient le projet Castle.Core. FakeItEasy s’appuie beaucoup sur Castle.Core, il n’aurait donc pas été possible de supporter .NET Core sans que Castle.Core le supporte aussi.
  • Jeremy Meng de Microsoft, qui a fait l’essentiel du gros-œuvre pour porter FakeItEasy et Castle.Core vers .NET Core.

Analyseur

Support de VB.NET

L’analyseur FakeItEasy, qui avertit en cas d’usage incorrect de la librairie, supporte maintenant VB.NET en plus de C#.

Nouvelles fonctionnalités et améliorations

Syntaxe améliorée pour configurer des appels successifs au même membre

Quand on configure les appels vers un fake, cela crée des règles qui sont "empilées" les unes sur les autres, ce qui fait qu’on peut écraser une règle précédemment configurée. Combiné avec la possibilité de spécifier le nombre de fois qu’une règle doit s’appliquer, cela permet de configurer des choses comme "renvoie 42 deux fois, puis lance une exception". Jusqu’ici, pour faire cela il fallait configurer les appels dans l’ordre inverse, ce qui n’était pas très intuitif et obligeait à répéter la spécification de l’appel :

A.CallTo(() => foo.Bar()).Throws(new Exception("oops"));
A.CallTo(() => foo.Bar()).Returns(42).Twice();

FakeItEasy 3.0.0 introduit une nouvelle syntaxe pour rendre cela plus simple :

A.CallTo(() => foo.Bar()).Returns(42).Twice()
    .Then.Throws(new Exception("oops"));

Notez que si vous ne spécifiez pas le nombre de fois que la règle doit s’appliquer, elle s’appliquera indéfiniment jusqu’à ce qu’elle soit écrasée. Par conséquent, on ne peut utiliser Then qu’après Once(), Twice() ou NumberOfTimes(...).

C’est un breaking change au niveau de l’API, dans la mesure où la forme des interfaces de configuration a changé, mais à moins que vous ne manipuliez explicitement ces interfaces, vous ne devriez pas être affecté.

Support automatique pour l’annulation

Quand une méthode accepte un paramètre de type CancellationToken, elle doit généralement lancer une exception si on lui passe un token qui est déjà annulé. Jusqu’à maintenant, ce comportement devait être configuré manuellement. Dans FakeItEasy 3.0.0, les méthodes d’un fake lanceront une OperationCanceledException par défaut si on les appelle avec un token déjà annulé. Les méthodes asynchrones renverront une tâche annulée.

Techniquement, c’est également un breaking change, mais la plupart des utilisateurs ne devraient pas être affectés.

Lancer une exceptionde façon asynchrone

FakeItEasy permet de configurer une méthode pour qu’elle lance une exception grâce à la méthode Throws. Mais pour les méthodes asynchrones, il y a en fait deux façons de signaler une erreur :

  • lancer une exception de façon synchrone, avant même de renvoyer une tâche (c’est ce que fait Throws)
  • renvoyer une tâche échouée (cela devait être fait manuellement jusqu’à maintenant)

Dans certains cas la différence peut être importante pour l’appelant, s’il n’await pas directement la méthode asynchrone. FakeItEasy introduit une méthode ThrowsAsync pour configurer une méthode pour qu’elle renvoie une tâche échouée :

A.CallTo(() => foo.BarAsync()).ThrowsAsync(new Exception("foo"));

Configuration des setters de propriétés sur les fakes non-naturels

Les fakes non-naturels (c’est-à-dire Fake<T>) ont maintenant une méthode CallsToSet, qui fait la même chose que A.CallToSet sur les fakes naturels :

var fake = new Fake<IFoo>();
fake.CallsToSet(foo => foo.Bar).To(0).Throws(new Exception("The value of Bar can't be 0"));

API améliorée pour spécifier des attributs supplémentaires

L’API pour spécifier des attributs supplémentaires sur les fakes n’était pas très pratique; il fallait créer une collection de CustomAttributeBuilders, qui eux-mêmes devaient être créés en spécifiant le constructeur et les valeurs des arguments. La méthode WithAdditionalAttributes a été supprimée dans FakeItEasy 3.0.0 et remplacée par une méthode plus simple WithAttributes qui accepte des expressions :

var foo = A.Fake<IFoo>(x => x.WithAttributes(() => new FooAttribute()));

C’est un breaking change.

Autres changements notables

Dépréciation des fakes auto-initialisés (self-initializing fakes)

Les fakes "auto-initialisés" permettent d’enregistrer les appels faits sur un vrai objet, et de faire rejouer les résultats obtenus par un fake (voir l’article de Martin Fowler à ce sujet). Cette fonctionnalité était utilisée par très peu de gens, et n’avait pas vraiment sa place dans la librairie principale de FakeItEasy. Elle est donc dépréciée et sera retirée dans une version future. Nous envisageons de fournir une fonctionnalité équivalente en tant que package distinct.

Corrections de bugs

  • Créer plusieurs fakes d’un même type avec des attributs supplémentaires différents génère maintenant plusieurs types de fake distincts. (#436)
  • Tous les appels non-void tentent maintenant de renvoyer un Dummy par défaut, même après avoir été reconfigurés par Invokes ou DoesNothing (#830)

La liste complète des changements est disponible dans les notes de versions.

Les autres personnes ayant contribué à cette version sont :

Un grand merci à eux !

Injecter automatiquement des fakes dans une test fixture avec FakeItEasy

Aujourd’hui j’aimerais partager avec vous une fonctionnalité sympa de FakeItEasy que j’ai découverte récemment.

Quand on écrit des tests unitaires pour une classe qui a des dépendances, on a généralement besoin de créer des objets factices (fakes) pour les dépendances et de les injecter manuellement dans la classe testée (SUT, System Under Test), ou d’utiliser un conteneur IoC pour enregistrer les dépendances factices et construire le SUT. C’est un peu laborieux, et il y a quelques mois j’avais créé une extension Unity pour créer automatiquement les fakes et rendre tout ça plus facile. Mais maintenant je réalise que FakeItEasy offre une solution encore meilleure : déclarer simplement les dépendances et le SUT comme des propriétés de la fixture, et appeler Fake.InitializeFixture sur la fixture pour les initialiser. Voilà à quoi ça ressemble :

public class BlogManagerTests
{
    [Fake] public IBlogPostRepository BlogPostRepository { get; set; }
    [Fake] public ISocialNetworkNotifier SocialNetworkNotifier { get; set; }
    [Fake] public ITimeService TimeService { get; set; }
 
    [UnderTest] public BlogManager BlogManager { get; set; }
 
    public BlogManagerTests()
    {
        Fake.InitializeFixture(this);
    }
 
    [Fact]
    public void NewPost_should_add_blog_post_to_repository()
    {
        var post = A.Dummy<BlogPost>();
 
        BlogManager.NewPost(post);
 
        A.CallTo(() => BlogPostRepository.Add(post)).MustHaveHappened();
    }
 
    [Fact]
    public void PublishPost_should_update_post_in_repository_and_publish_link_to_social_networks()
    {
        var publishDate = DateTimeOffset.Now;
        A.CallTo(() => TimeService.Now).Returns(publishDate);
 
        var post = A.Dummy<BlogPost>();
 
        BlogManager.PublishPost(post);
 
        Assert.Equal(BlogPostStatus.Published, post.Status);
        Assert.Equal(publishDate, post.PublishDate);
 
        A.CallTo(() => BlogPostRepository.Update(post)).MustHaveHappened();
        A.CallTo(() => SocialNetworkNotifier.PublishLink(post)).MustHaveHappened();
    }
}

Le SUT est déclaré comme une propriété avec l’attribut [UnderTest]. Chaque dépendance qu’on a besoin de manipuler est déclarée comme une propriété avec l’attribut [Fake]. La méthode Fake.InitializeFixture initialise le SUT, en créant les dépendances factices à la volée, et affecte ces dépendances aux propriétés correspondantes.

J’aime beaucoup le fait que les tests semblent beaucoup plus propres avec cette technique ; le code de plomberie est réduit au strict minimum, il ne reste plus qu’à configurer les dépendances et écrire les tests proprement dits.

Quelques remarques sur le code ci-dessus :

  • On peut utiliser des champs privés plutôt que des propriétés publiques pour les fakes et le SUT, mais comme ces champs ne sont jamais explicitement assignés dans le code, cela provoque un avertissement du compilateur (CS0649), donc je préfère utiliser des propriétés.
  • Les tests dans mon exemple utilisent xUnit, j’ai donc mis l’appel à Fake.InitializeFixture dans le constructeur, mais si vous utilisez un autre framework comme NUnit ou MSTest, vous pouvez le mettre dans la méthode Setup.

Notez aussi qu’il y a des limites aux scénarios supportés par cette approche :

  • seule l’injection par constructeur est supportée, pas l’injection par propriétés (autrement dit les dépendances doivent être des paramètres du constructeur du SUT)
  • les dépendances nommées ne sont pas supportées ; seul le type est pris en compte, on ne peut donc pas avoir plusieurs dépendances distinctes du même type
  • les collections de dépendances ne sont pas supportées (c’est à dire si votre SUT reçoit une collection de dépendances, par exemple IFooService[])

Test unitaires asynchrones avec NUnit

Récemment, mon équipe et moi avons commencé à écrire des tests unitaires pour une application qui utilise beaucoup de code asynchrone. Nous avons utilisé NUnit (2.6) parce que nous le connaissions déjà bien, mais nous ne l’avions encore jamais utilisé pour tester du code asynchrone.

Supposons que le système à tester soit cette très intéressante classe Calculator :

    public class Calculator
    {
        public async Task<int> AddAsync(int x, int y)
        {
            // simulate long calculation
            await Task.Delay(100).ConfigureAwait(false);
            // the answer to life, the universe and everything.
            return 42;
        }
    }

(Indice: ce code contient un bug… 42 n’est pas toujours la réponse. Ça m’a fait un choc quand j’ai appris ça!)

Et voici un test unitaire pour la méthode AddAsync :

        [Test]
        public async void AddAsync_Returns_The_Sum_Of_X_And_Y()
        {
            var calculator = new Calculator();
            int result = await calculator.AddAsync(1, 1);
            Assert.AreEqual(2, result);
        }

async void vs. async Task

Avant même de lancer ce test, je me suis dit : Ça ne va pas marcher! une méthode async void va retourner immédiatement sur le premier await, NUnit va donc croire que le test est terminé alors que l’assertion n’a pas été exécutée, et le test va donc passer même si l’assertion échoue. J’ai donc changé la signature de la méthode en async Task, en me croyant très malin d’avoir évité ce piège…

        [Test]
        public async Task AddAsync_Returns_The_Sum_Of_X_And_Y()

Comme prévu, le test a échoué, ce qui confirme que NUnit sait gérer les tests asynchrones. J’ai corrigé la classe Calculator, et je n’y ai plus pensé. Jusqu’au jour où j’ai remarqué qu’un collègue écrivait ses tests avec async void. J’ai donc commencé à lui expliquer pourquoi ça ne pouvait pas marcher, et j’ai essayé de le lui démontrer en ajoutant une assertion qui échouerait… et à ma grande surprise, le test a échoué, prouvant que j’avais tort !

Etant d’une nature curieuse, j’ai aussitôt commencé à investiguer… Ma première idée a été de vérifier le SynchronizationContext courant, et en effet, j’ai vu que NUnit l’avant remplacé par une instance de NUnit.Framework.AsyncSynchronizationContext. Cette classe maintient une file des continuations qui sont postées dessus. Après que la méthode async void retourne (c’est-à-dire la première fois qu’on await une tâche qui n’est pas encore terminée), NUnit appelle la méthode WaitForPendingOperationsToComplete, qui exécute toute les continuations de la file, jusqu’à ce que celle-ci soit vide. C’est seulement là que le test sera considéré comme terminé.

La morale de cette histoire est donc que vous pouvez écrire des tests async void avec NUnit 2.6. Cela fonctionne aussi avec les delegates passés à Assert.Throws, qui peuvent avoir le modificateur async. Cela étant dit, ce n’est pas parce que vous pouvez le faire que c’est forcément une bonne idée… Les frameworks de test unitaire n’ont pas tous le même support pour cela, et la prochaine version de NUnit (la 3.0, encore en alpha), ne supportera pas les tests async void.

Donc, à moins que vous ne comptiez rester sur NUnit 2.6.4 ad vitam æternam, il vaut probablement toujours utiliser async Task dans vos tests unitaires.

Un moyen facile de tester unitairement la validation des arguments null

Quand on teste unitairement une méthode, une des choses à tester est la validation des arguments : par exemple, vérifier que la méthode lève bien une ArgumentNullException quand un argument null est passé pour un paramètre qui ne doit pas être null. Ecrire ce genre de test est très facile, mais c’est une tâche fastidieuse et répétitive, surtout pour une méthode qui a beaucoup de paramètres. J’ai donc écrit une méthode qui automatise en partie cette tâche : elle essaie de passer null pour chacun des arguments spécifiés, et vérifie que la méthode lève bien une ArgumentNullException. Voici un exemple qui teste une méthode d’extension FullOuterJoin :

[Test]
public void FullOuterJoin_Throws_If_Argument_Null()
{
    var left = Enumerable.Empty<int>();
    var right = Enumerable.Empty<int>();
    TestHelper.AssertThrowsWhenArgumentNull(
        () => left.FullOuterJoin(right, x => x, y => y, (k, x, y) => 0, 0, 0, null),
        "left", "right", "leftKeySelector", "rightKeySelector", "resultSelector");
}

Le premier paramètre est une expression lambda qui représente la façon d’appeler la méthode testée. Dans cette lambda, tous les arguments passés à la méthode doivent être valides. Les paramètres suivants sont les noms des paramètres qui ne doivent pas être null. Pour chacun des noms spécifiés, AssertThrowsWhenArgumentNull va remplacer l’argument correspondant par null dans l’expression lambda, compiler et invoquer l’expression lambda, et vérifier que la méthode lève bien une ArgumentNullException.

Grâce à cette méthode, au lieu d’écrire un test pour chacun des arguments à valider, il suffit d’un seul test.

Voici le code de la méthode TestHelper.AssertThrowsWhenArgumentNull (vous pouvez aussi le trouver sur Gist):

using System;
using System.Linq;
using System.Linq.Expressions;
using NUnit.Framework;

namespace MyLibrary.Tests
{
    static class TestHelper
    {
        public static void AssertThrowsWhenArgumentNull(Expression<TestDelegate> expr, params string[] paramNames)
        {
            var realCall = expr.Body as MethodCallExpression;
            if (realCall == null)
                throw new ArgumentException("Expression body is not a method call", "expr");

            var realArgs = realCall.Arguments;
            var paramIndexes = realCall.Method.GetParameters()
                .Select((p, i) => new { p, i })
                .ToDictionary(x => x.p.Name, x => x.i);
            var paramTypes = realCall.Method.GetParameters()
                .ToDictionary(p => p.Name, p => p.ParameterType);
            
            

            foreach (var paramName in paramNames)
            {
                var args = realArgs.ToArray();
                args[paramIndexes[paramName]] = Expression.Constant(null, paramTypes[paramName]);
                var call = Expression.Call(realCall.Method, args);
                var lambda = Expression.Lambda<TestDelegate>(call);
                var action = lambda.Compile();
                var ex = Assert.Throws<ArgumentNullException>(action, "Expected ArgumentNullException for parameter '{0}', but none was thrown.", paramName);
                Assert.AreEqual(paramName, ex.ParamName);
            }
        }

    }
}

Notez que cette méthode a été écrite pour NUnit, mais vous pouvez facilement l’adapter à d’autres frameworks de test unitaire.

J’ai utilisé cette méthode dans ma librairie Linq.Extras, qui fournit de nombreuses méthodes d’extension supplémentaires pour travailler avec des séquences et collections (elle inclut par exemple la méthode FullOuterJoin mentionnée plus haut).