Покрытие кода для ElasticClient с использованием Moq - PullRequest
1 голос
/ 07 июня 2019

Я пытаюсь проанализировать покрытие кода с помощью модульного тестирования, в настоящее время я использую библиотеку 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 из внешнего внедрения, заключается в том, что мы планируем реализовать ISearchWorker для другого поставщика поиска (поиск Azure), поэтому мы хотели бы сохранить сущность клиента в пределах класса, реализующего интерфейс ISearchWorker

Хотелось бы узнать, есть ли лучший способ выполнить этот тест, а также как нам достичь покрытия кода для этого сценария.

1 Ответ

1 голос
/ 07 июня 2019

Итак, я предполагаю, что вы понимаете, почему это не работает, и вы просто просите "хороший способ" исправить это.

Теперь я не говорю, что это лучший способ сделать это, но этовероятно, самый быстрый, но при этом остается «чистым».

Применить разделение интерфейса («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 не будутв этом тесте.

В общем случае (если вы хотите, чтобы ваш код был более тестируемым модулем):

  • Используйте Порты и адаптеры (Шестиугольник)или как вы хотите это назвать) архитектура
  • Правило большого пальца.Не вызывайте открытые методы внутри других открытых методов.Используйте состав , чтобы исправить это.Я использовал композицию в этом примере.
  • Правило большого пальца.Не создавайте свои объекты, которые имеют поведение, внутри вашего класса.Позвольте вашему составному корню создать их и внедрить их в свой класс как зависимость, реализацию интерфейса.Если невозможно создать их в корне композиции (может быть, вы знаете только во время выполнения, какой тип объекта вам нужен), используйте фабричный шаблон.
  • Отказ от ответственности.Вам не нужен контейнер DI, чтобы использовать корневой шаблон композиции.Вы можете начать с DI бедного человека.
  • Всегда будут классы, которые не могут быть проверены юнитами, потому что некоторые зависимости от сторонних разработчиков не будут поддельными.Ваша цель - сделать эти классы очень маленькими и затем провести с ними интеграционное тестирование.(которые, как я полагаю, также подберут инструменты тестирования покрытия)
  • Отказ от ответственности.Я видел, как люди успешно высмеивали HttpClient через HttpClientHandler.Но мне еще предстоит увидеть успешный макет контекста EF db - все они в какой-то момент терпят неудачу.
  • Примечание.Существует распространенное мнение, что вы должны не имитировать интерфейсы, которыми вы не владеете .Таким образом, в этом смысле мое решение, приведенное выше, также не является чистым и может привести к неприятностям в будущем.
...