在使用Linq查询的单元测试中模拟IDocumentQuery

sup*_*nja 6 unit-testing moq azure asp.net-core azure-cosmosdb

我正在编写单元测试DocumentDBRepository但是我得到了一个空引用异常.我使用Moq框架和XUnit.

这是我DocumentDBRepository课堂上的方法.

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;
        }            
    }
}
Run Code Online (Sandbox Code Playgroud)

这是我的测试代码:

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);
    }
}
Run Code Online (Sandbox Code Playgroud)

这是运行测试方法后的错误消息:

消息:System.NullReferenceException:未将对象引用设置为对象的实例.

当我逐步完成代码时,错误发生在测试代码调用GetQuery()方法之后:

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

这是我的思考过程:当我逐步完成整个代码时,我没有看到任何空变量.但是在测试方法第二行的'response'变量中,它确实显示了很多属性为null异常,但结果视图显示了'expected'变量.

我的问题是,是因为响应变量导致空引用异常?或者别的地方?

PS:从这里测试代码参考

我也尝试将Mock行为打开为严格并看到此错误消息.

消息:System.AggregateException:发生一个或多个错误.(IDocumentClient.ReadDatabaseAsync(dbs/123,null)调用失败,模拟行为为Strict.模拟上的所有调用都必须具有相应的设置.)---- Moq.MockException:IDocumentClient.ReadDatabaseAsync(dbs/123,null)调用失败与模拟行为严格.模拟上的所有调用都必须具有相应的设置.

Nko*_*osi 5

正如怀疑的问题是.Where(predicate)。我使用提供的示例进行了测试并删除了该.Where子句,它执行完成。

假接口继承自IOrderedQueryableIDocumentQuery。问题在于,由于数据源的原因,它Where正在将其转换回普通格式,并且由于它期望一个IEnumerableListAsDocumentQueryIDocumentQuery

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

解决方法包括必须提供一个假的 LinqIQueryProvider来绕过任何查询并返回派生自的类型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;
    }
}
Run Code Online (Sandbox Code Playgroud)

请注意,getQuery没有执行任何异步操作,因此无论如何它都不应该返回 a Task<>

IDocumentQuery接下来,在测试中设置模拟以允许测试顺利完成。这是通过提供一个模拟来完成的,当针对它调用 Linq 查询时,IQueryProvider它将返回模拟。(这就是问题的根源)IDocumentQuery

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);
}
Run Code Online (Sandbox Code Playgroud)

这使得测试能够完成、按预期运行并通过测试。

  • 我面临着类似的情况,在研究了这段代码之后,我想知道这个测试是否有很多(任何?)优点,考虑到几乎所有东西都被嘲笑了。`GetEntities` 和 `getQuery` 没有业务规则。那么,最后到底测试了什么?谢谢! (4认同)