How do you mock an IAsyncEnumerable?

后端 未结 4 1152
时光取名叫无心
时光取名叫无心 2020-12-20 13:23

I want to unit test a method that calls another method of a service returning an IAsyncEnumerable. I have created a a mock of my service Mock<

相关标签:
4条回答
  • 2020-12-20 14:01

    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
       ////////
    }
    
    0 讨论(0)
  • 2020-12-20 14:11

    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());
    
    0 讨论(0)
  • 2020-12-20 14:18

    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.

    0 讨论(0)
  • 2020-12-20 14:28

    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`
    }
    
    0 讨论(0)
提交回复
热议问题