首页 > 解决方案 > 单元测试 - 模拟 - 可覆盖问题 - 扩展方法

问题描述

我正在尝试为以下 Azure 搜索方法编写单元测试:

public async Task Write(ISearchIndexClient indexClient, Search search)
{
    await UploadContents(indexClient, search.SearchContents);
}

private static async Task UploadContents(ISearchIndexClient indexClient, IReadOnlyCollection<dynamic> searchContents) => await Task.Run(() => indexClient.Documents.Index(IndexBatch.Upload(searchContents)));

单元测试代码1:

public async Task Write_Success()
{
    var searchIndexClientMock = new Mock<ISearchIndexClient>();
    searchIndexClientMock
        .Setup(x => x.Documents.Index(It.IsAny<IndexBatch<Document>>(), It.IsAny<SearchRequestOptions>()))
        .Returns(It.IsAny<DocumentIndexResult>()).Callback(() => IndexBatch.Upload(It.IsAny<IEnumerable<Document>>()));

    var pushFunction = new SearchIndexWriter();

    Search search = new Search();

    await pushFunction.Write(searchIndexClientMock.Object, search);

    //Assert, Verify checks
}

我收到以下错误:

消息:System.NotSupportedException:不支持的表达式:... => ....Index(It.IsAny>(), It.IsAny()) 扩展方法(此处:DocumentsOperationsExtensions.Index)可能无法用于设置/验证表达式。

单元测试代码 2:

public async Task Write_Success()
{
    var searchIndexClientMock = new Mock<ISearchIndexClient>();
    searchIndexClientMock
        .SetupGet(x => x.Documents).Returns(It.IsAny<IDocumentsOperations>());

    var pushFunction = new SearchIndexWriter();

    var search = new Search()
    {
        SearchContents = new List<dynamic>(),
    };

    await pushFunction.Write(searchIndexClientMock.Object, search);

    //Verify, Assert logic
}

我收到以下错误:

消息:System.NullReferenceException:对象引用未设置为对象的实例。
    在 Microsoft.Azure.Search.DocumentsOperationsExtensions.Index[T](IDocumentsOperations 操作,IndexBatch^1 批次,SearchRequestOptions searchRequestOptions,CancellationToken cancelToken)
    在 Microsoft.Azure.Search.DocumentsOperationsExtensions.Index[T](IDocumentsOperations 操作,IndexBatch^1 批次,搜索请求选项搜索请求选项)

如何测试上传功能?

标签: c#azureunit-testingmockingmoq

解决方案


您基本上是在尝试测试 3rd 方依赖项的功能。尽管这些依赖项具有可以模拟的抽象,但它们需要进行不必要的设置来隔离它们以进行单元测试,这对我来说是一种代码味道。

我建议抽象出第 3 方的依赖

private readonly ISearchService service;

//...assuming service injected
public SearchIndexWriter(ISearchService service) {
    this.service = service;
}

public Task Write(ISearchIndexClient indexClient, Search search) {
    return service.UploadContents(indexClient, search.SearchContents);
}

尽量避免与静态关注点紧密耦合,因为这会使孤立的单元测试变得困难。

服务定义看起来像

public interface ISearchService {
    Task UploadContents(ISearchIndexClient indexClient, IReadOnlyCollection<dynamic> searchContents);
}

用一个简单的实现来包装外部依赖

public class SearchService : ISearchService  {
    private Task UploadContents(ISearchIndexClient indexClient, IReadOnlyCollection<dynamic> searchContents) 
        =>  indexClient.Documents.IndexAsync(IndexBatch.Upload(searchContents));
}

不要再为试图测试你无法控制的代码而感到压力了。而是专注于您可以控制的逻辑。

public async Task Write_Success() {
    //Arrange
    var serviceMock = new Mock<ISearchService>();
    serviceMock
        .Setup(_ => _.UploadContents(It.IsAny<ISearchIndexClient>(), It.IsAny<IReadOnlyCollection<It.AnyType>>())
        .ReturnsAsync(new object());

    var searchIndexClientMock = Mock.Of<ISearchIndexClient>();

    var pushFunction = new SearchIndexWriter(serviceMock.Object);

    Search search = new Search();

    //Act
    await pushFunction.Write(searchIndexClientMock.Object, search);

    //Assert, Verify checks
    //...
}

推荐阅读