22

From the new CosmosDb emulator I got sort of a repository to perform basic documentdb operations, this repository gets injected to other classes. I wanted to unit test a basic query.

public class DocumentDBRepository<T> where T : class
{
 //Details ommited...

    public IQueryable<T> GetQueryable()
    {
        return _client.CreateDocumentQuery<T>(
            UriFactory.CreateDocumentCollectionUri(_databaseId, _collectionId),
            new FeedOptions { MaxItemCount = -1, EnableCrossPartitionQuery = true });
    }

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

        return results;
    }


}

This repository needs a document client to work, which also gets injected on the constructor.

public DocumentDBRepository(string databaseId, IDocumentClient client)
{
    _client = client;
    _databaseId = databaseId;
    _collectionId = GetCollectionName();
}

My initial approach was to use the CosmosDb emulator, but that required the emulator to run which I don't like and makes the tests slower.

My second approach was to try and use a mock of the document client.

var data = new List<MyDocumentClass>
{
    new MyDocumentClass{ Description= "BBB" },
    new MyDocumentClass{ Description= "ZZZ" },

}
.AsQueryable()
.OrderBy(q => q.Description);
var client = new Mock<IDocumentClient>();
client.As<IDocumentClient>()
    .Setup(foo => foo.CreateDocumentQuery<MyDocumentClass>(It.IsAny<Uri>(), It.IsAny<FeedOptions>()))
    .Returns(data);

DocumentDBRepository<MyDocumentClass> repo= new DocumentDBRepository<MyDocumentClass>(cosmosDatabase, client.Object);

The code that uses this repository works like this:

var query = _documentsRepository.GetQueryable()
                .Where(d => d.Description = description)
                .OrderByDescending(d => d.description)
                .Take(100);
//Execute query async fails. 
var result = await _documentsRepository.ExecuteQueryAsync(query);

It fails because the repository tries to convert the IQueryable to a IDocumentQuery object, which is very specific to DocumentDb api (See method ExecuteQueryAsync above). Later on, it executes HasMoreResults method on it. So the problem is, even if I mock .asDocumentQuery() to return my object, I don't know how to provide a result for HasMoreResults and ExecuteNextAsync so that it makes sense for my unit tests.

My third option would be to straight mock my repository instead of the DocumentClient object. Would be, I think, simpler, but I wouldn't be testing much of the DocumentDb api.

Nkosi
  • 191,971
  • 29
  • 311
  • 378
Ernesto
  • 1,452
  • 1
  • 15
  • 30
  • What you need to do is show the body of the `AsDocumentQuery` method from that Cosmos Db. It may be entirely possible to just mock the `IDocumentQuery` and for all the standard `IQueryable` methods call forward to the underlying `List().AsQuyerable()`. If you add the `AsDocumentQuery` listing i may be able to have a go at it – zaitsman Jan 20 '18 at 01:21
  • AsDocumentQuery is an AzureApi method, https://msdn.microsoft.com/en-us/library/azure/dn850283.aspx and I don't really know how it is implemented. That is the problem. – Ernesto Jan 21 '18 at 13:18
  • you do know that you can decompile dll to sources? Install resharper and it is as easy as one click – zaitsman Jan 21 '18 at 20:37
  • I do know. I appreciate the advice but I just can't see how one should need to de-compile source to write a simple unit test. I would do it if that's the only way, but I guess for now I will stick to just mock the repository unless something better appears. – Ernesto Jan 23 '18 at 16:22

2 Answers2

20

The key to this is that the CreateDocumentQuery you are calling, though shown as returning IOrderedQueryable, the encapsulated result will also be derived from IDocumentQuery which is what would allow .AsDocumentQuery() to work.

Now normally you should not be mocking what you do not own. However in this case if you want to exercise ExecuteQueryAsync to completion you can create a fake abstraction that will allow the test to be exercised to completion.

The following Example shows how it can be done.

[TestClass]
public class DocumentDBRepositoryShould {
    /// <summary>
    /// Fake IOrderedQueryable IDocumentQuery for mocking purposes
    /// </summary>        
    public interface IFakeDocumentQuery<T> : IDocumentQuery<T>, IOrderedQueryable<T> {

    }

