Интерфейсы и юнит-тесты - всегда тестирование белого ящика? - PullRequest
4 голосов
/ 13 сентября 2009

Я наконец-то вспомнил, что беспокоило меня по поводу инъекций зависимостей и подобных методов, которые должны облегчить юнит-тесты. Давайте возьмем этот пример:

public interface IRepository { void Item Find(); a lot of other methods here; }
[Test]
public void Test()
{
  var repository = Mock<IRepository>();
  repository.Expect(x => x.Find());
  var service = new Service(repository);
  service.ProcessWithItem();
}

Теперь, что не так с кодом выше? Дело в том, что наш тест примерно заглядывает в реализацию ProcessWithItem (). Что если он захочет сделать «из x в GetAll (), где x ...» - но нет, наш тест знает, что там произойдет. И это просто простой пример. Представление нескольких вызовов, с которыми теперь связан наш тест, и когда мы хотим перейти от метода GetAll () к лучшему GetAllFastWithoutStuff () внутри метода ... наши тесты не работают Пожалуйста, измените их. Много дерьмовой работы, которая случается так часто без реальной необходимости.

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

Конечно, речь идет не только об интерфейсе (или DI). POCO (и POJO, почему бы и нет) также страдают от того же, но теперь они связаны с данными, а не с интерфейсом. Но принцип тот же - наше окончательное утверждение тесно связано с нашим знанием того, что собирается делать наше SUT. «Да, вы ДОЛЖНЫ предоставить это поле, сэр, и лучше иметь это значение».

Как следствие, тесты не пройдут - скоро и часто. Это боль И проблема.

Есть ли какие-либо методы, чтобы справиться с этим? AutoMockingContainer (который в основном заботится обо всех ВСЕХ методах и вложенных иерархиях DI) выглядит многообещающе, но со своим собственным недостатком. Что-нибудь еще?

Ответы [ 5 ]

3 голосов
/ 13 сентября 2009

Внедрение зависимостей само по себе позволит вам внедрить реализацию IRepository, которая принимает любые вызовы, выполненные на нем, проверяет, удовлетворены ли инварианты и предварительные условия, и возвращает результаты, удовлетворяющие постусловиям. Когда вы решите внедрить фиктивный объект, который имеет очень конкретные ожидания относительно того, какие методы будут вызываться, тогда да, вы проводите тестирование с высокой степенью зависимости от реализации, но Dependency Injection совершенно невиновно в этом вопросе, так как оно никогда не диктует, ЧТО вам нужно. следует вводить; скорее, ваша говядина, по-видимому, связана с Mocking - на самом деле, в частности, с несколько автоматизированным подходом Mocking, который вы выбрали, который основан на очень специфических ожиданиях.

Насмешка с очень конкретными ожиданиями действительно полезна только для тестирования белого ящика. В зависимости от используемых инструментов / платформ / библиотек (и вы даже не указываете точный язык программирования в теге, поэтому я предполагаю, что ваш вопрос полностью открыт), вы можете указать разрешенные степени свободы ( эти вызовы могут поступать в любых порядках, эти аргументы должны удовлетворять только следующим предварительным условиям и т. д. и т. д.). Тем не менее, я не знаю автоматизированного инструмента, который бы выполнял именно то, что вам нужно для непрозрачного тестирования, которое представляет собой «универсальную, толерантную реализацию интерфейса yonder со всеми необходимыми проверками« программирования по контракту », и нет». другой».

То, что я склонен делать в течение жизни проекта, - это создать библиотеку «не совсем издевательств» для основных необходимых интерфейсов. В некоторых случаях это может быть несколько очевидно с самого начала, но в других случаях они появляются постепенно, так как я рассматриваю некоторые основные рефакторинг, как показано ниже (типичный сценарий) ...:

На ранних этапах рефакторинга нарушаются некоторые аспекты хрупкой насмешки над сильными ожиданиями, которую я изначально дешево применил, я размышляю, стоит ли просто подстроить ожидания или пойти на все, если я решу, что это не разовое (т. е. отдача от будущих рефакторингов и тестов оправдает инвестиции), тогда я вручную пишу хороший «не совсем издевательский» материал и прячу его в специальном пакете трюков проекта, который часто можно использовать в разных проектах; такие классы / пакеты, как MockFilesystem, MockBigtable, MockDom, MockHttpClient, MockHttpServer и т. д., и т. д., попадают в независимый от проекта репозиторий и повторно используются для тестирования всех видов будущих проектов (и фактически могут использоваться совместно с другими командами в компании, если несколько команд используют интерфейсы файловой системы, интерфейсы больших таблиц, DOM, http-клиент-серверные интерфейсы и т. д. и т. д., которые одинаковы для всех групп).

Я признаю, что использование слова «mock» может быть немного неуместным, если вы берете «mock», чтобы ссылаться конкретно на стиль точного ожидания «поддельной реализации для целей тестирования» интерфейсов. Может быть, Stub, Shim, Fake, Test или какой-то другой префикс все же может быть предпочтительным (я склонен использовать Mock по историческим причинам, за исключением случаев, когда я специально называю его Fake или подобным; -).

Если бы я использовал языки с ясным и точным способом выражения в самом языке различных спецификаций проектирования по контракту в интерфейсе, я думаю, что я бы получил автоматическую поддержку инструментов для большей части этого подделка / прокладки / и т.д.; однако я в основном пишу на других языках, поэтому здесь мне нужно немного больше ручного труда. Но я думаю, что это отдельная проблема.

