如何(我应该)在进行DocumentDb单元测试时模拟DocumentClient?

22

我从新的 CosmosDb 模拟器中获得了一种执行基本 DocumentDb 操作的存储库,将该存储库注入到其他类中。我想对一个基本查询进行单元测试。

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


}

这个代码库需要一个文档客户端来工作,该客户端也会在构造函数中注入。

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

我的初始方法是使用CosmosDb模拟器,但这需要运行模拟器,我不喜欢它,而且会使测试变慢。

我的第二种方法是尝试使用文档客户端的模拟。

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

这个仓库所使用的代码工作方式如下:
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);

它失败的原因是,存储库试图将 IQueryable 转换为 IDocumentQuery 对象,这对于 DocumentDb api 是非常特定的(请参见上面的 ExecuteQueryAsync 方法)。之后,它在其上执行 HasMoreResults 方法。所以问题在于,即使我模拟了 .asDocumentQuery() 返回我的对象,我也不知道如何为 HasMoreResultsExecuteNextAsync 提供合理的结果,以便对我的单元测试有意义。

我的第三个选项是直接模拟我的存储库而不是 DocumentClient 对象。 我认为这会更简单,但我不会测试 DocumentDb api 的很多内容。


你需要展示来自 Cosmos Db 的 AsDocumentQuery 方法的主体内容。很可能只需模拟 IDocumentQuery<T>,并对所有标准的 IQueryable 方法调用基础的 List<T>().AsQuyerable()。如果你添加了 AsDocumentQuery 的列表,我可以试着去做。 - zaitsman
AsDocumentQuery是AzureApi的一个方法,https://msdn.microsoft.com/en-us/library/azure/dn850283.aspx,我并不知道它是如何实现的。这就是问题所在。 - Ernesto
你知道可以将DLL反编译为源代码吗?安装Resharper只需一个点击即可轻松实现。 - zaitsman
我明白。我很感激你的建议,但是我实在想不通为什么要反编译源代码才能写一个简单的单元测试。如果那是唯一的方法,我会去做的,但是目前我打算继续用模拟存储库的方式,除非出现了更好的方法。 - Ernesto
2个回答

21
关键在于,您调用的CreateDocumentQuery尽管显示返回IOrderedQueryable,但封装的结果也将派生自IDocumentQuery,这将允许.AsDocumentQuery()起作用。

现在通常情况下不应模拟您不拥有的内容。然而,在这种情况下,如果您想要完成ExecuteQueryAsync的练习,您可以创建一个假抽象层,使测试能够得到完整的练习。

以下示例展示了如何实现此操作。

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

1
我想这就是了。我仍在考虑是否应该模拟这个,因为有“不模拟我不拥有的东西”的原则,但这确实有效。我不知道 SetupSequence,我认为这是缺失的部分。非常感谢。 - Ernesto
12
“不要嘲笑你所不拥有的”这句话被断章取义了。作为单元测试的一部分,你应该对任何能确保你正确隔离“被测单元”或“系统被测单元”的东西进行MOCK、STUB、FAKE或DUMMY处理。这通常意味着你的第三方数据源库。有些第三方库(例如Cosmos DB)很难实现,而其他库则更容易。如果你不这样做,你就无法正确地“单元测试”你自己的存储库或“工作单元”实现,因为它们都将读写数据源,从而成为“集成测试”。 - Rodney S. Foley
3
无法与 where 子句或任何 Linq 子句一起使用。 - Emy Blacksmith
1
@JeremyF,这个问题没有使用过滤器,所以答案解决了具体问题。如果您想要能够使用查询,请查看此链接https://dev59.com/KbLma4cB1Zd3GeqPXDI4#54930277 - Nkosi
@JeremyF,这是关于Moq的问题:https://dev59.com/Pqrka4cB1Zd3GeqPiLp3?noredirect=1&lq=1 - Nkosi
显示剩余2条评论

2

以下是将 Nkosi 的答案转换为 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;
        }
    }
}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接