使用 Moq 的 ElasticClient 的代码覆盖率

Code coverage for ElasticClient using Moq

我正在尝试通过单元测试分析代码覆盖率,我目前正在使用 Moq 库执行单元测试,不知何故我走错了路,想知道以下场景是否适用于使用 Moq

下面是一段代码

public interface ISearchWorker
{
    void DeleteIndex(string indexName);

    T GetClient<T>();
}

public class ElasticSearchWorker : ISearchWorker
{
    public void DeleteIndex(string indexName)
    {
        IElasticClient elasticClient = GetClient<IElasticClient>();

        if (elasticClient.IndexExists(indexName).Exists)
        {
            _ = elasticClient.DeleteIndex(indexName);
        }
    }

    public T GetClient<T>()
    {        
        string nodeList = "http://localhost:9200/";

        List<Node> nodes = nodeList.Split(',').Select(uri => new Node(new Uri(uri))).ToList();

        IConnectionPool sniffingConnectionPool = new SniffingConnectionPool(nodes);

        IConnectionSettingsValues connectionSettings = new ConnectionSettings(sniffingConnectionPool);

        return (T)(IElasticClient)new ElasticClient(connectionSettings);        
    }
}

下面是单元测试的代码片段



    [TestClass]
    public class SearchTestClass
    {
        private ISearchWorker searchWorker;

        private Mock<ISearchWorker> searchWorkerMoq;

        private readonly string indexName = "testIndex";

        [TestInitialize]
        public void SetupElasticClient()
        {
            searchWorkerMoq = new Mock<ISearchWorker>();

            var elasticClient = new Mock<IElasticClient>();

            searchWorkerMoq.Setup(c => c.GetClient<IElasticClient>()).Returns(elasticClient.Object).Verifiable();

            searchWorker = searchWorkerMoq.Object;
        }

        [TestMethod]
        public void DeleteIndexTest()
        {
            try
            {
                searchWorker.DeleteIndex(indexName);

                searchWorkerMoq.Verify(c => c.GetClient<IElasticClient>(), Times.Once()); 
            }
            catch (System.Exception)
            {
                throw;
            }
        }
    }


searchWorkerMoq.Verify(c => c.GetClient<IElasticClient>(), Times.Once());

抛出以下异常

(Moq.MockException: '
Expected invocation on the mock once, but was 0 times: c => c.GetClient<IElasticClient>())

通读大多数与 Moq 相关的信息,这似乎不是执行 Moq 测试的合适方法,IElasticClient 对象应该从外部提供给 ElasticSearchWorker class

不通过外部注入提供 IElasticClient 对象的原因是我们计划为另一个搜索提供程序(Azure 搜索)实现 ISearchWorker,因此希望将客户端实体包含在实现 ISearchWorker 的 class 中界面

想知道是否有更好的方法来执行此测试,以及我们如何实现此场景的代码覆盖率。

所以我假设您明白为什么这不起作用,您只是要求 "a good way" 解决这个问题。

现在我并不是说这是最好的方法,但它可能是最快的,同时仍然是 "clean"。

应用接口隔离("I" 来自 SOLID)。制作两个接口而不是一个接口,然后在稍后阶段实现它们。

// Don't have a C# IDE with me, so sorry if I leave some syntax errors. 
public interface ISearchClientProvider
{
    T GetClient<T>();
}

public interface ISearchWorker
{
    void DeleteIndex(string indexName);
}

public class ElasticSearchWorker : ISearchWorker{

    private readonly ISearchClientProvider _clientProvider;

    public ElasticSearchWorker(ISearchClientProvider clientProvider){
        _clientProvider = clientProvider;
    }

    public void DeleteIndex(string indexName)
    {
        var elasticClient = _clientProvider.GetClient<IElasticClient>();

        if (elasticClient.IndexExists(indexName).Exists)
        {
            _ = elasticClient.DeleteIndex(indexName);
        }
    }
}

public class ElasticSearchClientProvider : ISearchClientProvider{/*some implementation*/}
public class AzureSearchWorker : ISearchWorker{/*some implementation*/}
public class AzureSearchClientProvider : ISearchClientProvider{/*some implementation*/}

那么测试代码应该是这样的:

// would actually prefer to name it ElasticSearchWorkerTests
[TestClass]
    public class SearchTestClass
    {
        private readonly ElasticSearchWorker _searchWorker;
        private readonly ISearchClientProvider _elasticClientProvider;

        private readonly string indexName = "testIndex";

        // would prefer to name it SetupElasticSearchWorker
        [TestInitialize]
        public void SetupElasticClient()
        {
            var elasticClient = new Mock<IElasticClient>();
            // Setup for IElasticClient.IndexExists() function:
            // I don't know what is the return type of IndexExists, 
            // so I am assuming here that it is some dynamic Object
            elasticClient.Setup(c => c.IndexExists(indexName)).Returns(new {Exists = true});
            // Setup for IElasticCleint.DeleteIndex might also be necessary here.

            _elasticClientProvider = new Mock<ISearchClientProvider>();
            _elasticClientProvider.Setup(c => c.GetClient<IElasticClient>()).Returns(elasticClient.Object).Verifiable();

            _searchWorker = new ElasticSearchWorker(_elasticClientProvider);
        }

        // would prefer to name it DeleteIndexTest_GetsSearchClient, 
        // because the function does more than is checked here, e.g., Checks index, deletes index.
        [TestMethod]
        public void DeleteIndexTest()
        {
            try
            {
                searchWorker.DeleteIndex(indexName);

                searchWorkerMoq.Verify(c => c.GetClient<IElasticClient>(), Times.Once()); 
            }
            catch (System.Exception)
            {
                throw;
            }
        }
    }

这样本次测试就没有http请求了。

一般来说(有争议的部分)如果您希望您的代码更易于单元测试:

  • 使用Ports and Adapters(六边形,或者你想怎么称呼它)架构
  • 经验法则。不要在其他 public 方法中调用 public 方法。使用 composition 来解决这个问题。我在这个例子中使用了合成。
  • 经验法则。不要在 class 中创建具有行为的对象。让你的 composition root 创建它们并将它们作为依赖项注入到你的 class 中,一个接口的实现。如果无法在你的组合根中创建它们(也许你只在 运行 时间知道你需要什么样的对象),那么使用工厂模式。
  • 免责声明。您不需要 DI 容器即可使用组合根模式。你可以开始 with poor man's DI.
  • 总会有 class 不可单元测试的,因为来自第 3 方的某些依赖项将不可模拟。您的目标是使那些 classes very small 然后对它们进行一些集成测试。 (我相信,测试覆盖率工具也会使用)
  • 免责声明。我看到有人通过 HttpClientHandler 替换成功地模拟了 HttpClient。但我还没有看到成功的 EF db 上下文模拟 - 它们在某些时候都失败了。
  • 注意。人们普遍认为你应该 not mock interfaces you do not own。所以,从这个意义上说,我上面的解决方案也不干净,将来可能会给你带来麻烦。