Как бороться со статическими служебными классами при разработке для тестируемости


63

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

В разных местах нам необходимо использовать статические вспомогательные методы, такие как ImageIO и URLEncoder (оба являются стандартными Java API) и различные другие библиотеки, которые состоят в основном из статических методов (например, библиотеки Apache Commons). Но крайне сложно протестировать те методы, которые используют такие статические вспомогательные классы.

У меня есть несколько идей для решения этой проблемы:

  1. Используйте фиктивный фреймворк, который может имитировать статические классы (например, PowerMock). Это может быть самое простое решение, но почему-то хочется сдаться.
  2. Создайте инстанцируемые классы-оболочки вокруг всех этих статических утилит, чтобы их можно было внедрить в классы, которые их используют. Это звучит как относительно чистое решение, но я боюсь, что в итоге мы создадим очень много этих классов-обёрток.
  3. Извлеките каждый вызов этих статических вспомогательных классов в функцию, которую можно переопределить, и протестируйте подкласс класса, который я на самом деле хочу протестировать.

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

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


Я не уверен, что вы подразумеваете под «достоверными и / или официальными источниками», но я согласен с тем, что @berecursive написал в своем ответе. PowerMock существует по какой-то причине, и не стоит отказываться от него, особенно если вы не хотите сами писать классы-оболочки. Финальные и статические методы являются проблемой, когда дело доходит до юнит-тестирования (и TDD). Лично? Я использую метод 2, который вы описали.
Деку

«Достоверные и / или официальные источники» - это только один из вариантов, которые вы можете выбрать, когда начнете награду за вопрос. Что я на самом деле имею в виду: опыт или ссылки на статьи, написанные экспертами TDD. Или любой другой опыт, который кто-то столкнулся с той же проблемой ...

Ответы:


34

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

Когда эти статические методы представляют подлинные зависимости, создайте оболочки. Так что для таких вещей, как:

  • ImageIO
  • HTTP-клиенты (или что-либо еще, связанное с сетью)
  • Файловая система
  • Получение текущего времени (мой любимый пример, где помогает внедрение зависимостей)

... имеет смысл создать интерфейс.

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

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


20

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

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

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


1
Еще одна проблема, в которой я не совсем уверен: при реализации обёрток вы реализуете все методы класса, который обернут, или только те, которые нужны в данный момент?

3
Следуя проворным идеям, вы должны делать простейшую вещь, которая работает, и избегать работы, которая вам не нужна. Поэтому вы должны выставлять только те методы, которые вам действительно нужны.
Ассаф Стоун

@AssafStone согласился

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

Вам действительно нужно много писать обертку, если вы сочетаете тестирование / миграцию с использованием библиотеки DI / IoC?

4

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

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

Это означает, что нам не нужно писать оболочку для простой утилиты, такой как Apache Commons StringEscapeUtils(потому что строки, которые им нужны, могут быть легко предоставлены), и мы не используем mock для статических методов (если мы думаем, что нам может понадобиться, пришло время написать класс обертки, а затем макет экземпляра обертки).


2

Я бы протестировал эти классы, используя Groovy . Groovy просто добавить в любой проект Java. С его помощью вы можете легко смоделировать статические методы. См. Mocking Static Methods with Groovy для примера.


1

Я работаю в крупной страховой компании, и наш исходный код занимает до 400 МБ чистых java-файлов. Мы разрабатывали все приложение, не задумываясь о TDD. С января этого года мы начали тестирование Junit для каждого отдельного компонента.

Лучшим решением в нашем отделе было использование объектов Mock в некоторых методах JNI, которые зависели от системы (написаны на C), и поэтому вы не могли точно оценить результаты каждый раз в каждой ОС. У нас не было другого выбора, кроме как использовать поддельные классы и конкретные реализации методов JNI специально для тестирования каждого отдельного модуля приложения для каждой поддерживаемой нами ОС.

Но это было действительно быстро и до сих пор работало довольно хорошо. Я рекомендую это - http://www.easymock.org/


1

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

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


1

Иногда я использую вариант 4

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

Что-то вроде этого.

public class DateUtil {
    public interface ITimestampGenerator {
        long getUtcNow();
    }

    class ConcreteTimestampGenerator implements ITimestampGenerator {
        public long getUtcNow() { return System.currentTimeMillis(); }
    }

    private static ITimestampGenerator timestampGenerator;

    static {
        timestampGenerator = new ConcreteTimeStampGenerator;
    }

    public static DateTime utcNow() {
        return new DateTime(timestampGenerator.getUtcNow(), DateTimeZone.UTC);
    }

    public static void setTimestampGenerator(ITimestampGenerator t) {...}

    // plus other util routines, which may or may not use the timestamp generator 
}

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

Math.sum(17, 29, 42);
// vs
new Math().sum(17, 29, 42);
Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.