Является ли проверка типа isInUnitTest() антипаттерном?

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

Кодовая база моей компании усеяна таким кодом:

public Response chargeCard(CreditCard card, Money amount) {
  if(Config.isInUnitTests()) {
      throw new IllegalStateException("Cannot make credit card API calls in unit tests!");
  }
  return CreditCardPOS.connect().charge(card, amount);
}

Целью здесь является активное предотвращение выполнения опасного кода / кода с внешними зависимостями во время тестирования. Мне нравится понятие быстрого провала, если модульные тесты делают что-то плохое, но мне не нравится эта реализация по нескольким причинам:

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

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

Используя приведенный выше пример кредитной карты, я могу избежать isInUnitTests() проверить каждую зарядку, правильно завернув это в макете CardCharger класс, или что-то подобное, но, хотя я чище, я чувствую, что я поднял проблему только на один уровень - как мне предотвратить модульный тест для создания реального экземпляра CardCharger без проверки в методе конструктора / фабрики, который его создает?

  • Является isInUnitTests() запах кода?
  • Если так, как я могу по-прежнему обеспечивать, чтобы модульные тесты не достигали внешних зависимостей?
  • Если нет, каков наилучший способ реализации такого метода, и какова хорошая практика, когда его использовать / избегать?

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

6 ответов

Решение

Является ли isInUnitTests() запахом кода?

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

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

Ваши тесты должны проверять только единицу кода и создавать макеты или заглушки для внешних зависимостей.

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

РЕДАКТИРОВАТЬ

как предотвратить модульный тест от создания реального экземпляра HTTPRequest без установки проверки в методе конструктора / фабрики, который его создает

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

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

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

В любом случае, я понимаю вашу точку зрения и позволяю рассказать вам о похожей ситуации, с которой я столкнулся.

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

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

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

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

Итак, вкратце:

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

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

Есть ли способ контролировать путь к классам, используемый для модульных тестов, чтобы сделать библиотеки, необходимые для доступа к внешним ресурсам, недоступными? Если на пути к классам нет JSoup и драйвера JDBC, тесты на код, который пытается их использовать, не пройдут. Вы не сможете исключить классы уровня JDK, такие как Socket а также URLConnection таким образом, но это все еще может быть полезным.

Если бы вы проводили тесты с использованием Gradle, это было бы довольно просто. Если вы используете Maven, возможно, нет. Я не думаю, что есть какой-либо способ иметь разные пути к классам для компиляции и тестирования в Eclipse.

Обновить

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


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

Вы можете избежать сохранения внешнего состояния о том, находитесь ли вы в модульном тесте или нет, непосредственно изучив стек выполнения, например, так:

/**
 * Determines at runtime and caches per-thread if we're in unit tests.
 */
public static class IsInUnitTest extends ThreadLocal<Boolean> {
    private static final ImmutableSet<String> TEST_PACKAGES = 
                                       ImmutableSet.of("org.testng");

    @Override
    protected Boolean initialValue() {
        for(StackTraceElement ste : Thread.currentThread().getStackTrace()) {
            for(String pkg : TEST_PACKAGES) {
                if(ste.getClassName().startsWith(pkg)) {
                    return true;
                }
            }
        }
        return false;
    }
}

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

Любопытно, что другие думают об этой идее.

Ну, вы можете добиться того же чистого пути, используя Abstract Factory Pattern хотя (может быть, не подходит, чтобы назвать это Abstract Factory Pattern).

Пример в C#:

public class CardChargerWrapper{
    public CardChargerWrapper(
        NullCardCharger nullCharger
        , TestUnitConfig config){
        // assign local value
        this.charger = new CardCharger();
    }
    CardCharger charger;
    NullCardCharger nullCharger;
    TestUnitConfig config;

    public Response Charge(CreditCard card, Money amount){
        if(!config.IsUnitTest()) { return charger.connect().charge(card, amount); }
        else { return NullCardCharger.charge(card, amount); }
    }
}

РЕДАКТИРОВАТЬ: Изменен Card ChargerWrapper для использования жестко закодированного экземпляра CardCharger вместо того, чтобы вводить это.

