49

I want to unit test a method that calls another method of a service returning an IAsyncEnumerable<T>. I have created a a mock of my service Mock<MyService> and I want to setUp this mock but I don't know how to do that. Is it possible ? Are there other ways of unit testing a method that calls something retuning an IAsyncEnumerable

public async Task<List<String>> MyMethodIWantToTest()
{
  var results = new List<string>();
  await foreach(var item in _myService.CallSomethingReturningAsyncStream())
  {
    results.Add(item);
  }
  return results;
}
Kirill Rakhman
  • 42,195
  • 18
  • 124
  • 148
TechWatching
  • 1,363
  • 1
  • 11
  • 23
  • How do you mock an IEnumerable ? You don't, it's an interface. You create something that returns that interface. Eg instead of a normal iterator method, an `async` iterator method. – Panagiotis Kanavos Nov 27 '19 at 14:10
  • 5
    It's no different from setting up a mock of any other method. You'll just need to provide an implementation that returns an `IAsyncEnumerable`, which you can do by writing an async iterator method, and hook this up to the mock with whatever method your mocking framework provides. For simplicity of testing, if the asynchronous nature of the method is of no importance to the test, you can also use the `Enumerable.ToAsyncEnumerable()` extension method of the `System.Linq.Async` package to just use any old regular enumerable (like an array). – Jeroen Mostert Nov 27 '19 at 14:11
  • 2
    Is the real question perhaps how to use your mocking framework with it? What mocking framework are you using? You could convert any IEnumerable into an IAsyncEnumerable with an iterator method that returns `IAsyncEnumerable`, eg : `async IAsyncEnumerable ToAsyncEnumerable(IEnumerable inp) { foreach(var item in inp) yield return item; }` – Panagiotis Kanavos Nov 27 '19 at 14:11
  • Thanks @PanagiotisKanavos, I am using Mock and I was stucked with the fact that Moq does not provide ReturnsAsync for mocking the a method retuning an IAsyncEnumerable. I did not realize solution was so simple, thanks :) – TechWatching Nov 27 '19 at 14:42

4 Answers4

73

I recommend using ToAsyncEnumerable from System.Linq.Async, as Jeroen suggested. It seems like you're using Moq, so this would look like:

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`
}
poke
  • 369,085
  • 72
  • 557
  • 602
Stephen Cleary
  • 437,863
  • 77
  • 675
  • 810
  • 10
    Upvoted for teaching me about `.ToAsyncEnumerable()`. Thank you. – Trae Moore Nov 27 '19 at 14:30
  • 2
    Works as well,I upvoted but selected the answer that does not need another nuget. I knew about System.Linq.Async but did not realize they provided such an interesting method. Thanks for letting us know :) – TechWatching Nov 27 '19 at 14:44
  • 1
    ++ This is the easiest path when upgrading existing code – mhand Feb 08 '20 at 21:56
  • 1
    If you're using IAsyncEnumerable, then you should consider using the `System.Linq.Async` as you'll likely want/need more than just that one method. This is the better answer IMHO – Nate Zaugg Nov 29 '21 at 19:57
50

If you don’t want to do anything special, e.g. a delayed return which is usually the point of async enumerables, then you can just create a generator function that returns the values for you.

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

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

With that, you can simply create a mock for your service and test your object:

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]);

Of course, since you are now using a generator function, you can also make this more complicated and add actual delays, or even include some mechanism to control the yielding.

poke
  • 369,085
  • 72
  • 557
  • 602
7

It really depends on which mocking framework your using. But, it would be something simple like this example using Moq

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

One way of solving this is to use dedicated test classes that wrap an IEnumerable that is enumerated synchronously.

TestAsyncEnumerable.cs

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);
   }
}

Usage:

[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
   ////////
}
silkfire
  • 24,585
  • 15
  • 82
  • 105