6

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.

Nkosi
  • 235,767
  • 35
  • 427
  • 472
superninja
  • 3,114
  • 7
  • 30
  • 63
  • 1
    It is most likely because of `.Where(predicate)` but that is just from my initial look at what you provided. Have not had the time to give it a proper review as yet. – Nkosi Apr 18 '18 at 18:22
  • 1
    Provide the constructor for `DocumentDBRepository` so that a [mcve] can be done to reproduce the problem. – Nkosi Apr 18 '18 at 18:23
  • Thanks I'll take a look. But would be greatly appreciate if you could review :) – superninja Apr 18 '18 at 18:23
  • 1
    You are trying to Setup `CreateDocumentQuery` to return `IDocumentQuery` but it actually returns `IOrderedQueryable`. That means that yout mock setup won't match and it will return null on the object. It will only return `IDocumentQuery` once you do cast it with `AsDocumentQuery`. It works on that other example because there is no where clause. – Nick Chapsas Apr 18 '18 at 18:24
  • @Elfocrash I changed from `client.Setup(_ => _.CreateDocumentQuery(It.IsAny(), It.IsAny())) .Returns(mockDocumentQuery.Object);` to `client.Setup(_ => _.CreateDocumentQuery(It.IsAny(), It.IsAny()).AsDocumentQuery()) .Returns(mockDocumentQuery.Object);` The program stops right after this line and the error shows `Message: System.NotSupportedException : Invalid setup on an extension method: _ => _.CreateDocumentQuery(It.IsAny(), It.IsAny()).AsDocumentQuery()` – superninja Apr 18 '18 at 18:57
  • @Elfocrash But I do understand and agree with your reasoning. – superninja Apr 18 '18 at 18:58
  • 1
    @WendyWang This wouldn't work. It's not valid mock setup. You need to keep the setup part the same but change your return to something that implements `IOrderedQueryable`. – Nick Chapsas Apr 18 '18 at 19:12
  • 2
    As suspected the problem is `.Where`. Ran a test with your example and removed the `.Where` and it executes to completion. The fake interface inherits from both `IOrderedQueryable` and `IDocumentQuery`. The issue is that the `Where` is converting it back to a plain `IEnumerable` and the `AsDocumentQuery` is crapping out as it is expecting an `IDocumentQuery` . Investigating now. – Nkosi Apr 18 '18 at 19:27
  • @Nkosi Yes I can confirm on my end that removing `Where` make the test passed.. – superninja Apr 18 '18 at 19:32
  • @Nkosi Could you please show me an example with code for how to fix it? Or would it make sense to do unit testing on local document DB? – superninja Apr 18 '18 at 20:40

1 Answers1

5

As suspected the problem is .Where(predicate). I ran a test with the provided example and removed the .Where clause and it executed to completion.

The fake interface inherits from both IOrderedQueryable and IDocumentQuery. The issue is that the Where is converting it back to a plain IEnumerable because of the List data source and the AsDocumentQuery is crapping out as it is expecting an IDocumentQuery

I am not a fan of tightly coupling to APIs I can't control. I would abstract my way around such implementation details for that very reason.

The work around involved having to provide a fake Linq IQueryProvider to bypass any queries and return a type that derives from IDocumentQuery so as to allow AsDocumentQuery to behave as intended.

But first I refactored GetEntities and made GetQuery private to stop the repository from being a leaky abstraction.

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

Note that getQuery is not doing anything async so it should not be returning a Task<> anyway.

Next in the test the mocked IDocumentQuery was set up to allow the test to flow to completion. This was done by providing a mocked IQueryProvider the would return the mocked IDocumentQuery when Linq queries are invoked against it. (which was the cause of the problem to begin with)

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

This allowed the test to be exercised to completion, behave as expected, and pass the test.

Nkosi
  • 235,767
  • 35
  • 427
  • 472
  • 4
    I'm facing similar situation and after studying this code I'm wondering if there is much (any?) merit in this test, considering that almost everything has been mocked. `GetEntities` and `getQuery` have no business rules. So, at the end, what exactly has been tested? Thanks! – luizs81 Jun 28 '18 at 08:02