使用Moq进行单元测试有时会失败ToListAsync()

时间:2017-12-25 03:53:09

标签: linq unit-testing asp.net-core moq entity-framework-core

我对使用moq进行测试相当新,而且我有一个奇怪的问题(至少对我来说这似乎很奇怪),但我可能只是没有正确设置模拟对象。我有一个存储库层,它使用EntityFrameworkCore来处理我的DbContext。存储库中的一个特定功能允许我返回一个已排序的列表,而不会将Linq或EFCore函数暴露给调用该函数的服务层。

假设我有一个这样的Model类:

public class SomeClass {
    public string Foo { get; set; }
}

我的DbContext中有一个名为someClasses的DbSet。我用来对某些类进行排序的我的存储库中的三个函数是:

public async Task<List<SomeClass>> GetSomeClassesAsync(string orderBy = "", bool descending = false) {
    var returnVals = _context.someClasses.AsQueryable();

    returnVals = SortQueryableCollectionByProperty(returnVals, orderBy, descending);

    return await returnVals.ToListAsync();
}

private IQueryable<T> SortQueryableCollectionByProperty<T>(IQueryable<T> queryable, string propertyName, bool descending) where T : class {
    if (typeof(T).GetProperty(propertyName) != null) {
        if (descending) {
            queryable = queryable.OrderByDescending(q => GetPropertyValue(propertyName, q));
        } else {
            queryable = queryable.OrderBy(q => GetPropertyValue(propertyName, q));
        }
    }

    return queryable;
}

private object GetPropertyValue<T>(string propertyName, T obj) {
    return obj.GetType().GetProperty(propertyName).GetAccessors()[0].Invoke(obj, null);
}

所以我对GetSomeClassesAsync()进行了2次单元测试。第一个单元测试确保返回的列表由Foo排序,第二个单元测试在尝试按Bar排序(不存在的属性)时返回无序列表。以下是我的测试设置方式:

private Mock<DbContext> mockContext;
private MyRepository repo;

[TestInitialize]
public void InitializeTestData() {
    mockContext = new Mock<DbContext>();

    repo = new MyRepository(mockContext.Object);
}

[TestMethod]
public async Task GetSomeClassesAsync_returns_ordered_list() {
    var data = new List<SomeClass> {
        new SomeClass { Foo = "ZZZ" },
        new SomeClass { Foo = "AAA" },
        new SomeClass { Foo = "CCC" }
    };
    var mockSomeClassDbSet = DbSetMocking.CreateMockSet(new TestAsyncEnumerable<SomeClass>(data));
    mockContext.Setup(m => m.someClasses).Returns(mockSomeClassDbSet.Object);

    var sortedResults = await repo.GetSomeClassesAsync(nameof(SomeClass.Foo));

    Assert.AreEqual("AAA", sortedResults[0].Foo);
    Assert.AreEqual("CCC", sortedResults[1].Foo);
    Assert.AreEqual("ZZZ", sortedResults[2].Foo);
}

[TestMethod]
public async Task GetSomeClassesAsync_returns_unordered_list() {
    var data = new List<SomeClass> {
        new SomeClass { Foo = "ZZZ" },
        new SomeClass { Foo = "AAA" },
        new SomeClass { Foo = "CCC" }
    };
    var mockSomeClassDbSet = DbSetMocking.CreateMockSet(new TestAsyncEnumerable<SomeClass>(data));
    mockContext.Setup(m => m.someClasses).Returns(mockSomeClassDbSet.Object);

    var unsortedResults = await repo.GetSomeClassesAsync("Bar");

    Assert.AreEqual("ZZZ", unsortedResults[0].Foo);
    Assert.AreEqual("AAA", unsortedResults[1].Foo);
    Assert.AreEqual("CCC", unsortedResults[2].Foo);
}

DbSetMocking.CreateMockSet()取自here,而TestAsyncEnumerable取自here

我反击的是第一个返回有序列表的测试。一切正常。第二次测试失败,我收到此错误消息:

  

System.NotImplementedException:未实现方法或操作。

当代码到达ToListAsync()时会抛出此异常。我没有得到的是为什么当它经历排序然后调用ToListAsync()时没有错误发生,但是当跳过排序并且调用ToListAsync()时,抛出该异常。我没有正确设置我的模拟对象吗?

2 个答案:

答案 0 :(得分:2)

MosquitoBite 的答案是正确的。但是对于 .Net 5,IAsyncQueryProvider 接口中的 ExecuteAsync 方法发生了一些变化:

TResult IAsyncQueryProvider.ExecuteAsync<TResult>(Expression expression, CancellationToken cancellationToken)
{
    return Execute<TResult>(expression);
}

对于 IAsyncEnumeratorDisposeAsyncMoveNextAsync 方法也发生了变化:

public ValueTask DisposeAsync()
{
    _inner.Dispose();
    return ValueTask.CompletedTask;
}

public ValueTask<bool> MoveNextAsync()
{
    return ValueTask.FromResult(_inner.MoveNext());
}

答案 1 :(得分:0)

简短的回答是,使用Moq进行的设置中没有任何东西导致引发NotImplementedException。需要设置Ef Provider设置以支持异步方法。