1 голос
/ 14 сентября 2009

Как следствие, тесты терпеть неудачу - скоро и часто. Это боль И проблема.

Ну да, юнит-тесты могут зависеть от внутренних деталей реализации. И конечно, такие тесты «белого ящика» являются более хрупкими, чем тесты «черного ящика», которые основаны только на опубликованном извне контракте.

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

Есть ли какие-либо методы, чтобы иметь дело с это?

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

На практике вы должны быть прагматичными; время от времени вы будете писать «модульные тесты», которые на самом деле являются интеграционными тестами, включающими несколько классов или классов больших размеров. Хрупкие тесты в зависимости от внутренних деталей реализации более опасны в этом случае. Но для действительно TDD-стиля классы, не так много.

1 голос
/ 14 сентября 2009

читаю отличную книгу http://www.manning.com/rainsberger/. Я хотел бы дать некоторое представление, которое я получил от этого. Я полагаю, что несколько советов могут помочь вам уменьшить связь между вашими тестами и вашей реализацией.

Отредактировано : в эту связь включен тест, утверждающий, что тестируемый код вызывает некоторые методы. Вызов какого-либо метода никогда не является функциональной необходимостью, это проблема реализации. Он относится к интерфейсу, отличному от того, который тестируется.

  1. Во многих случаях тестирование должно быть посвящено внешнему поведению интерфейса и должно выполняться полностью в «черном ящике».

    Автор приводит пример того, что тестовые классы должны быть в другом пакете, чем класс для тестирования. Сначала я был уверен, что это неправильно, потому что это затрудняет тестирование защищенных и пакетных методов. Но он утверждает, что вам следует проверять только внешнее поведение системы, то есть общедоступные методы. Непубличные методы являются деталями реализации, и их тестирование приводит к соединению теста с реализацией . Это было очень проницательно для меня.

    Кстати, в этой книге так много отличных практических советов о том, как разрабатывать тесты (скажем, тесты JUnit), что я бы купил их на свои собственные деньги, если бы они не были предоставлены компанией! ; -)

  2. Отличным советом из книги было тестирование на уровне функциональности, а не на уровне метода. Например, тестирование метода add () для списка требует доверенных методов size () и get (), но они, в свою очередь, требуют add (), поэтому у нас есть цикл, который мы не можем протестировать безопасно. Но тестирование поведения списка глобально (по всем методам) при добавлении включает в себя тестирование трех методов одновременно, не доказывая, что каждый из них является правильным в отдельности, но проверяя, что вместе они обеспечивают ожидаемое поведение. Часто, когда вы пытаетесь протестировать один из ваших методов изолированно, вы не можете написать разумный тест, не используя другие методы, поэтому вместо этого вы заканчиваете тестированием реализации; следствием является связь между тестом и реализацией .
    Только функциональные возможности тестирования, а не методы .

  3. Также обратите внимание, что тестирование с использованием внешних ресурсов (база данных является наиболее распространенной, но существует много других) намного медленнее, требует некоторого доступа (IP, лицензии и т. Д.) С исполняющей машины, требует запуска контейнера, может быть чувствителен к одновременному доступу (база данных не может работать одновременно с несколькими кампаниями JUnit одновременно) и имеет много других недостатков. Если все ваши тесты используют внешние ресурсы, то у вас проблемы, вы не можете запускать все свои тесты все время, с любой машины, со многих машин одновременно и т. Д. Итак, я понял (все еще из книги):

    Тестирование только один раз для каждого внешнего ресурса (например, базы данных) в специальном тесте, который не является модульным тестом, а интеграционным тестом (хотя он все еще может использовать та же технология JUnit, если применимо).

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

    Обратите внимание, что нынешние лучшие практики Maven дают аналогичные рекомендации (см. Бесплатную книгу "Лучшие сборки с Maven"). Я считаю, что это не совпадение:

    • JUnits в каталоге test проекта являются реальными модульными тестами. Они запускаются каждый раз, когда вы что-то делаете со своим проектом (за исключением только компиляции).
    • Интеграционные и функциональные тесты должны предоставляться в другом проекте, интеграционном тестовом проекте. Они запускаются только в гораздо более поздней (необязательной) фазе после того, как вы развернули все приложение в контейнере.
0 голосов
/ 13 сентября 2009

Да, это одна из самых больших проблем с юнит-тестированием. Это и рефакторинг. И изменения дизайна, которые регулярно происходят с Agile. И неопытность тех, кто создает тесты. И тд и тп ...

Я думаю, что единственное, что может сделать рядовой разработчик некритических систем, - это мудро выбирать битвы. На ранних этапах разработки определите действительно важные пути и протестируйте их. Взвесьте вероятность изменения этого кода, прежде чем тратить много времени на его тестирование.

Если кто-нибудь все выяснит, пожалуйста, сообщите нам.

0 голосов
/ 13 сентября 2009

Помните, что когда вы пишете тест, вы не тестируете свой репозиторий, вы тестируете свой класс обслуживания. В этом конкретном примере метод ProcessWithItem. Вы создаете свои ожидания для объекта хранилища. Кстати, вы забыли указать ожидаемый доход для вашего метода x.Find. В этом прелесть DI, что вы изолируете все от кода, который собираетесь написать (я полагаю, вы делаете TDD).

Если честно, я не могу относиться к проблеме, которую вы описываете.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...