Написание / реализация API: тестируемость против сокрытия информации - PullRequest
2 голосов
/ 17 февраля 2011

Часто я участвую в разработке / реализации API, с которыми я сталкиваюсь перед этой дилеммой.

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

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

Давайте посмотрим на конкретный пример:

public class Foo {
   SomeType attr1;
   SomeType attr2;
   SomeType attr3;

   public void someMethod() {
      // calculate x, y and z
      SomethingThatExpectsMyInterface something = ...;
      something.submit(new InnerFoo(x, y, z));
   }

   private class InnerFoo implements MyInterface {
      private final SomeType arg1;
      private final SomeType arg2;
      private final SomeType arg3;

      InnerFoo(SomeType arg1, SomeType arg2, SomeType arg3) {
         this.arg1 = arg1;
         this.arg2 = arg2;
         this.arg3 = arg3;
      }

      @Override
      private void methodOfMyInterface() {
         //has access to attr1, attr2, attr3, arg1, arg2, arg3
      }
   }
}

Существуют веские причины не раскрывать InnerFoo - ни один другой класс, библиотека не должна иметь к нему доступ, так как он не определяет публичный договор и автор намеренноне хотел, чтобы это было доступно.Однако, чтобы сделать его 100% TDD-кошерным и доступным без каких-либо трюков с отражением, InnerFoo следует изменить следующим образом:

private class OuterFoo implements MyInterface {
   private final SomeType arg1;
   private final SomeType arg2;
   private final SomeType arg3;
   private final SomeType attr1;
   private final SomeType attr2;
   private final SomeType attr3;

   OuterFoo(SomeType arg1, SomeType arg2, SomeType arg3, SomeType attr1, SomeType attr2, SomeType attr3) {
      this.arg1 = arg1;
      this.arg2 = arg2;
      this.arg3 = arg3;
      this.attr1 = attr1;
      this.attr2 = attr2;
      this.attr3 = attr3;
   }

   @Override
   private void methodOfMyInterface() {
      //can be unit tested without reflection magic
   }
}

В этом примере используется только 3 атрибута, но вполне разумно иметь 5-6, и конструктор OuterFoo должен будет принять 8-10 параметров!Добавьте методы getter сверху, и у вас уже есть 100 строк совершенно бесполезного кода (для получения этих атрибутов также потребуется получить эти атрибуты для тестирования).Да, я мог бы сделать ситуацию немного лучше, предоставив шаблон компоновщика, но я думаю, что это не только чрезмерное проектирование, но и наносит ущерб цели самого TDD!

Другим решением этой проблемы было бы разоблачениеЗащищенный метод для класса Foo, расширьте его до FooTest и получите необходимые данные.Опять же, я думаю, что это тоже плохой подход, потому что protected метод действительно определяет контракт и, выставляя его, я теперь неявно подписал его.

Не поймите меня неправильно. Мне нравится писать тестируемый код . Мне нравятся лаконичные, чистые API, блоки короткого кода, удобочитаемость и т. Д. Но мне не нравится, когда идет на жертвы, когда речь идет о сокрытии информации только потому, что легчемодульный тест .

Кто-нибудь может высказать какие-либо мысли по этому поводу (в целом, и в частности)?Есть ли другие, лучшие решения для данного примера?

Ответы [ 6 ]

4 голосов
/ 17 февраля 2011

Мой ответ на этот вопрос - «тестовый прокси». В вашем тестовом пакете выведите подкласс из тестируемой системы, который содержит «сквозные» средства доступа к защищенным данным.

Преимущества:

  • Вы можете напрямую протестировать или смоделировать методы, которые вы не хотите делать общедоступными.
  • Поскольку тестовый прокси находится в тестовом пакете, вы можете убедиться, что он никогда не используется в рабочем коде.
  • Тестовый прокси требует гораздо меньше изменений в коде, чтобы сделать его тестируемым, чем если бы вы тестировали класс напрямую.

Недостатки:

  • Класс должен быть наследуемым (нет final)
  • Любые скрытые участники, к которым вам нужен доступ, не могут быть приватными; защищенный - лучшее, что вы можете сделать.
  • Это не строго TDD; TDD подойдет для шаблонов, которые в первую очередь не требуют тестового прокси.
  • Это не строго даже модульное тестирование, потому что на каком-то уровне вы зависите от "интеграции" между прокси-сервером и фактическим SUT.

Короче, это обычно должно быть редкостью. Я склонен использовать его только для элементов пользовательского интерфейса, где рекомендуется (и поведение многих IDE по умолчанию) объявлять вложенные элементы управления пользовательским интерфейсом недоступными извне класса. Это определенно хорошая идея, поэтому вы можете контролировать, как вызывающие абоненты получают данные из пользовательского интерфейса, но это также затрудняет предоставление элементу управления некоторых известных значений для проверки этой логики.

3 голосов
/ 17 февраля 2011

Я думаю, вам следует пересмотреть вопрос об использовании отражения.

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

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

2 голосов
/ 17 февраля 2011

SomethingThatExpectsMyInterface можно проверить снаружи Foo, верно? Вы можете вызвать его submit() метод с вашим собственным тестовым классом, который реализует MyInterface. Так что об устройстве заботятся. Теперь вы тестируете Foo.someMethod() с этим хорошо протестированным модулем и вашим непроверенным внутренним классом. Это не идеально, но не так уж плохо. Когда вы тест-драйв someMethod(), вы неявно тест-драйв внутреннего класса. Я знаю, что это не чистый TDD, по некоторым строгим стандартам, но я считаю это достаточным. Вы не пишете строку внутреннего класса, кроме как для того, чтобы выполнить неудавшийся тест; что между тестом и тестируемым кодом существует один уровень косвенности, это не составляет большой проблемы.

2 голосов
/ 17 февраля 2011

Я не вижу, как сокрытие информации в абстрактной форме снижает вашу тестируемость.

Если вы вводите SomethingThatExpectsMyInterface, используемый в этом методе, а не создаете его напрямую:

public void someMethod() {
   // calculate x, y and z
   SomethingThatExpectsMyInterface something = ...;
   something.submit(new InnerFoo(x, y, z));
}

Тогда в модульном тесте вы могли бы внедрить этот класс с фиктивной версией SomethingThatExpectsMyInterface и легко утверждать, что происходит, когда вы вызываете someMethod() с разными входами - что mockSomething получает аргументы определенных значений.

Я думаю, что вы, возможно, слишком упростили этот пример, так как InnerFoo не может быть закрытым классом, если SomethingThatExpectsMyInterface получает аргументы своего типа.

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

0 голосов
/ 06 марта 2011

В вашем примере, похоже, классу Foo действительно нужен сотрудник InnerFoo.

По моему мнению, противоречие между сокрытием информации и проверяемостью решается с помощью девиза ", который проще, чем сумма его частей ".

Foo - это фасад надComposite (только InnerFoo в вашем случае, но не имеет значения.) Фасадный объект должен быть проверен на его предполагаемое поведение.Если вы чувствуете, что объектный код InnerFoo недостаточно управляется тестами на поведение Foo, вы должны рассмотреть, что InnerFoo представляет в вашем дизайне.Возможно, вам не хватает концепции дизайна.Когда вы найдете его, назовете его и определите его обязанности, вы можете проверить его поведение отдельно.

0 голосов
/ 17 февраля 2011

мне не нравится приносить жертвы, когда дело доходит до сокрытия информации

Во-первых, работать с Python в течение нескольких лет.

privateне особенно полезно.Это затрудняет расширение класса и затрудняет тестирование.

Подумайте над тем, чтобы переосмыслить свою позицию в отношении "сокрытия".

...