unit-testing - 在使用 Linq 查询的单元测试中模拟 IDocumentQuery

标签 unit-testing azure asp.net-core moq azure-cosmosdb

我正在为 DocumentDBRepository 编写单元测试,但出现空引用异常。我使用 Moq 框架和 XUnit。

这是我在 DocumentDBRepository 类中的方法。

public class DocumentDBRepository<T> : IRepository<T> where T: class
{
    private static string DatabaseId;
    private static string CollectionId;
    private static IDocumentClient client;
    public DocumentDBRepository(IDocumentClient documentClient, string databaseId, string collectionId)
    {
        DatabaseId = databaseId;
        CollectionId = collectionId;
        client = documentClient;
        CreateDatabaseIfNotExistsAsync().Wait();
        CreateCollectionIfNotExistsAsync().Wait();
    }

    public async Task<IDocumentQuery<T>> GetQuery(Expression<Func<T, bool>> predicate)
    {
        try
        {
            IDocumentQuery<T> query = client.CreateDocumentQuery<T>(
          UriFactory.CreateDocumentCollectionUri(DatabaseId, CollectionId),
          new FeedOptions { MaxItemCount = -1, EnableCrossPartitionQuery = true })
          .Where(predicate)
          .AsDocumentQuery();

            return query;
        }
        catch (Exception e) {
            throw;
        }    
    }

    public async Task<IEnumerable<T>> GetEntities(IDocumentQuery<T> query)
    {
        try
        {
            List<T> results = new List<T>();
            while (query.HasMoreResults)
            {
                results.AddRange(await query.ExecuteNextAsync<T>());
            }

            return results;
        }
        catch (Exception e)
        {
            throw;
        }            
    }
}

这是我的测试代码:

public interface IFakeDocumentQuery<T> : IDocumentQuery<T>, IOrderedQueryable<T>
{

}

[Fact]
public async virtual Task Test_GetBooksById()
{

    var expected = new List<Book> {
        new Book { ID = "123", Description = "HarryPotter"},
        new Book { ID = "124", Description = "HarryPotter2"} };


    var response = new FeedResponse<Book>(expected);

    var mockDocumentQuery = new Mock<IFakeDocumentQuery<Book>>();

    mockDocumentQuery.SetupSequence(_ => _.HasMoreResults)
                     .Returns(true)
                     .Returns(false);

    mockDocumentQuery.Setup(_ => _.ExecuteNextAsync<Book>(It.IsAny<CancellationToken>()))
                     .ReturnsAsync(response);

    var client = new Mock<IDocumentClient>();

    client.Setup(_ => _.CreateDocumentQuery<Book>(It.IsAny<Uri>(), It.IsAny<FeedOptions>()))
          .Returns(mockDocumentQuery.Object);

    var documentsRepository = new DocumentDBRepository<Book>(client.Object, "123", "123");

    //Act
    var query = await documentsRepository.GetQuery(t => t != null);
    var entities = await documentsRepository.GetEntities(query);

    //Assert
    if (entities != null)
    {
        entities.Should().BeEquivalentTo(expected);
    }
}

这是运行测试方法后的错误消息:

Message: System.NullReferenceException : Object reference not set to an instance of an object.

当我单步执行代码时,错误发生在调用 GetQuery() 方法的测试代码之后:

 IDocumentQuery<T> query = client.CreateDocumentQuery<T>(
              UriFactory.CreateDocumentCollectionUri(DatabaseId, CollectionId),
              new FeedOptions { MaxItemCount = -1, EnableCrossPartitionQuery = true })
              .Where(predicate)
              .AsDocumentQuery();

这是我的思考过程:当我单步执行整个代码时,我没有看到任何空变量。但是在测试方法第二行的“response”变量中,它确实显示了很多属性为空的异常,但结果 View 显示了“预期”变量。

我的问题是,是否是因为响应变量导致了空引用异常?还是其他地方?

PS:测试代码引用来自here

我还尝试将 Mock 行为打开为严格,并看到了此错误消息。

Message: System.AggregateException : One or more errors occurred. (IDocumentClient.ReadDatabaseAsync(dbs/123, null) invocation failed with mock behavior Strict. All invocations on the mock must have a corresponding setup.) ---- Moq.MockException : IDocumentClient.ReadDatabaseAsync(dbs/123, null) invocation failed with mock behavior Strict. All invocations on the mock must have a corresponding setup.

最佳答案

正如怀疑的问题是 .Where(predicate) 。我使用提供的示例进行了测试并删除了 .Where条款并执行至完成。

假接口(interface)继承自 IOrderedQueryableIDocumentQuery 。问题是Where正在将其转换回普通的 IEnumerable因为List数据源和 AsDocumentQuery正在崩溃,因为它期待 IDocumentQuery

我不喜欢与我无法控制的 API 紧密耦合。正是出于这个原因,我会围绕此类实现细节进行抽象。

