Alternatives à Moq pour les Tests unitaire?

Au cours de l’été 2023, un événement secoue la communauté des développeurs C# : la nouvelle version de Moq (4.20.0) ferait du scraping d’information en récupérant l’adresse email de ses utilisateurs. Ce serait provoqué par l’ajout de SponsorLink à Moq, un autre projet du même créateur, qui permet d’intégrer les sponsors GitHub dans les projets. SponsorLink récupérerait les adresses email des comptes Git dans les repos utilisant des dépendances qui l’incluent.

Depuis le créateur a retiré SponsorLink de Moq (4.20.69).

Cet événement pourrait avoir brisé la confiance des développeurs envers Moq, incitant ceux-ci à changer de librairie pour mocker les tests.

Bien que cette polémique soit maintenant lointaine, c’est un bon point de départ pour cet article. Cela fait un moment que je voulais étudier les alternatives à Moq, pour voir si l’herbe est plus verte ailleurs, sans jamais réussir à prendre le temps. Profitons de cette histoire pour le faire.

Qu’est-ce qu’un Mock et comment en faire avec Moq?

Lorsque l’on écrit des tests unitaires on souhaite uniquement tester le comportement de la classe ou du service à tester. Cependant, il y a souvent des dépendances vers d’autres objets ou services, plus ou moins complexes, dont le comportement est inconnu et non contrôlable.

Un test doit rester simple, ciblé et rapide à écrire.

C’est à ce moment là qu’interviennent les Mocks.

Un mock est un objet simulé ayant la même interface publique qu’un objet réel permettant de contrôler ou remplacer le comportement de celui-ci.

Par exemple, imaginons que nous avons le service suivant :

C#
public class MyOtherService
{
    private readonly IMyService _myService;
    
    public MyOtherService(IMyService myService)
    {
        _myService = myService;
    }

    public string IsValidAsString(int number)
    {
        return _myService.IsValid(number) ? "Is Valid" : "Is Not Valid";
    }
}

Le service à une dépendance vers l’interface IMyService :

C#
public interface IMyService
{
    public bool IsValid(int number);
}

L’implémentation de cette interface ne nous concerne pas ici, elle pourrait très bien appeler une API REST, faire une recherche en base de données, lire un fichier, faire un calcul, etc. On ne sait pas, et on a pas à le savoir.

Notre service expose donc une méthode IsValidAsString qui converti le résultat de IMyService.IsValid en string.

On souhaite tester unitairement cette méthode :

C#
using FluentAssertions;

namespace MockExamples.UnitTests;

public class MyOtherServiceTests
{
    [Fact]
    public void IsValidAsString_IsValid()
    {
        MyOtherService myOtherService = new(/*??*/);

        string result = myOtherService.IsValidAsString(42);

        result.Should().Be("Is Valid");
    }

    [Fact]
    public void IsValidAsString_IsNotValid()
    {
        MyOtherService myOtherService = new(/*??*/);

        string result = myOtherService.IsValidAsString(23);

        result.Should().Be("Is Not Valid");
    }
}

Comme dit juste avant, on ne connait pas l’implémentation de IMyService et on ne sait donc pas quoi injecter dans le service. De plus, il faut qu’on soit en mesure de savoir exactement comment elle fonctionne pour tester les différents cas de nos tests.

On pourrait nous même écrire une implémentation pour les besoins du test, c’est ce qu’on appelle faire un stub. Mais cela rallongerait la taille, la complexité et le temps d’écriture du test. C’est pas top.

C’est là que les mocks interviennent. Ils permettent de définir le comportement de fausses implémentations de services que l’on va injecter au service à tester.

Ce qui donnerait un code qui ressemble à ça :

C#
using FluentAssertions;
using Moq;

namespace MockExamples.UnitTests;

public class MyOtherServiceTests
{
    private readonly Mock<IMyService> _myServiceMock = new();

    [Fact]
    public void IsValidAsString_IsValid()
    {
        _myServiceMock.Setup(m => m.IsValid(42)).Returns(true);
        MyOtherService myOtherService = new(_myServiceMock.Object);

        string result = myOtherService.IsValidAsString(42);

        result.Should().Be("Is Valid");
        _myServiceMock.Verify(m => m.IsValid(42), Times.Once);
    }

    [Fact]
    public void IsValidAsString_IsNotValid()
    {
        _myServiceMock.Setup(m => m.IsValid(It.IsAny<int>())).Returns(false);
        MyOtherService myOtherService = new(_myServiceMock.Object);

        string result = myOtherService.IsValidAsString(23);

        result.Should().Be("Is Not Valid");
        _myServiceMock.Verify(m => m.IsValid(It.IsAny<int>()), Times.Once);
    }
}

On définit un Mock sur IMyService :

private readonly Mock<IMyService> _myServiceMock = new();

On définit le comportement d’une méthode pour des paramètres données et ce qui doit être retourné avec la méthode Setup :

...
_myServiceMock.Setup(m => m.IsValid(42)).Returns(true);
...
_myServiceMock.Setup(m => m.IsValid(It.IsAny<int>())).Returns(false);

It.IsAny<T>() est une sorte de meta variable qui permet de remplacer un paramètre d’une méthode par n’importe quelle valeur possible, utile lorsque l’on ne connait pas les valeurs possibles pour un Setup, ou lors d’un Verify si on veut s’assurer de l’appel d’une méthode peu importe la valeur des paramètres.

On injecte l’objet dans le service (on n’utilise pas directement le mock ici) :

MyOtherService myOtherService = new(myServiceMock.Object);

On peut ensuite tester la méthode comme d’habitude.

En plus des vérifications habituelles sur les résultats du code, on peut utiliser Verify(), qui permet de vérifier le nombre de fois où une méthode est appelée (pratique pour vérifier qu’une méthode n’est jamais appelée, ou sur une méthode qui ne retourne rien pour savoir si elle fait bien ce qui est attendu).

