使用Moq的ElasticClient的代码覆盖率

时间:2019-06-07 06:37:20

标签: c# unit-testing elasticsearch moq code-coverage

我正在尝试通过单元测试来分析代码覆盖率,我目前正在使用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类

不通过外部注入提供IElasticClient对象的原因是,我们计划为另一个搜索提供程序(Azure Search)实现ISearchWorker,因此希望将客户端实体保留在实现ISearchWorker接口的类之内

想知道是否有更好的方法来执行此测试,以及在这种情况下我们将如何实现代码覆盖。

1 个答案:

答案 0 :(得分:1)

所以我认为您了解为什么这不起作用,而您只是在寻求“好的方法”来解决此问题。

现在我并不是说这是最好的方法,但是在保持“干净”的同时,这可能是最快的方法。

应用接口隔离(来自SOLID的“ I”)。创建两个接口,而不是一个,然后在以后的阶段将它们都实现。

// 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(六角形,或者您想称呼它)体系结构
  • 经验法则。不要在其他公共方法内部调用公共方法。使用composition修复此问题。在此示例中,我使用了合成。
  • 经验法则。不要在类内部创建具有行为的对象。让您的composition root创建它们并将它们作为依赖项(接口的实现)注入到您的类中。如果无法在合成根目录中创建它们(也许您只在运行时知道需要哪种对象),则使用工厂模式。
  • 免责声明。您不需要DI容器即可使用合成根模式。您可以启动with poor man's DI
  • 总是会有一些不可单元测试的类,因为来自第三方的某些依赖项是不可模拟的。您的目标是制作这些类very small,然后对其进行一些集成测试。 (我相信测试覆盖率工具也将使用)
  • 免责声明。我见过人们通过HttpClientHandler替代成功地嘲笑HttpClient。但是我还没有看到成功的EF db上下文模拟-它们都在某个时刻失败。
  • 注意。人们普遍认为您应该not mock interfaces you do not own。因此,从这个意义上讲,我上面的解决方案也不是很干净,并且可能在将来的某个时候让您遇到麻烦。