NSubstitute : framework de mocking pour .NET

NSubstitute permet de créer facilement des objets mock pour vos tests unitaires avec une syntaxe fluide.

Notions de base

Substitution
var mock = Substitute.For<IFoo>();
Retour simple
mock.Bar().Returns(42);
Valeur calculée
mock.Add(Arg.Any<int>(), Arg.Any<int>()).Returns(callInfo => callInfo.Arg<int>(0) + callInfo.Arg<int>(1));
Exceptions
mock.DoSomething().Throws(new InvalidOperationException());
Vérification d’appel
mock.Received().Bar();
mock.DidNotReceive().Baz();
Arguments spéciaux
Arg.Is<string>(s => s.StartsWith("Hello"));
Partial mock
var partial = Substitute.ForPartsOf<MyConcreteClass>();
Capturer les arguments
int captured;
// Simple variant (with When/Do)
mock.When(x => x.Do(Arg.Any<int>())).Do(call => captured = call.Arg<int>());
// (then execute mock.Do(123); so that captured = 123)

Exemple complet

Le code à tester
public interface ICalculator
{
    int Add(int a, int b);
    void Log(string message);
}

public class MathService
{
    private readonly ICalculator _calculator;

    public MathService(ICalculator calculator)
    {
        _calculator = calculator;
    }

    public int ComputeSumAndLog(int x, int y)
    {
        var result = _calculator.Add(x, y);
        _calculator.Log($"Result is {result}");
        return result;
    }
}
Le test avec NSubstitute
using Xunit;
using NSubstitute;

public class MathServiceTests
{
    [Fact]
    public void ComputeSumAndLog_ReturnsSum_LogsMessage()
    {
  // Arrange: create a mock of ICalculator
  var calc = Substitute.For<ICalculator>();

  // Force Add() to return 5 when passed 2 and 3
  calc.Add(2, 3).Returns(5);

  // Prepare the service with the mock
  var sut = new MathService(calc);

  // Act: call the tested method
  int result = sut.ComputeSumAndLog(2, 3);

  // Assert: expected result
  Assert.Equal(5, result);

  // Assert: verify Log was called with the expected message
  calc.Received().Log("Result is 5");
    }
}
Remarques
  • <code>calc.Add(2,3).Returns(5)</code> force la valeur de retour uniquement pour ces arguments.
  • Si vous ne précisez pas de retour explicite, le mock renverra la valeur par défaut du type (<code>0</code> pour <code>int</code>, <code>null</code> pour les références, etc.).

Cas pratiques supplémentaires

Mock d’une méthode asynchrone
public interface IAsyncService
{
    Task<string> GetDataAsync(string key);
}

var asyncMock = Substitute.For<IAsyncService>();
asyncMock.GetDataAsync("abc").Returns(Task.FromResult("mocked data"));
Mock d’une méthode qui lance une exception
calc.DoSomething().Throws(new ArgumentException());
Vérifier le nombre d’appels
// Au moins deux appels
calc.Received(2).Log(Arg.Any<string>());

// Aucun appel
calc.DidNotReceive().Add(Arg.Any<int>(), Arg.Any<int>());
Utiliser When pour capturer l’argument passé
int captured = -1;
calc.Add(Arg.Any<int>(), Arg.Any<int>())
    .Returns(x => x.Arg<int>(0) + x.Arg<int>(1));

calc.When(call => call.Log(Arg.Any<string>()))
    .Do(callInfo =>
    {
  var msg = callInfo.GetArguments()[0] as string;
  // extract the number from the message
  captured = int.Parse(msg.Split(' ')[2]);
    });

Bonnes pratiques

  • Noms explicites — exemple lisible comme un pseudo‑code :
    calc.Add(Arg.Any<int>(), Arg.Any<int>()).Returns(call => ...)
  • Évite les mocks inutiles — si vous n’interagissez pas avec l’objet, ne le mockez pas.
  • Utilise Substitute.ForPartsOf<T>() — pour tester une classe concrète partiellement — vous gardez la logique interne et vous ne stubb ez que le nécessaire.
  • Ne cache jamais un bug — si vous stubbez tout, un test peut passer même si le code est incorrect.
Écrit le 2025-08-12