如何模拟 IAsyncEnumerable?

53
我想对调用服务的另一个返回 IAsyncEnumerable<T> 方法的方法进行单元测试。 我已创建了服务的模拟 Mock<MyService>,并希望设置此模拟,但我不知道如何做到。这可能吗?还有其他方法可以对调用返回 IAsyncEnumerable 的方法进行单元测试吗?
public async Task<List<String>> MyMethodIWantToTest()
{
  var results = new List<string>();
  await foreach(var item in _myService.CallSomethingReturningAsyncStream())
  {
    results.Add(item);
  }
  return results;
}

你如何模拟一个IEnumerable<T>?你不能,因为它是一个接口。你需要创建一个返回该接口的对象。例如,你可以使用异步迭代器方法来替代普通的迭代器方法。 - Panagiotis Kanavos
5
这与设置其他任何方法的模拟没有什么不同。您只需要提供一个返回IAsyncEnumerable的实现,可以通过编写异步迭代器方法来完成,并使用您的模拟框架提供的任何方法将其连接到模拟中。为了简化测试,如果方法的异步性对测试没有重要性,您还可以使用System.Linq.Async包的Enumerable.ToAsyncEnumerable()扩展方法来使用任何旧的常规可枚举对象(如数组)。 - Jeroen Mostert
2
也许真正的问题是如何与你的模拟框架一起使用它?你正在使用哪个模拟框架?你可以使用一个迭代器方法将任何IEnumerable<T>转换为IAsyncEnumerable<T>,该方法返回IAsyncEnumerable<T>,例如:async IAsyncEnumerable<T> ToAsyncEnumerable<T>(IEnumerable<T> inp) { foreach(var item in inp) yield return item; } - Panagiotis Kanavos
谢谢@PanagiotisKanavos,我正在使用Mock,但我卡在了Moq不提供ReturnsAsync来模拟返回IAsyncEnumerable的方法上。我没有意识到解决方案是如此简单,谢谢 :) - TechWatching
4个回答

77

我建议使用System.Linq.Async中的ToAsyncEnumerable,就像Jeroen建议的那样。看起来你正在使用Moq,所以代码如下:

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`
}

11
因为教会了我 .ToAsyncEnumerable() 这个方法,所以我点了一个赞。谢谢。 - Trae Moore
2
可以正常工作,我点了赞,但选择了不需要其他 NuGet 包的答案。我知道 System.Linq.Async,但没有意识到它们提供了这么有趣的方法。谢谢告诉我们 :) - TechWatching
1
++ 当升级现有代码时,这是最简单的路径。 - mhand
1
如果您正在使用IAsyncEnumerable<T>,那么您应该考虑使用System.Linq.Async,因为您可能需要更多的方法。在我看来,这是更好的答案。 - Nate Zaugg

52

如果你不想做任何特殊的事情,比如延迟返回通常是异步枚举的重点,那么你可以创建一个生成器函数来为你返回值。

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

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

有了它,你可以轻松地为你的服务创建一个模拟,并测试你的对象:

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]);
当然,由于您现在使用的是生成器函数,因此您还可以使其更加复杂,并添加实际的延迟,甚至包括一些机制来控制 yield。

7

这真的取决于你使用的模拟框架。但是,如果使用Moq,那么可以像这个简单的例子一样:

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

4

解决这个问题的一种方法是使用专门的测试类,它包装了一个同步枚举的IEnumerable

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

使用方法:

[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
   ////////
}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接