    [TestMethod]
    public async Task ExecuteQueryAsync() {
        //Arrange
        var description = "BBB";
        var expected = new List<MyDocumentClass> {
            new MyDocumentClass{ Description = description },
            new MyDocumentClass{ Description = "ZZZ" },
            new MyDocumentClass{ Description = "AAA" },
            new MyDocumentClass{ Description = "CCC" },

        };
        var response = new FeedResponse<MyDocumentClass>(expected);

        var mockDocumentQuery = new Mock<IFakeDocumentQuery<MyDocumentClass>>();
        mockDocumentQuery
            .SetupSequence(_ => _.HasMoreResults)
            .Returns(true)
            .Returns(false);

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

        var client = new Mock<IDocumentClient>();

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

        var cosmosDatabase = string.Empty;

        var documentsRepository = new DocumentDBRepository<MyDocumentClass>(cosmosDatabase, client.Object);

        //Act
        var query = documentsRepository.GetQueryable(); //Simple query.

        var actual = await documentsRepository.ExecuteQueryAsync(query);

        //Assert
        actual.Should().BeEquivalentTo(expected);
    }
}
Nkosi
  • 191,971
  • 29
  • 311
  • 378
  • 1
    I think this is it. I am still debating whether I should mock this or not because of the "not mocking what i don't own" but this actually works. I didn't know of SetupSequence which i think was the missing part. Thanks a lot. – Ernesto Jan 25 '18 at 14:55
  • 11
    The "not mocking what you don't own" quote has been taken out of context. As part of unit testing, you should MOCK, STUB, FAKE, or DUMMY anything you can that ensures you are properly isolating your "Unit Under Test" or "System Under Test". Which typically means your 3rd party data source library. Which some 3rd parties make easy to do another like Cosmos DB make very difficult. If you didn't you could never properly "Unit Test" your own repository or "unit of work" implementations as they all would be reading and writting to a data source making them an "Integration Test" – Rodney S. Foley Mar 03 '18 at 18:58
  • Unfortunately `response` is not a valid FeedResponse ... though you can iterate it, you can't touch some useful properties like `RequestCharge` as they will throw an exception. – Iain Jan 11 '19 at 06:41
  • 3
    Does not work with `where` clause or any Linq clause. – RamenTurismo Mar 14 '19 at 15:12
  • 1
    @JeremyF. this particular question was not using filters so the answer addresses the specific issue. Check this one https://stackoverflow.com/questions/54929728/mock-idocumentquery-with-ability-to-use-query-expressions/54930277#54930277 if you want to be able to use queries – Nkosi Mar 14 '19 at 15:14
  • @JeremyF. this one is for moq https://stackoverflow.com/questions/49906029/mocking-idocumentquery-in-unit-test-that-uses-linq-queries?noredirect=1&lq=1 – Nkosi Mar 14 '19 at 15:18
  • @Nkosi I actually found this solution by myself, but now I got cast problems with `AsDocumentQuery()`. I'll be using sync methods from now! – RamenTurismo Mar 14 '19 at 15:36
2

Here is Nkosi's answer ported to NSubstitute:

[TestClass]
public class DocumentDBRepositoryShould
{
    [TestMethod]
    public async Task ExecuteQueryAsync()
    {
        // Arrange
        var description = "BBB";
        var expected = new List<MyDocumentClass> {
            new MyDocumentClass{ Description = description },
            new MyDocumentClass{ Description = "ZZZ" },
            new MyDocumentClass{ Description = "AAA" },
            new MyDocumentClass{ Description = "CCC" },

        };
        var response = new FeedResponse<MyDocumentClass>(expected);

        var mockDocumentQuery = Substitute.For<IFakeDocumentQuery<MyDocumentClass>>();

        mockDocumentQuery.HasMoreResults.Returns(true, false);
        mockDocumentQuery.ExecuteNextAsync<MyDocumentClass>(Arg.Any<CancellationToken>())
            .Returns(Task.FromResult(response));
        
        var client = Substitute.For<IDocumentClient>();
        client.CreateDocumentQuery<MyDocumentClass>(Arg.Any<Uri>(), Arg.Any<FeedOptions>())
            .ReturnsForAnyArgs(mockDocumentQuery);
        var cosmosDatabase = string.Empty;
        var documentsRepository = new DocumentDBRepository<MyDocumentClass>(cosmosDatabase, client);
        
        //Act
        var actual = await documentsRepository.GetDataAsync(); //Simple query.

        //Assert
        actual.Should().BeEquivalentTo(expected);
    }

    public class MyDocumentClass
    {
        public string Description { get; set; }
    }
    
    public interface IFakeDocumentQuery<T> : IDocumentQuery<T>, IOrderedQueryable<T> {

    }
    
    public class DocumentDBRepository<T>
    {
        private readonly string cosmosDatabase;
        private readonly IDocumentClient documentClient;

        public DocumentDBRepository(string cosmosDatabase, IDocumentClient documentClient)
        {
            this.cosmosDatabase = cosmosDatabase;
            this.documentClient = documentClient;
        }

        public async Task<IEnumerable<MyDocumentClass>> GetDataAsync()
        {
            var documentUri = UriFactory.CreateDocumentCollectionUri(cosmosDatabase, "test-collection");
        
            var query = documentClient
                .CreateDocumentQuery<MyDocumentClass>(documentUri)
                .AsDocumentQuery();
        
            var list = new List<MyDocumentClass>();
            while (query.HasMoreResults)
            {
                var rules = await query.ExecuteNextAsync<MyDocumentClass>();
                list.AddRange(rules);
            }
            return list;
        }
    }
}
Rafael Merlin
  • 1,997
  • 19
  • 29
Tjaart
  • 3,323
  • 2
  • 35
  • 55