Mocking IDocumentQuery in Unit Test that uses Linq queries

时间:2018-04-18 17:51:11

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

I am writing unit tests for DocumentDBRepository but I got a null reference exception. I use Moq framework and XUnit.

Here's my methods in DocumentDBRepository class.

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

Here's my test code:

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

Here's the error message after running the test method:

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

When I stepped through the code, the error happens right after the the test code called GetQuery() method:

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

Here's my thought process: when I stepped through the entire code, I do not see any null variables. But in the 'response' variable from the second line of the test method, it does show a lot of the properties are null exception but result view shows the 'expected' variable.

My question is, is it because of the response variable that caused the null reference exception? Or somewhere else?

PS: Test code reference from here

I also tried turning on the Mock behavior to strict and saw this error message.

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.

1 个答案:

答案 0 :(得分:4)

怀疑问题是.Where(predicate)。我使用提供的示例运行测试并删除.Where子句并执行完成。

虚假接口继承自IOrderedQueryableIDocumentQuery。问题是Where正在将其转换回普通IEnumerable,因为List数据源并且AsDocumentQuery因为期待{{1}而被淘汰}}

我不喜欢与我无法控制的API紧密耦合。出于这个原因,我会抽象出这些实现细节。

需要提供假Linq IDocumentQuery以绕过任何查询并返回源自IQueryProvider的类型,以便允许IDocumentQuery按预期行事。

但首先我重构了AsDocumentQuery并使GetEntities私有化以阻止存储库成为漏洞。

GetQuery

请注意,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<>以允许测试流程完成。这是通过提供模拟的IDocumentQuery来完成的,当针对它调用Linq查询时,将返回模拟的IQueryProvider(这是问题的原因)

IDocumentQuery

这允许测试完成,表现如预期,并通过测试。