12
votes

Comment vous moquez-vous d'un IAsyncEnumerable?

Je souhaite tester unitaire une méthode qui appelle une autre méthode d'un service retournant un IAsyncEnumerable<T> . J'ai créé une maquette de mon service Mock<MyService> et je souhaite configurer cette maquette mais je ne sais pas comment faire cela. C'est possible ? Existe-t-il d'autres méthodes de test unitaire d'une méthode qui appelle quelque chose en réaccordant un IAsyncEnumerable

public async Task<List<String>> MyMethodIWantToTest()
{
  var results = new List<string>();
  await foreach(var item in _myService.CallSomethingReturningAsyncStream())
  {
    results.Add(item);
  }
  return results;
}


4 commentaires

Comment vous moquez-vous d'un IEnumerable <T>? Vous ne le faites pas, c'est une interface. Vous créez quelque chose qui renvoie cette interface. Par exemple, au lieu d'une méthode itératrice normale, une méthode itérateur async .


Ce n'est pas différent de la mise en place d'une simulation de toute autre méthode. Vous aurez juste besoin de fournir une implémentation qui renvoie un IAsyncEnumerable , ce que vous pouvez faire en écrivant une méthode d'itérateur asynchrone, et de la connecter à la maquette avec la méthode fournie par votre framework de simulation. Pour simplifier les tests, si la nature asynchrone de la méthode n'a aucune importance pour le test, vous pouvez également utiliser la méthode d'extension Enumerable.ToAsyncEnumerable() du package System.Linq.Async pour simplement utiliser n'importe quel ancien énumérable régulier (comme un tableau).


Est-ce que la vraie question est peut-être de savoir comment utiliser votre framework moqueur? Quel framework moqueur utilisez-vous? Vous pouvez convertir n'importe quel IEnumerable <T> en un IAsyncEnumerable <T> avec une méthode d'itérateur qui renvoie IAsyncEnumerable<T> , par exemple: async IAsyncEnumerable<T> ToAsyncEnumerable<T>(IEnumerable<T> inp) { foreach(var item in inp) yield return item; }


Merci @PanagiotisKanavos, j'utilise Mock et j'étais coincé avec le fait que Moq ne fournit pas ReturnsAsync pour se moquer de la méthode a réaccordant un IAsyncEnumerable. Je ne savais pas que la solution était si simple, merci :)


4 Réponses :


1
votes

Cela dépend vraiment du framework moqueur que vous utilisez. Mais ce serait quelque chose de simple comme cet exemple en utilisant Moq

var data = new [] {1,2,3,4};
var mockSvc = new Mock<MyService>();
mockSvc.Setup(obj => obj.CallSomethingReturningAsyncStream()).Returns(data.ToAsyncEnumerable());


0 commentaires

16
votes

Je recommande d'utiliser ToAsyncEnumerable de System.Linq.Async , comme Jeroen l'a suggéré. Il semble que vous utilisez Moq, donc cela ressemblerait à ceci:

async Task MyTest()
{
  var mock = new Mock<MyService>();
  var mockData = new[] { "first", "second" };
  mock.Setup(x => x.CallSomethingReturningAsyncStream()).Returns(mockData.ToAsyncEnumerable());

  var sut = new SystemUnderTest(mock.Object);
  var result = await sut.MyMethodIWantToTest();

  // TODO: verify `result`
}


3 commentaires

J'ai voté pour m'avoir .ToAsyncEnumerable() sur .ToAsyncEnumerable() . Merci.


Fonctionne aussi, j'ai voté pour mais sélectionné la réponse qui n'a pas besoin d'une autre pépite. Je connaissais System.Linq.Async mais je ne savais pas qu'ils fournissaient une méthode aussi intéressante. Merci de nous en informer :)


++ C'est le chemin le plus simple lors de la mise à niveau du code existant



17
votes

Si vous ne voulez rien faire de spécial, par exemple un retour retardé qui est généralement le point des énumérables asynchrones, vous pouvez simplement créer une fonction de générateur qui renvoie les valeurs pour vous.

var serviceMock = new Mock<IMyService>();
serviceMock.Setup(s => s.CallSomethingReturningAsyncStream()).Returns(GetTestValues);

var thing = new Thing(serviceMock.Object);
var result = await thing.MyMethodIWantToTest();
Assert.Equal("foo", result[0]);
Assert.Equal("bar", result[1]);

Avec cela, vous pouvez simplement créer une maquette pour votre service et tester votre objet:

public static async IAsyncEnumerable<string> GetTestValues()
{
    yield return "foo";
    yield return "bar";

    await Task.CompletedTask; // to make the compiler warning go away
}

Bien sûr, puisque vous utilisez maintenant une fonction de générateur, vous pouvez également rendre cela plus compliqué et ajouter des retards réels, ou même inclure un mécanisme pour contrôler le rendement.


0 commentaires

1
votes

Une façon de résoudre ce problème consiste à utiliser des classes de test dédiées qui encapsulent un IEnumerable qui est énuméré de manière synchrone.

TestAsyncEnumerable.cs

[Fact]
public async Task MyTest() {
   var myItemRepository = A.Fake<IMyItemRepository>();

   A.CallTo(       () => myRepository.GetAll())
    .ReturnsLazily(() => new TestAsyncEnumerable<MyItem>(new List<MyItem> { new MyItem(), ... }));


   //////////////////
   /// ACT & ASSERT
   ////////
}

Usage:

internal class TestAsyncEnumerable<T> : List<T>, IAsyncEnumerable<T>
{
   public TestAsyncEnumerable(IEnumerable<T> enumerable) : base(enumerable) { }

   public IAsyncEnumerator<T> GetAsyncEnumerator(CancellationToken cancellationToken = default) => new TestAsyncEnumerator<T>(GetEnumerator());
}

internal class TestAsyncEnumerator<T> : IAsyncEnumerator<T>
{
   private readonly IEnumerator<T> _inner;

   public TestAsyncEnumerator(IEnumerator<T> inner)
   {
      _inner = inner;
   }

   public ValueTask<bool> MoveNextAsync() => new ValueTask<bool>(_inner.MoveNext());

   public T Current => _inner.Current;

   public ValueTask DisposeAsync()
   {
      _inner.Dispose();

      return new ValueTask(Task.CompletedTask);
   }
}


0 commentaires