以下是最详尽的答案。 :) 我研究了在测试使用EFCore上下文的异步方法时使用哪种方法。由于setup used with Entity Framework 6上的文档确实非常好,但是the documentation for EFCore专注于InMemoryProvider和SQLite-InMemory-Mode,并且不包括用于异步测试的文档,甚至没有暗示它甚至不是,这并不是很明显。支持的。或更准确地说,我没有找到任何东西。

因此,到目前为止,我发现与EFCore配合使用的解决方案如下:

  1. Follow the EF6 async setup steps described in the MSDN Documentation 这将为您提供一些包装类和某些接口(IDbAsyncQueryProvider,IDbAsyncEnumerable和IDbAsyncEnumerator)上的实现的秘诀。点网核心将找不到这些接口,因为它们在Core中的名称不同,因此您必须重命名它们。
  2. 将接口重命名为现有的Core接口: 这些接口位于Microsoft.EntityFrameworkCore.Query.Internal中,并称为IAsyncQueryProvider,IAsyncEnumerable和IAsyncEnumerator。因此,您只需要从接口名称中删除“ Db”即可。
  3. 在类TestAsyncQueryProvider,TestAsyncEnumerable和TestAsyncEnumerator中注释掉除构造函数和私有字段以外的所有内容
  4. 自动实现接口。选择“实现接口”,在这些类上有红色的线条,您将获得这些接口所需的方法。
  5. EF6接口的实现类似于EFCore接口。只是名字不同。因此,将它们粘贴并修改它们。

或者,如果您想节省时间,请在下面复制并粘贴此代码。 :) 我只是想告诉您我是如何到达那里的,因为这可能不是一成不变的解决方案。但是,除非有一个标准的解决方案(或者至少在我找到它之前),否则这似乎是可行的方法。

public static class DbSetMockSetup
{
    public static Mock<DbSet<T>> SetupMockDbSet<T>(IEnumerable<T> dataToBeReturnedOnGet) where T : class
    {
        var mocks = dataToBeReturnedOnGet.AsQueryable();

        var mockSet = new Mock<DbSet<T>>();
        mockSet.As<IQueryable<T>>().Setup(m => m.Provider).Returns(new TestAsyncQueryProvider<T>(mocks.Provider));
        mockSet.As<IQueryable<T>>().Setup(m => m.Expression).Returns(mocks.Expression);
        mockSet.As<IQueryable<T>>().Setup(m => m.ElementType).Returns(mocks.ElementType);
        mockSet.As<IQueryable<T>>().Setup(m => m.GetEnumerator()).Returns(mocks.GetEnumerator());

        mockSet.As<IAsyncEnumerable<T>>()
            .Setup(x => x.GetEnumerator())
            .Returns(new TestAsyncEnumerator<T>(mocks.GetEnumerator()));

        return mockSet;
    }

}

internal class TestAsyncQueryProvider<TEntity> : IAsyncQueryProvider
{
    private readonly IQueryProvider _inner;

    internal TestAsyncQueryProvider(IQueryProvider inner)
    {
        _inner = inner;
    }

    public IQueryable CreateQuery(Expression expression)
    {
        return new TestAsyncEnumerable<TEntity>(expression);
    }

    public IQueryable<TElement> CreateQuery<TElement>(Expression expression)
    {
        return new TestAsyncEnumerable<TElement>(expression);
    }

    public object Execute(Expression expression)
    {
        return _inner.Execute(expression);
    }

    public TResult Execute<TResult>(Expression expression)
    {
        return _inner.Execute<TResult>(expression);
    }

    public IAsyncEnumerable<TResult> ExecuteAsync<TResult>(Expression expression)
    {
        return new TestAsyncEnumerable<TResult>(expression);
    }

    public Task<TResult> ExecuteAsync<TResult>(Expression expression, CancellationToken cancellationToken)
    {
        return Task.FromResult(Execute<TResult>(expression));
    }
}

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

    public TestAsyncEnumerable(Expression expression)
        : base(expression)
    { }

    public IAsyncEnumerator<T> GetAsyncEnumerator()
    {
        return new TestAsyncEnumerator<T>(this.AsEnumerable().GetEnumerator());
    }

    IQueryProvider IQueryable.Provider
    {
        get { return new TestAsyncQueryProvider<T>(this); }
    }
    public IAsyncEnumerator<T> GetEnumerator()
    {
        return GetAsyncEnumerator();
    }
}

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

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

    public void Dispose()
    {
        _inner.Dispose();
    }

    public T Current
    {
        get { return _inner.Current; }
    }
    public Task<bool> MoveNext(CancellationToken cancellationToken)
    {
        return Task.FromResult(_inner.MoveNext());
    }
}

然后您可以使用类似()的设置:

    public async Task Create_ReturnsModelWithANonEmptyListOfProducts()
    {
        var dbSetOfFoos = DbSetMockSetup.SetupMockDbSet(new List<Foo> { new Foo{ ... }});

        _context.Reset(); // _context is a Mock<MyContext>
        _context.Setup(db => db.Foos).Returns(dbSetOfFoos.Object);

        var sut = new ProductListViewModelFactory(_context.Object);
        var model = await sut.CreateAsync();
        // assert
        ...
    }

我希望这会帮助您解决问题。祝你好运!