解决方法包括必须提供一个假的 Linq IQueryProvider绕过任何查询并返回派生自 IDocumentQuery 的类型以便允许 AsDocumentQuery按预期行事。

但首先我重构了GetEntities并制作GetQuery private 来阻止存储库成为一个有漏洞的抽象。

private IDocumentQuery<T> getQuery(Expression<Func<T, bool>> predicate) {
    var uri = UriFactory.CreateDocumentCollectionUri(DatabaseId, CollectionId);
    var feedOptions = new FeedOptions { MaxItemCount = -1, EnableCrossPartitionQuery = true };
    var queryable = client.CreateDocumentQuery<T>(uri, feedOptions);
    IQueryable<T> filter = queryable.Where(predicate);
    IDocumentQuery<T> query = filter.AsDocumentQuery();
    return query;
}

public async Task<IEnumerable<T>> GetEntities(Expression<Func<T, bool>> predicate) {
    try {
        IDocumentQuery<T> query = getQuery(predicate);
        var results = new List<T>();
        while (query.HasMoreResults) {
            results.AddRange(await query.ExecuteNextAsync<T>());
        }
        return results;
    } catch (Exception e) {
        throw;
    }
}

请注意getQuery没有执行任何异步操作,因此它不应该返回 Task<>无论如何。

下一步在测试中被 mock IDocumentQuery被设置为允许测试顺利完成。这是通过提供模拟的 IQueryProvider 来完成的将返回 mock 的 IDocumentQuery当针对它调用 Linq 查询时。 (这就是问题开始的原因)

public async virtual Task Test_GetBooksById() {
    //Arrange
    var id = "123";
    Expression<Func<Book, bool>> predicate = t => t.ID == id;
    var dataSource = new List<Book> {
        new Book { ID = id, Description = "HarryPotter"},
        new Book { ID = "124", Description = "HarryPotter2"} 
    }.AsQueryable();

    var expected = dataSource.Where(predicate);

    var response = new FeedResponse<Book>(expected);

    var mockDocumentQuery = new Mock<IFakeDocumentQuery<Book>>();

    mockDocumentQuery
        .SetupSequence(_ => _.HasMoreResults)
        .Returns(true)
        .Returns(false);

    mockDocumentQuery
        .Setup(_ => _.ExecuteNextAsync<Book>(It.IsAny<CancellationToken>()))
        .ReturnsAsync(response);

    var provider = new Mock<IQueryProvider>();
    provider
        .Setup(_ => _.CreateQuery<Book>(It.IsAny<System.Linq.Expressions.Expression>()))
        .Returns((Expression expression) => {                
            if (expression != null) {
                dataSource = dataSource.Provider.CreateQuery<Book>(expression);
            }
            mockDocumentQuery.Object;
        });

    mockDocumentQuery.As<IQueryable<Book>>().Setup(x => x.Provider).Returns(provider.Object);
    mockDocumentQuery.As<IQueryable<Book>>().Setup(x => x.Expression).Returns(() => dataSource.Expression);
    mockDocumentQuery.As<IQueryable<Book>>().Setup(x => x.ElementType).Returns(() => dataSource.ElementType);
    mockDocumentQuery.As<IQueryable<Book>>().Setup(x => x.GetEnumerator()).Returns(() => dataSource.GetEnumerator());

    var client = new Mock<IDocumentClient>();

    client.Setup(_ => _.CreateDocumentQuery<Book>(It.IsAny<Uri>(), It.IsAny<FeedOptions>()))
          .Returns(mockDocumentQuery.Object);

    var documentsRepository = new DocumentDBRepository<Book>(client.Object, "123", "123");

    //Act
    var entities = await documentsRepository.GetEntities(predicate);

    //Assert
    entities.Should()
        .NotBeNullOrEmpty()
        .And.BeEquivalentTo(expected);
}

这使得测试能够完成、按预期运行并通过测试。

关于unit-testing - 在使用 Linq 查询的单元测试中模拟 IDocumentQuery,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/49906029/

相关文章:

javascript - 浏览器控制台错误 : Invalid character: '@'

c# - 如何将身份用户从 MVC5 应用程序迁移到 ASP.NET Core 2.2 应用程序

sql - 主设备和事件辅助设备之间的通信是否安全以及其工作原理

Azure DNS - 如何创建 MB 和 RP 记录?

linux - 如何在 Azure 上使用 ssh 认证部署 Linux VM

asp.net-core - 在 AfterMap 方法 (AutoMapper) 中异步设置 View 模型的属性

asp.net-core - Blazor 服务器和 SignalR 以及 Azure AD

java - Powermock 在模拟静态对象时使用 spring 注入(inject)错误模拟对象

java - 使用 Mockito 模拟注入(inject) Spring Boot 组件

Angular Mock ActivatedRoute 使用 Snapshot 和 ParamMap