Написание тестируемого кода против предотвращения спекулятивной общности

11

Этим утром я читал несколько постов в блоге и наткнулся на это :

Если единственным классом, который когда-либо реализует интерфейс Customer, является CustomerImpl, у вас нет полиморфизма и заменяемости, потому что на практике нет ничего, что можно заменить во время выполнения. Это поддельная общность.

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

Но, если я следую за TDD, я не могу (легко) создавать тестовые дубликаты без этой спекулятивной общности, будь то в форме реализации интерфейса или нашей другой полиморфной опции, делающей класс наследуемым и его методы виртуальными.

Итак, как нам согласовать этот компромисс? Стоит ли быть умозрительно общим, чтобы облегчить тестирование / TDD? Если вы используете тестовые двойники, они считаются вторыми реализациями и, таким образом, делают общность больше не спекулятивной? Стоит ли рассматривать более тяжелую среду для имитации, которая позволяет высмеивать конкретных сотрудников (например, Moles против Moq в мире C #)? Или вы должны тестировать с конкретными классами и писать то, что можно считать «интеграционными» тестами, пока ваш дизайн, естественно, не требует полиморфизма?

Мне интересно читать мнения других людей по этому вопросу - заранее спасибо за ваше мнение.

Эрик Дитрих
источник
Лично я думаю, что сущности не должны насмехаться. Я только симулирую сервисы, и им в любом случае нужен интерфейс, потому что код области кода обычно не имеет ссылки на код, в котором реализованы сервисы.
CodesInChaos
7
Мы, пользователи динамически типизированных языков, смеемся над тем, как вы наталкиваетесь на цепочки, которые навязали вам статически типизированные языки. Это одна вещь, которая облегчает модульное тестирование в динамически типизированных языках, мне не нужно было разрабатывать интерфейс для замены объекта в целях тестирования.
Уинстон Эверт
Интерфейсы используются не только для обеспечения общности. Они используются для многих целей, и одним из важных является разделение вашего кода. Что, в свою очередь, значительно облегчает тестирование вашего кода.
Марьян Венема
@WinstonEwert Это интересное преимущество динамической типизации, которое я раньше не рассматривал как человека, который, как вы указываете, обычно не работает на языках с динамической типизацией.
Эрик Дитрих
@CodeInChaos Я не рассматривал различие для целей этого вопроса, хотя это разумное различие. В этом случае мы можем подумать о классах службы / каркаса только с одной (текущей) реализацией. Допустим, у меня есть база данных, к которой я обращаюсь с помощью DAO. Должен ли я не взаимодействовать с DAO, пока у меня не будет вторичной модели персистентности? (Это похоже на то, что подразумевает автор поста в блоге)
Эрик Дитрих

Ответы:

14

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

Даниэль Манн
источник
3
Совершенно верно. Тестирование кода является частью приложения, потому что оно необходимо для правильного проектирования, реализации, обслуживания и т. Д. Тот факт, что вы не отправляете его клиенту, не имеет значения - если в вашем наборе тестов есть вторая реализация, то есть общность, и вы должны ее учесть.
Килиан Фот
1
Это ответ, который я нахожу наиболее убедительным (и @KilianFoth добавляет, что, независимо от того, будет ли в коде реализована вторая реализация, все еще существует). Я немного задержусь, чтобы принять ответ, чтобы посмотреть, не вмешивается ли кто-то еще.
Эрик Дитрих
Я также добавил бы, что когда вы зависите от интерфейсов в тестах, общность больше не является умозрительной
Пит
«Не делать этого» (используя интерфейсы) автоматически не приводит к тому, что ваши классы тесно связаны. Это просто не так. Например, в .NET Framework есть Streamкласс, но нет тесной связи.
Люк Пуплетт
3

Тестирование кода в целом не так просто. Если бы это было так, мы бы делали это все давным-давно, и не делали бы ничего подобного только в последние 10-15 лет. Одна из самых больших трудностей всегда заключалась в определении того, как тестировать код, который был написан слаженно, хорошо продуманно и тестируемо, не нарушая инкапсуляцию. Принцип BDD предполагает, что мы почти полностью фокусируемся на поведении, и в некотором смысле, кажется, говорит о том, что вам действительно не нужно беспокоиться о внутренних деталях в такой большой степени, но это часто может усложнить тестирование, если есть множество частных методов, которые делают «вещи» очень скрытно, так как это может увеличить общую сложность вашего теста, чтобы иметь дело со всеми возможными результатами на более открытом уровне.

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

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

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

Там, где я нахожу «непроверяемый» код, я смотрю, смогу ли я сделать рефакторинг, чтобы сделать его более тестируемым. Там, где у вас есть много закрытого кода, выполняющего скрытые закулисные вещи, часто вы обнаружите новые классы, ожидающие выхода из строя. Эти классы могут использоваться для внутренних целей, но часто могут тестироваться независимо с менее частным поведением, а зачастую и с меньшим количеством уровней доступа и сложности. Однако одну вещь, которую я стараюсь избегать, - это написание производственного кода со встроенным тестовым кодом. Может возникнуть соблазн создать « тестовые бобышки », которые приводят к таким ужасам if testing then ..., которые указывают на то, что проблема тестирования не полностью деконструирована и не полностью решена.

Возможно, вам будет полезно прочитать книгу Джерарда Месароса « Тестовые шаблоны xUnit» , которая охватывает все эти вещи гораздо более подробно, чем я могу здесь рассказать. Я, вероятно, не делаю все, что он предлагает, но это помогает прояснить некоторые из более сложных тестовых ситуаций, с которыми приходится иметь дело. В конце концов, вы хотите быть в состоянии удовлетворить ваши требования к тестированию, в то же время применяя ваши предпочтительные проекты, и это помогает лучше понять все варианты, чтобы лучше решить, где вам может потребоваться компромисс.

S.Robins
источник
1

Есть ли у языка, который вы используете, способ «насмешить» объект для тестирования? Если так, то эти надоедливые интерфейсы могут исчезнуть.

С другой стороны, могут быть причины иметь SimpleInterface и единственный ComplexThing, который его реализует. Могут быть части ComplexThing, которые вы не хотите, чтобы они были доступны пользователю SimpleInterface. Это не всегда из-за чрезмерного избыточного кодирования.

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


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

Я отвечу в двух частях:

  1. Вам не нужны интерфейсы, если вы заинтересованы только в тестировании. Для этой цели я использую фреймворки (в Java: Mockito или easymock). Я считаю, что код, который вы разрабатываете, не должен быть изменен для целей тестирования. Написание тестируемого кода эквивалентно написанию модульного кода, поэтому я склонен писать модульный (тестируемый) код и тестировать только открытые интерфейсы кода.

  2. Я работал в большом проекте Java , и я становлюсь глубоко убежден , что с помощью чтения (только геттеры) интерфейсов является путь (пожалуйста , обратите внимание , что я большой поклонник неизменности). Реализующий класс может иметь сеттеры, но это детали реализации, которые не должны быть представлены внешним слоям. С другой стороны, я предпочитаю композицию, а не наследование (модульность, помните?), Поэтому здесь также помогают интерфейсы. Я готов заплатить за спекулятивную генеральность, чем застрелить себя в ногу.

Джил Брандао
источник
0

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

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

Многие люди согласятся с тем, что больше, меньшие классы лучше, чем меньше, большие классы. Вам не нужно концентрироваться на одном и том же моменте, и у каждого класса есть четко определенная цель. Другие могут сказать, что вы увеличиваете сложность, имея больше классов.

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

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

Деспертар
источник