web-dev-qa-db-fra.com

Comment (dois-je) simuler DocumentClient pour les tests unitaires DocumentDb?

Du nouvel émulateur CosmosDb, j'ai obtenu une sorte de référentiel pour effectuer les opérations de base de documentdb, ce référentiel est injecté dans d'autres classes. Je voulais tester unitaire une requête de base.

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


}

Ce référentiel a besoin d'un client de document pour fonctionner, qui est également injecté sur le constructeur.

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

Mon approche initiale était d'utiliser l'émulateur CosmosDb, mais cela nécessitait que l'émulateur fonctionne, ce que je n'aime pas et rend les tests plus lents.

Ma deuxième approche a été d'essayer d'utiliser une maquette du client de document.

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

Le code qui utilise ce référentiel fonctionne comme ceci:

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

Il échoue car le référentiel essaie de convertir le IQueryable en un objet IDocumentQuery, qui est très spécifique à l'API DocumentDb (voir la méthode ExecuteQueryAsync ci-dessus). Plus tard, il exécute la méthode HasMoreResults dessus. Donc, le problème est, même si je me moque de .asDocumentQuery() pour retourner mon objet, je ne sais pas comment fournir un résultat pour HasMoreResults et ExecuteNextAsync afin que cela ait du sens pour mes tests unitaires.

Ma troisième option serait de se moquer directement de mon référentiel au lieu de l'objet DocumentClient. Ce serait, je pense, plus simple, mais je ne testerais pas beaucoup de l'API DocumentDb.

17
Ernesto

La clé de ceci est que le CreateDocumentQuery que vous appelez, bien qu'il soit affiché comme retournant IOrderedQueryable, le résultat encapsulé sera également dérivé de IDocumentQuery, ce qui autoriserait la fonction .AsDocumentQuery() travailler.

Maintenant, normalement, vous ne devriez pas vous moquer de ce que vous ne possédez pas. Cependant, dans ce cas, si vous souhaitez exercer ExecuteQueryAsync jusqu'à la fin, vous pouvez créer une fausse abstraction qui permettra au test d'être exercé jusqu'à la fin.

L'exemple suivant montre comment cela peut être fait.

[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);
    }
}
16
Nkosi