Фото Луис Рид на Unsplash
POJOs (Простые Старые Объекты Java) и подобные им, такие как объекты значений и JavaBeans |/, занимают уникальное место в современных приложениях Java. У них есть сотни немного отличающихся вариантов использования. Они часто сериализуются и десериализуются в JSON, XML или SQL. Они образуют мосты между слоями в сложных приложениях. Они представляют язык, специфичный для конкретной предметной области.
И они часто остаются непроверенными.
Аргумент против их тестирования обычно основан на предположении, что POJO имеют состояние, но не ведут себя. POJO обычно представляет собой просто пакет данных с геттерами, а иногда и сеттерами. Если вы когда—либо видели модульный тест для типичного POJO getter — типа, который напрямую возвращает переменную, – это кажется довольно бессмысленным. Зачем проверять, что язык делает то, что делает язык?
Идея тестирования такого геттера не проходит даже простейший тест самого ярого сторонника тестирования. В ответе Stack Overflow , Роберт К. Мартин говорит:
Мое личное правило заключается в том, что я напишу тест для любой функции, которая принимает решение или выполняет нечто большее, чем тривиальное вычисление. Я не буду писать тест для i + 1, но, вероятно, сделаю это для if (i<0)… и определенно будет для (-b + Math.sqrt(b * b – 4 * a * c))/(2 * a).
И это, безусловно, справедливое замечание. Я бы поспорил, что большинство людей на самом деле
Однако я хотел бы рассмотреть этот вопрос в другом контексте, который, как мне кажется, меняет все: объектно-ориентированный дизайн.
Развязка и Принцип Единого Доступа
Давайте начнем с принципа единообразного доступа (UAP). В значительной степени забытый в современном мире, UAP заявляет, что:
Все услуги, предлагаемые модулем, должны быть доступны с помощью единой нотации, которая не выдает, реализованы ли они с помощью хранилища или с помощью вычислений
В терминах Java UAP задает вопрос: “когда вы вызываете средство получения (сервис) для класса (модуля), является ли полученное значение просто частью данных или оно вычисляется (или выводится) из других данных?”
Клиентский код не должен знать.
Ну и что? вы могли бы спросить. Мы все знаем, что POJO – это всего лишь несколько оболочек вокруг данных. Зачем притворяться, что это не так?
Что ж, UAP (несколько педантично) указывает, что клиентский код не может сделать такого предположения. Если модуль выдает свою реализацию клиентскому коду или клиентский код делает предположения о том, откуда взялось значение, это нарушает фундаментальные принципы проектирования OO.
С этой точки зрения вы должны даже протестировать геттеры. Они могут просто возвращать значение, но что, если это не так? Или, что, если это так, но это изменится в будущем?
Преимущество такого мышления
Одна из целей OO состоит в том, чтобы отделить интерфейс от реализации, сделав реализацию способной развиваться без принудительных изменений в сотрудничающем коде. На заре разработки Java-приложений это разделение осуществлялось буквально путем создания интерфейса для каждого класса. Это явное физическое разделение (два отдельных файла) подчеркивало идею о том, что текущий класс является лишь одной возможной реализацией интерфейса среди потенциально многих.
Мысля в четвертом измерении, “потенциально многие” реализации также могут включать различные эволюции одного и того же POJO в течение жизненного цикла приложения.
Таким образом, преимущество просмотра приложения в контексте интерфейсов заключается в том, что мы максимально расширяем его возможности. Используя интерфейсы в качестве единиц проектирования приложений, мы можем развивать любую заданную часть приложения, которая нуждается в изменениях, не навязывая эти изменения каким-либо взаимодействующим классам. Они отделены от реализации. Интерфейс действует как ограждение от изменений. Пока интерфейс не меняется, соавторам это тоже не нужно.
С практической точки зрения это означает, что мы можем “быстро реагировать” на изменения в требованиях пользователей и/или бизнес-среде.
Цель испытаний
Хорошо, какое это имеет отношение к тестированию? Что ж, быстрое продвижение возможно только тогда, когда разработчики могут быстро и безопасно разрабатывать реализации. Поэтому я считаю, что наряду с компилятором тесты должны обеспечивать такую безопасность. Тесты должны гарантировать, что реализации соответствуют интерфейсу, который они рекламируют.
Когда я говорю “интерфейс” в этом контексте, я имею в виду не только Java interface ключевое слово, но термин |/OO/| . Ведут ли методы, предоставляемые классом, себя так, как ожидает клиент? Если возвращаемый тип является объектом, что означает null? Если примитив, существуют ли определенные значения, такие как -1, которые указывают на то, что чего-то не существует? Какая проверка выполняется или не выполняется для переданных входных данных? Что требуется от этого ввода?
В принципе, думайте о тестах как об одном из способов застраховать контракты методов . В современной Java тесты – лучший способ сделать это. Тесты обеспечивают проверку контракта метода, чего не может сделать компилятор. Мы не хотим проверять, что язык делает то, что делает язык (компилятор уже делает это), но есть еще другие вещи, которые нам нужно знать.
Практические примеры
Это не просто теория. Это всплывает все время. Вот несколько примеров.
Соглашение между конструктором или установщиками и получателями
Несмотря на то, что POJO является “немым объектом”, все равно ожидается, что после создания геттеры объекта вернут значение, с которым был создан объект. И если это изменяемый объект (которого вам следует избегать, но это, вероятно, разговор в другой раз), то после того, как вы вызовете сеттер, вызов соответствующего геттера должен вернуть новое значение.
Тот факт, что так много POJO или частей POJO создаются путем копирования и вставки, делает это не таким уж надежным. Это довольно легко для двух добытчиков, скажем getFirstName
и getLastName
, чтобы люди, склонные к ошибкам, попадали в тупик следующим образом:
public String getFirstName() { return firstName; } public String getLastName() { return firstName; // ack! }
Поймает ли это компилятор? Нет. Будет ли торопливый разработчик? Иногда? Будет ли хороший модульный тест? Да.
Обеспечение значимости данных
Даже “тупые” данные могут иметь контракт. Я упоминал об этом ранее, когда спрашивал: “Если это объект, то что означает значение null? ” Существуют всевозможные ситуации, когда данные имеют специальные значения, такие как null, -1, значение из перечисления или что-то еще. Это целая куча ситуаций, когда тесты действительно должны помочь дать нам гарантии того, что наше приложение работает правильно. Другой класс проблем, связанных только с данными, возникает из-за согласования между несколькими полями данных. Например, если я отработал ноль часов в период оплаты, я все равно не могу работать сверхурочно. Аналогично, если в данном POJO есть какая-либо проверка, это должно быть проверено. Что делать, если получатель возвращает значение int, но это значение представляет собой процент? Нормально ли, если это число когда-либо будет меньше нуля или больше 100? Как тесты этого приложения застрахуют от ошибок разработчика?
И, кстати, процент, возвращаемый получателем, является отличным примером, когда UAP применяется в полной мере. Процент, вероятно, вычисляется из двух других значений, но кто знает?
Добавление поведения там, где его когда-то не было
Возможно, вы философски против того, чтобы POJOs обладал поведением, но в реальном приложении это иногда лучше, чем альтернатива. Классическим примером является рефакторинг для устранения feature envy . Особенность Envy – это знаменитый запах кода, введенный Мартином Фаулером в книге Рефакторинг и это также видно в классической книге Чистый код Роберта К. Мартина, где он фигурирует как G14 в главе Запахи и эвристика . Там приведен следующий пример:
public class HourlyPayCalculator { public Money calculateWeeklyPay(HourlyEmployee e) { int tenthRate = e.getTenthRate().getPennies(); int tenthsWorked = e.getTenthsWorked(); int straightTime = Math.min(400, tenthsWorked); int overTime = Math.max(0, tenthsWorked - straightTime); int straightPay = straightTime * tenthRate; int overtimePay = (int)Math.round(overtime*tenthRate*1.5); return new Money(straightPay + overtimePay); } }
Дается следующее объяснение функции envy:
Метод calculateWeeklyPay
обращается к объекту HourlyEmployee
, чтобы получить данные, с которыми он работает. Метод Calculateeklypay
|/завидует масштабу Hourly Employee . Он "желает", чтобы он мог быть внутри
Почасовой работник
.
Конечно, ответ заключается в том, чтобы переместить Calculateeklypay
в Почасовой работник
. К сожалению, это проблема, если у вас есть кодовая база с правилом, исключающим пакет Почасового сотрудника
из тестирования и покрытия кода.
Равенство и идентичность
Последний краткий пример. Одним из основных применений POJOs является придание приложению осмысленной логики равенства. Но варианты использования различаются; вы хотите проверить равенство или идентичность? Главное, что определяет объект value , – это то, что он равен другому объекту value, когда все их соответствующие члены равны. Но другие типы POJO, вероятно, необходимо дифференцировать на основе идентичности. Всегда ли очевидно, кто есть кто? Доверяете ли вы своей команде разработчиков (или себе) автоматически распознавать разницу и добавлять или удалять тесты по мере изменения ситуации? Должны ли вы предоставлять тесты, гарантирующие такое поведение?
Что вы думаете?
Я надеюсь, что это, по крайней мере, покажет самому ярому противнику POJO-тестирования, что есть веские причины для их тестирования. Возможно, теперь вы, по крайней мере, добавите тесты, если какой-либо из этих случаев возникнет в ваших существующих POJOS. Что вы думаете? Если вы не протестируете POJOs прямо сейчас, как вы думаете, вы начнете? Или вы уже тестируете их? Если да, то почему? Каковы ваши причины? Дайте мне знать ваши мысли в комментариях ниже.
Оригинал: “https://dev.to/scottshipp/why-test-pojos-1lfb”