Alternatives

En faisant quelques recherches on trouve un certains nombre d’alternatives à Moq, plus ou moins intéressantes :

  • NSubstitute
  • FakeItEasy
  • Rhino Mocks
  • JustMock

J’élimine Rhino Mocks, qui semble abandonné depuis des années et JustMock qui est payant.

Il reste donc NSubstitute et FakeItEasy.

NSubstitute

NSbustitute est en général le premier nom qui ressort quand on cherche une alternative à Moq. Il s’agit d’un projet open source sous licence BSD.

Voici à quoi ressemble l’utilisation de NSubstitute, en gardant le même exemple :

C#
using FluentAssertions;
using NSubstitute;

namespace MockExamples.UnitTests;

public class MyOtherServiceWithNSubstituteTests
{    
    private readonly IMyService _myServiceMock = Substitute.For<IMyService>();

    [Fact]
    public void IsValidAsString_IsValid()
    {
        _myServiceMock.IsValid(42).Returns(true);
        MyOtherService myOtherService = new(_myServiceMock);

        string result = myOtherService.IsValidAsString(42);

        result.Should().Be("Is Valid");
        _myServiceMock.Received().IsValid(42);
    }

    [Fact]
    public void IsValidAsString_IsNotValid()
    {

        _myServiceMock.IsValid(Arg.Any<int>()).Returns(false);
        MyOtherService myOtherService = new(_myServiceMock);

        string result = myOtherService.IsValidAsString(23);

        result.Should().Be("Is Not Valid");
        _myServiceMock.Received().IsValid(Arg.Any<int>());
    }
}

Il y a quelques différences avec Moq. Le première, on utilise des Substitute au lieux de mock.

L’instanciation est similaire, on dit qu’on souhaite avoir un substitut de IMyService.

Au lieu d’appeler une méthode Setup, avec NSubstitute on appelle la méthode Returns à la suite de la méthode que l’on souhaite mocker. Les paramètres de la méthode sont directement dans celle-ci et pas dans le mock.

La plus grosse différence avec Moq est qu’au lieu de manipuler un objet mock à parti duquel on appelle la propriété .Object pour récupérer l’instance utilisable dans les tests, le subtitut est directement l’instance utilisable.

On peut aussi noter les points suivants :

  • Verify est remplacé par Received, suivi de la méthode à tester.
  • It.IsAny est remplacé par Arg.Any.

La syntaxe est plus « fluent », perso, j’aime bien. C’est également plus facile à appréhender selon moi. En effet l’absence d’objet mock intermédiaire facilite la manipulation et la compréhension.

FakeItEasy

FakeItEasy est également open source, comme les deux précédents, sous licence MIT.

Voyons à quoi ressemble le test vu précédemment avec FakeItEasy :

C#
using FakeItEasy;
using FluentAssertions;

namespace MockExamples.UnitTests;

public class MyOtherServiceWithFakeItEasyTests
{
    private readonly IMyService _myServiceMock = A.Fake<IMyService>();

    [Fact]
    public void IsValidAsString_IsValid()
    {
        A.CallTo(() => _myServiceMock.IsValid(42)).Returns(true);
        MyOtherService myOtherService = new(_myServiceMock);

        string result = myOtherService.IsValidAsString(42);

        result.Should().Be("Is Valid");
        A.CallTo(() => _myServiceMock.IsValid(42)).MustHaveHappened();
    }

    [Fact]
    public void IsValidAsString_IsNotValid()
    {
        A.CallTo(() => _myServiceMock.IsValid(A<int>.Ignored)).Returns(false);
        MyOtherService myOtherService = new(_myServiceMock);

        string result = myOtherService.IsValidAsString(23);

        result.Should().Be("Is Not Valid");
        A.CallTo(() => _myServiceMock.IsValid(A<int>.Ignored)).MustHaveHappened();
    }
}

Cette fois en lieu et place de substitut ou de mock, nous avons des Fakes. Par contre on ne les manipules pas directement, on passe par une méthode statique de la librairie : A.CallTo. Cette méthode permet de manipuler les fakes et d’y ajouter le comportement attendu :

  • Returns, qui remplace Setup pour donner les valeurs de retour des méthodes
  • MustHaveHappened, qui remplace Verify, pour vérifier les appels.

Dans la même façon, la meta variable est une propriété statique : A<T>.Ignored.

Comme pour NSubstitute, on ne manipule pas un objet mock dans le test mais directement l’objet final, on se passe donc encore une fois d’un appel à .Object, ce qui allège le code.

Je trouve que la syntaxe est plus lourde que NSubstitute. Cependant, celle-ci permet de différencier facilement le code de mock du reste.

Conclusion

Après ce petit tour d’horizon, très rapide, de Moq et des deux principales alternatives, j’espère vous avoir éclairé sur le sujet. Le but n’est pas ici de prôner une alternative plutôt qu’une autre, mais de voir ce qui existe et faire un choix éclairé au moment d’écrire ses tests.

Personnellement, je pense utiliser plus NSubstitute, car la syntaxe me parle plus avec l’approche fluent, mais les deux autres librairies sont des solutions tout aussi valides.

Liens

https://fr.wikipedia.org/wiki/Mock_(programmation_orientée_objet)

https://blog.elmah.io/moq-vs-nsubstitute-vs-fakeiteasy-which-one-to-choose/

https://beetechnical.com/tech-tutorial/alternatives-to-moq/

https://nsubstitute.github.io/help/received-calls/

https://fakeiteasy.github.io

Laissez un commentaire

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

Ce site utilise Akismet pour réduire les indésirables. En savoir plus sur comment les données de vos commentaires sont utilisées.