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

Много раз, когда я участвую в разработке / реализации 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 ответов

Решение

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

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

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

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

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

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

Недостатки:

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Другие вопросы по тегам