Примечание: вы можете изменить NullCardCharger что-то вроде MockCardCharger или же OfflineCardCharger для целей регистрации.

Примечание еще раз: вы можете изменить CardChargerWrapperконструктор, чтобы соответствовать. Пример, вместо конструктора, внедряющего NullCardChargerВы можете сделать это свойство введено. То же самое с TestUnitConfig,

РЕДАКТИРОВАТЬ: Относительно вызова IsUnitTest() хорошая идея или нет

Это действительно зависит от вашей бизнес-перспективы и от того, как вы проводите тестирование. Как говорили многие люди, коду, который еще не был проверен, не доверяют за его правильность. Это не может быть надежным. На заметку, я предпочитаю IsChargeRealCard() будет более подходящим, чем IsUnitTest(),

Скажи, что мы вынимаем unit test в нашем контексте, по крайней мере, вам все равно нужно будет проводить интеграционное тестирование в тестовой среде. Вы, вероятно, хотите проверить что-то вроде:

  1. Я хочу проверить валидацию кредитной карты (реальная она или нет и т. Д.).
  2. Я хочу проверить способ оплаты и посмотреть, будет ли карта снята. Как процесс, но не как реальная оплата кредитной картой.

Что касается второго пункта, я думаю, что лучше всего сделать макет credit card charger зарегистрировать транзакцию. Это для того, чтобы зарядка была правильной. И это будет проводиться как на тестовом, так и на dev сервере.

Итак, как можно CardChargerWrapper помочь в такой ситуации?

Теперь с Card ChargerWrapper вы можете:

  1. Переключите NullCardCharger на любые макетные зарядные устройства для улучшения вашего юнит-тестирования.
  2. Все классы используют CardChargerWrapper можно убедиться, что они проверяют IsUnitTest сначала, прежде чем заряжать реальную карту.
  3. Ваш разработчик должен использовать CardChargerWrapper вместо CardCharger, предотвращая ошибки разработки для юнит-тестов.
  4. Во время проверки кода вы можете найти CardCharger используется в другом классе вместо CardChargerWrapper, Это должно гарантировать отсутствие утечки кода.
  5. Я не уверен, но кажется, что вы можете скрыть ссылки вашего основного проекта на ваш реальный CardCharger, Это защитит ваш код в дальнейшем.

Если [isInUnitTest() такое антипаттерн], как я могу по-прежнему обеспечивать, чтобы модульные тесты не доходили до внешних зависимостей?

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

Например, у меня есть утилита подключения Http:

public class HttpOp {
  public HttpOp(Config conf) {
    if(!conf.isHttpAllowed()) {
      throw new UnsupportedOperationException("Cannot execute HTTP requests in "+
          getClass()+" if HTTP is disabled.  Did you mean to mock this class?");
    }
  }
  ....
}

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

@Test
public void connect() {
  State.Http httpState = mock(State.Http.class);
  when(httpState.isEnabled()).thenReturn(true);
  RemoteDataProcessor rdp = new RemoteDataProcessor(new HttpOp(httpState));
  ...

}

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

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

Из четырех предложенных ими методов рекомендуется использовать метод внедрения зависимостей, чтобы поток производственного кода совпадал с потоком тестирования. Разница лишь в том, что объекты, которые вы передаете в свой код, будут другими. Обратите внимание, что Клаудио использовал термин "заглушка" для обозначения объектов, которые вы передаете в метод, чтобы действовать в качестве заполнителей для тех, которые вы будете использовать в производстве:

public Document getPage(String url, AbstractJsoup jsoup) {
  return jsoup.connect(url).get();
}

Моя Java немного устарела, поэтому учтите, что вам может потребоваться сделать что-то напуганное, например, проверить тип объекта jsoup или привести его к тестовому объекту Jsoup или к производственному объекту Jsoup, который может просто победить вся цель использования внедрения зависимости. Таким образом, если вы говорите о динамическом языке, таком как JavaScript, или о каком-то другом типе функционального языка программирования со слабой типизацией, внедрение зависимостей будет действительно чистым.

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

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

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