Избранное сообщение

Фетісов В. С. Комп’ютерні технології в тестуванні. Навчально-методичний посібник. 2-ге видання, перероблене та доповнене / Мои публикации

В 10-х годах я принимал участие в программе Европейского Союза Tempus "Освітні вимірювання, адаптовані до стандартів ЄС". В рамк...

Благодаря Интернету количество писателей и поэтов увеличивается в геометрической прогрессии. Поголовье читателей начинает заметно отставать.

четверг, 27 декабря 2018 г.

Римско-католическая церковь в Бонне / Фото из личного архива



Римско-католическая церковь в Бонне - одна из старейших церквей Германии, построенных между 11-ми и 13-ми веками. Церковь даже служила собором для Архиепископства Кельна. Сейчас церковь -  просто незначительная базилика.

Смотри также Бонн:

    Памятник Бетховену. https://fetisovvs.blogspot.com/2018/11/blog-post_79.html
    Дом-музей Бетховена в Бонне. https://fetisovvs.blogspot.com/2018/12/blog-post_17.html
    Сувенирные галстуки. https://fetisovvs.blogspot.com/2018/12/blog-post_18.html
    Церковь святого Мартина. https://fetisovvs.blogspot.com/2018/12/blog-post_73.html
    Монастырь-базилика святого Мартина. https://fetisovvs.blogspot.com/2018/11/blog-post_20.html
    https://fetisovvs.blogspot.com/2018/11/blog-post_25.html
    Фонтан св. Мартина. https://fetisovvs.blogspot.com/2018/11/blog-post_55.html
    Римско-католическая церковь в Бонне. https://fetisovvs.blogspot.com/2018/12/blog-post_19.html
    https://fetisovvs.blogspot.com/2018/12/11-13.html
    https://fetisovvs.blogspot.com/2018/12/blog-post_98.html
    Университет. https://fetisovvs.blogspot.com/2018/12/blog-post_10.html
    Мерия. https://fetisovvs.blogspot.com/2018/12/blog-post_4.html
    https://fetisovvs.blogspot.com/2018/12/blog-post_9.html
    Здание почтампа. https://fetisovvs.blogspot.com/2018/11/blog-post_27.html
    Улочки Бонна. https://fetisovvs.blogspot.com/2018/11/blog-post_18.html
    https://fetisovvs.blogspot.com/2018/12/blog-post_3.html
    Архитектура Бонна. https://fetisovvs.blogspot.com/2018/12/blog-post_83.html
    https://fetisovvs.blogspot.com/2018/12/blog-post_13.html
    Площадь. https://fetisovvs.blogspot.com/2018/12/blog-post_51.html
    Уличные музыканты. https://fetisovvs.blogspot.com/2018/11/blog-post_28.html
    Рейн в черте Бонна. https://fetisovvs.blogspot.com/2018/12/blog-post_14.html

    среда, 26 декабря 2018 г.

    Динамический прокси Java: что это и как им пользоваться? / Программирование на Java

    Всем привет!

    Ну что ж до Нового года и старта десятого потока «Разработчик Java» осталось совсем шуть-шуть. Так что у нас остался один открытый урок, который мы подготавливаем для публикации и сегодняшняя заметка, из которой вы узнаете о динамическом прокси Java: что это такое, когда и как его использовать в коде.

    Что такое прокси?

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

    Таким образом, прокси-классы удобно реализуют многие вещи:

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



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

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

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

    Это сложная тема, которая требует использования класса отражения, или манипулирования байт-кодом, или компиляции Java-кода, сгенерированного динамически. А может всего и сразу. Чтобы новый класс не был доступен в качестве байт-кода во время исполнения, потребуются сгенерированный байт-код и загрузчик классов для загрузки байт-кода. Для создания байт-кода, используйте cglibbytebuddy или встроенный компилятор Java.

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

    Как этим пользоваться в нашем коде?

    Самое простое — использовать java.lang.reflect.Proxy, который является частью JDK. Этот класс может создать прокси-класс или напрямую его инстанс. Пользоваться прокси, встроенным в Java, очень просто. Все что нужно — реализовать java.lang.InvocationHandler, чтобы прокси-объект мог его вызывать. Интерфейс InvocationHandler крайне прост и содержит только один метод: invoke(). При его вызове, аргументы содержат проксируемый оригинальный объект, вызванный метод (как отражение объекта Method) и массив объектов исходных аргументов. Фрагмент кода ниже демонстрирует применение:

    package proxy;
    
    import java.lang.reflect.InvocationHandler;
    import java.lang.reflect.InvocationTargetException;
    import java.lang.reflect.Method;
    import java.lang.reflect.Proxy;
    public class JdkProxyDemo {
        interface If {
            void originalMethod(String s);
        }
        static class Original implements If {
            public void originalMethod(String s) {
                System.out.println(s);
            }
        }
        static class Handler implements InvocationHandler {
            private final If original;
            public Handler(If original) {
                this.original = original;
            }
            public Object invoke(Object proxy, Method method, Object[] args)
                    throws IllegalAccessException, IllegalArgumentException,
                    InvocationTargetException {
                System.out.println("BEFORE");
                method.invoke(original, args);
                System.out.println("AFTER");
                return null;
            }
        }
        public static void main(String[] args){
            Original original = new Original();
            Handler handler = new Handler(original);
            If f = (If) Proxy.newProxyInstance(If.class.getClassLoader(),
                    new Class[] { If.class },
                    handler);
            f.originalMethod("Hallo");
        }
    }

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

    В особом случае, вы можете создать обработчик вызова и прокси интерфейса без оригинального объекта. Более того, класс для реализации интерфейса в исходном коде — не требуется. Его реализует динамически созданный прокси-класс. 

    Если же проксируемый класс не реализует интерфейс, стоит задуматься об использовании какой-либо иной реализации прокси. 

    THE END

    Ждём ваши комментарии и вопросы. Как всегда или тут, или можно зайти к Виталию на день открытых дверей.


    Смотри также популярное:

    Зачем нужна Java. http://fetisovvs.blogspot.com/2014/07/java.html
    Когда Java наконец помрёт, что с этим делать и что будет с JPoint. https://fetisovvs.blogspot.com/2018/11/java-jpoint-java.html
    Разбор основных концепций параллелизма. http://fetisovvs.blogspot.com/2018/04/java.html
    Первый контакт с «var» в Java 10. http://fetisovvs.blogspot.com/2018/01/var-java-10-java.html
    JAVA 9. Что нового? http://fetisovvs.blogspot.com/2017/10/java-9-java.html
    Руководство по Java 9 для тех, кому приходится работать с legacy-кодом. http://fetisovvs.blogspot.com/2018/08/java-9-legacy-java.html
    Концепции объектно-ориентированного программирования — ООП в Java. http://fetisovvs.blogspot.com/2017/01/java-java.html
    Анимации в Android по полочкам (Часть 1. Базовые анимации). http://fetisovvs.blogspot.com/2018/02/android-1-java.html
    Двести пятьдесят русскоязычных обучающих видео докладов и лекций о Java. http://fetisovvs.blogspot.com/2015/12/java-5-java-java.html
    Абстрактные классы и методы. http://fetisovvs.blogspot.com/2017/02/java.html
    Полное руководство по Java Reflection API. Рефлексия на примерах. http://fetisovvs.blogspot.com/2017/02/java-reflection-api-java.html
    Микросервисы для Java программистов. Практическое введение во фреймворки и контейнеры. http://fetisovvs.blogspot.com/2017/10/java-java.html
    Микросервисы для Java программистов. Практическое введение во фреймворки и контейнеры. (Часть 3). http://fetisovvs.blogspot.com/2017/10/java-3-java.html
    ТОП-3 способа конвертировать массив в ArrayList. Пример на Java. http://fetisovvs.blogspot.com/2016/09/3-arraylist-java-java.html
    Ввод–вывод в Java. http://fetisovvs.blogspot.com/2016/05/java-java_28.html
    Java Challengers #2: Сравнение строк. https://fetisovvs.blogspot.com/2018/11/java-challengers-2-java.html
    Enum-Всемогущий. http://fetisovvs.blogspot.com/2017/02/enum-java.html
    Массивы в Java. Создание и обработка. http://fetisovvs.blogspot.com/2017/10/java-java_18.html
    Arrays, Collections: Алгоритмический минимум. http://fetisovvs.blogspot.com/2017/12/arrays-collections.html
    Популярные методы для работы с Java массивами. http://fetisovvs.blogspot.com/2016/09/java-java_29.html
    Пример использования метода replace в Java. Как заменить символ в строке? http://fetisovvs.blogspot.com/2017/01/replace-java-java.html
    Класс Scanner в Java — описание и пример использования. http://fetisovvs.blogspot.com/2017/01/scanner-java-java.html
    Пример использования метода trim в Java: как удалить пробелы в начале и конце строки? http://fetisovvs.blogspot.com/2017/01/trim-java-java.html
    Spark — Потрясающий веб-микрофреймворк для Java. http://fetisovvs.blogspot.com/2017/10/spark-java-java.html
    Чтение и запись CSV файла с помощью SuperCSV. http://fetisovvs.blogspot.com/2017/01/csv-supercsv-java-java.html
    Конструкция try/catch/finally (исключения). http://fetisovvs.blogspot.com/2017/01/trycatchfinally-java.html
    1000+ часов видео по Java на русском. http://fetisovvs.blogspot.nl/2017/06/1000-java-java.html
    Раздача халявы: нетормозящие треды в Java. Project Loom. http://fetisovvs.blogspot.com/2018/09/java-project-loom-java.html
    Шпаргалка Java программиста 7.1 Типовые задачи: Оптимальный путь преобразования InputStream в строку. http://fetisovvs.blogspot.com/2016/04/java-71-inputstream-java.html
    Шпаргалки Java программиста 10: Lombok. http://fetisovvs.blogspot.nl/2017/12/java-10-lombok-java.html
    Шпаргалки Java программиста 9: Java SE — Шпаргалка для собеседований и повторений. http://fetisovvs.blogspot.com/2017/12/java-9-java-se-java.html
    Шпаргалка Java программиста 8. Библиотеки для работы с Json (Gson, Fastjson,
    LoganSquare, Jackson, JsonPath и другие). http://fetisovvs.blogspot.com/2016/04/java-8-json-gson-fastjson-logansquare.html
    Java 8 и паттерн Стратегия. http://fetisovvs.blogspot.com/2018/03/java-8-java.html
    Java EE Concurency API. http://fetisovvs.blogspot.com/2018/08/java-ee-concurency-api-java.html
    Реализация ООП-наследования в классах, работающих с SQL и MS Entity Framework. http://fetisovvs.blogspot.com/2017/02/sql-ms-entity-framework.html
    Как установить соединение с СУБД MySQL в IntelliJ IDEA в редакции Community. http://fetisovvs.blogspot.com/2016/04/mysql-intellij-idea-community-java.html
    TDD приложений на Spring Boot: работа с базой данных. https://fetisovvs.blogspot.com/2018/12/tdd-spring-boot-java.html
    Максимально простой в поддержке способ интеграции java-клиента с java-сервером. http://fetisovvs.blogspot.com/2018/09/java-java-java.html
    Как с помощью maven работать с библиотеками, которых в maven нет. http://fetisovvs.blogspot.com/2017/03/maven-maven-java.html
    Проекты по созданию компиляторов из Java в JavaScript и исполняемые файлы. http://fetisovvs.blogspot.com/2018/01/java-javascript-java.html
    Реактивное программирование с JAX-RS. http://fetisovvs.blogspot.com/2018/09/jax-rs-java.html
    Компактные строки в Java 9. https://fetisovvs.blogspot.com/2018/10/java-9-java.html
    Абстрактный CRUD от репозитория до контроллера: что ещё можно сделать при помощи Spring + Generics. http://fetisovvs.blogspot.com/2018/09/crud-spring-generics-java.html
    Диагностика утечек памяти в Java. http://fetisovvs.blogspot.com/2017/03/java-java_18.html
    Spring AOP и JavaConfig в плагинах для Atlassian Jira. http://fetisovvs.blogspot.com/2018/04/spring-aop-javaconfig-atlassian-jira.html
    Блеск и нищета Java для настольных систем. http://fetisovvs.blogspot.com/2018/04/java-haulmont-java.html
    Разбор задачек от Одноклассников на JPoint 2018. http://fetisovvs.blogspot.com/2018/04/jpoint-2018-java.html
    Программируем… выход из лабиринта. http://fetisovvs.blogspot.com/2015/10/java.html
    Основы работы с IntelliJ IDEA. Интерфейс программы. http://fetisovvs.blogspot.com/2016/09/intellij-idea-java.html
    Ускоряем время сборки и доставки java web приложения. http://fetisovvs.blogspot.com/2018/03/java-web-java.html
    Открытый урок Java Enterprise «CDI in action». http://fetisovvs.blogspot.com/2018/09/java-enterprise-cdi-in-action-java.html
    «Мы все стремимся к сложности, а потом с ней боремся»: интервью с Венкатом Субраманиамом. http://fetisovvs.blogspot.com/2018/09/java_16.html

    вторник, 25 декабря 2018 г.

    TDD приложений на Spring Boot: работа с базой данных / Программирование на Java

    Вторая статья из цикла "Test-Driven Development приложений на Spring Boot" и в этот раз я буду говорить про тестирование доступа к базе данных, важного аспекта интеграционного тестирования. Я расскажу как через тесты определять интерфейс будущего сервиса для доступа к данным, как использовать встраиваемые in-memory базы для тестирования, работать с транзакциями и загружать тестовые данные в базу.


    Я не буду много говорить про TDD и тестирование вообще, всех желающих приглашаю почитать первую статью — Как построить пирамиду в багажнике или Test-Driven Development приложений на Spring Boot / Хабр.
    Начну, как и в прошлый раз, с небольшой теоретической части, и перейду к end-to-end тесту.

    Пирамида тестирования



    Для начала, маленькое, но необходимое, описание такой важной сущности в тестировании, как The Test Pyramid или пирамида тестирования.
    image
    (взято с сайта The Practical Test Pyramid)
    Пирамидой тестирования называется подход, когда тесты организуются в несколько уровней.
    • UI (или end-to-end, E2E) тестов мало и они медленные, но тестируют реальное приложение — никаких моков и тестовых двойников. На этом уровне часто мыслит бизнес и здесь обитают все BDD фреймворки (см. Cucumber в предыдущей статьей).
    • За ними идут интеграционные тесты (сервисные, компонентные — терминология у каждого своя), которые уже фокусируются на конкретном компоненте (сервисе) системы, изолируя его от остальных компонентов через моки / двойники, но по прежнему проверяющие интеграцию с реальными внешними системами — эти тесты подключаются к базе, посылают REST запросы, работаю с очередью сообщений. По-сути, это тесты которые проверяют интеграцию бизнес логики с внешним миром.
    • В самом низу находятся быстрые юнит-тесты, которые тестируют минимальные блоки кода (классы, методы) в полной изоляции.


    Spring помогает с написанием тестов для каждого уровня — даже для unit-тестов, хотя это может звучать странно, ведь в мире юнит-тестов никакого знания про фреймворк вообще существовать не должно. После написания E2E теста я как раз покажу, как Spring позволяет даже такие чисто "интеграционные" вещи, как контроллеры, тестировать в изоляции.
    Но начну я с самой вершины пирамиды — медленного UI теста, которые стартует и тестирует полноценное приложение.

    End-to-end test



    Итак, новая фича:
    Feature: A list of available cakes
    
      Background: catalogue is updated
        Given the following items are promoted
          | Title           | Price |
          | Red Velvet      | 3.95  |
          | Victoria Sponge | 5.50  |
    
      Scenario: a user visiting the web-site sees the list of items
        Given a new user, Alice
        When she visits Cake Factory web-site
        Then she sees that "Red Velvet" is available with price £3.95
        And she sees that "Victoria Sponge" is available with price £5.50


    И здесь сразу интересный аспект — что делать с предыдущим тестом, про приветствие на главной странице? Он вроде уже не актуален, после запуска сайта на главной уже будет каталог, а не приветствие. Здесь нет однозначного ответа, я бы сказал — зависит от ситуации. Но главный совет — не привязывайтесь к тестам! Удаляйте, когда они теряют актуальность, переписывайте, чтобы было проще читать. Особенно E2E тесты — это должна быть, по-сути, живая и актуальная спецификация. В моем случае, я просто удалил старый тесты, и заменил их новыми, используя некоторые предыдущие шаги и добавив несуществующие.
    Теперь же я подошел к важному моменту — выбор технологии для хранения данных. В соответствии с lean подходом, я бы хотел отложить выбор до самого последнего момента — когда я точно будут знать, реляционная модель или нет, какие требование к консистентности, транзакционности. В общем случае, для этого есть решения — например, создание тестовых двойников и различных in-memory хранилищ, но пока я не хочу усложнять статью и сразу выберу технологию — реляционные базы данных. Но чтобы сохранить хоть какую-то возможность выбора БД, я добавлю абстракцию — Spring Data JPA. JPA сама по себе достаточно абстрактная спецификация для доступа к реляционным базам, а Spring Data делает её использование еще проще.
    Spring Data JPA по умолчанию использует Hibernate в качестве провайдера, но поддерживает и другие технологии, например EclipseLink и MyBatis. Для людей не очень знакомых с Java Persistence API — JPA это как бы интерфейс, а Hibernate класс, его реализующий.
    Итак, чтобы добавить поддержку JPA я добавил пару зависимостей:
    implementation('org.springframework.boot:spring-boot-starter-data-jpa')
    runtime('com.h2database:h2')


    В качестве базы данных я буду использовать H2 — встраиваемую базу данных, написанную на Java, с возможностью работать в in-memory режиме.
    Используя Spring Data JPA я сразу определяю интерфейс для доступа к данным:
    interface CakeRepository extends CrudRepository<CakeEntity, String> { }


    И сущность:
    @Entity
    @Builder
    @AllArgsConstructor
    @Table(name = "cakes")
    class CakeEntity {
    
        public CakeEntity() {
        }
    
        @Id
        @GeneratedValue(strategy = GenerationType.IDENTITY)
        Long id;
    
        @NotBlank
        String title;
    
        @Positive
        BigDecimal price;
    
        @NotBlank
        @NaturalId
        String sku;
    
        boolean promoted;
    
        @Override
        public boolean equals(Object o) {
            if (this == o)
                return true;
            if (o == null || getClass() != o.getClass())
                return false;
            CakeEntity cakeEntity = (CakeEntity) o;
            return Objects.equals(title, cakeEntity.title);
        }
    
        @Override
        public int hashCode() {
            return Objects.hash(title);
        }
    
    }


    В описании сущности есть пара не самых очевидных вещей.
    • @NaturalId для поля sku. Это поле используется как “натуральный идентификатор” для проверки равенства сущностей — использование всех полей или @Id поля в equals / hashCode методах это, скорее, анти-паттерн. О том, как правильно проверять равенство сущностей хорошо написано, например, тут.
    • Чтобы хоть немного снизить количество boilerplate кода, я использую Project Lombok — annotation processor для Java. Он позволяет добавлять разные полезные вещи, вроде @Builder — чтобы автоматически генерить билдер для класса и @AllArgsConstructor чтобы создать конструктор для всех полей.


    Реализация интерфейса будет предоставлена автоматически Spring Data.

    Вниз по пирамиде



    Теперь время спуститься на следующий уровень пирамиды. В качестве эмпирического правила, я бы рекомендовал всегда начинать с e2e теста, потому, что это позволит определить "конечную цель" и границы новой фичи, но дальше строгих правил нет. Не обязательно писать сначала интеграционный тест, перед тем, как перейти на юнит-уровень. Просто чаще всего получается, что так удобнее и проще — и вполне естественно спускаться "вниз".
    Но конкретно сейчас, я бы хотел сразу нарушить это правило и написать юнит-тест, который поможет определить интерфейс и контракт нового компонента, который пока не существует. Контроллер должен вернуть модель, которую заполнит из некоего компонента X, и я написал такой тест:
    
    @ExtendWith(MockitoExtension.class)
    class IndexControllerTest {
    
        @Mock
        CakeFinder cakeFinder;
    
        @InjectMocks
        IndexController indexController;
    
        private Set<Cake> cakes = Set.of(new Cake("Test 1", "£10"),
                new Cake("Test 2", "£10"));
    
        @BeforeEach
        void setUp() {
            when(cakeFinder.findPromotedCakes()).thenReturn(cakes);
        }
    
        @Test
        void shouldReturnAListOfFoundPromotedCakes() {
            ModelAndView index = indexController.index();
    
            assertThat(index.getModel()).extracting("cakes").contains(cakes);
        }
    
    }


    Это чистый юнит-тест — никаких контекстов, никаких баз данных тут нет, только Mockito для моков. И этот тест как раз хорошая демонстрация, как Spring помогает юнит тестам — контроллер в Spring MVC это просто класс, методы которого принимают параметры обычных типов и возвращают POJO объекты — View Models. Нет ни HTTP запросов, ни ответов, хедеров, JSON, XML — все это будет автоматически применено ниже по стеку, в виде конвертеров и сериализаторов. Да, есть небольшой "намек" на Spring в виде ModelAndView, но это обычный POJO и даже от него при желании можно избавиться, он нужен именно для UI контроллеров.
    Я не буду много говорить про Mockito, можно все прочитать в официальной документации. Конкретно в этом тесте есть только интересных моментов — я использую MockitoExtension.class в качестве исполнителя тестов, и он автоматически сгенерит моки для полей, аннотированных @Mock и потом заинжектит эти моки, как зависимости в конструктор для объекта в поле помеченном @InjectMocks. Можно все это сделать вручную, используя Mockito.mock() метод и потом создав класс.
    И этот тест помогает определить метод нового компонента — findPromotedCakes, список тортов, которые мы хотим показать на главной странице. Он не определяет, что это такое, или как это должно работать с базой. Единственная ответственность контроллера — взять то, что ему передали, и вернуть в определенном поле модели ("cakes"). Но тем не менее, в моем интерфейсе CakeFinder уже есть первый метод, а значит можно писать для него интеграционный тест.
    Я сознательно сделал все классы внутри пакета cakes package private, чтобы никто, за пределами пакета не смог их использовать. Единственный способ получить данные из базы — это интерфейс CakeFinder, который и есть мой “компонент Х” для доступа к базе. Он становится естественным “коннектором”, который я дальше могу легко замокать, если мне нужно будет тестировать что-то в изоляции и не трогать базу. А его единственная реализация — это JpaCakeFinder. И если, например, в будущем тип базы или источник данных поменяется — то нужно будет добавить реализацию интерфейса CakeFinder, не меняя код, его использующий.


    Интеграционный тест для JPA используя @DataJpaTest



    Интеграционные тесты — это хлеб и масло Spring. В нем, вообщем-то, все так здорово сделано для интеграционного тестирования, что разработчики иногда не хотят уходить на юнит-уровень или пренебрегают UI уровнем. Это не плохо и не хорошо — повторюсь, что главная цель тестов — это уверенность. И набора быстрых и эффективных интеграционных тестов может быть достаточно, чтобы эту уверенность предоставить. Однако есть опасность, что эти тесты со временем либо будут медленнее и медленнее, либо просто начнут тестировать компоненты в изоляции, вместо интеграции.
    Интеграционные тесты могут запустить приложение, как есть (@SpringBootTest), либо его отдельный компонент (JPA, Web). В моем случае, я хочу написать сфокусированный тест для JPA — поэтому мне нет необходимости конфигурировать контроллеры или любые другие компоненты. За это в Spring Boot Test отвечает аннотация @DataJpaTest. Это мета-аннотация, т.е. она комбинирует сразу несколько разных аннотаций, конфигурирующих разные аспекты теста.
    • @AutoConfigureDataJpa
    • @AutoConfigureTestDatabase
    • @AutoConfigureCache
    • @AutoConfigureTestEntityManager
    • @Transactional


    Сначала расскажу про каждый в отдельности, а потом покажу готовый тест.
    @AutoConfigureDataJpa
    Загружает целый набор конфигураций и настраивает — репозитории (автоматическая генерация реализаций для CrudRepositories), инструменты миграции базы FlyWay и Liquibase, подключение к БД используя DataSource, менеджер транзакций, и, наконец, Hibernate. По-сути, это просто набор конфигураций, актуальных для доступа к данным — сюда не включены ни DispatcherServlet из Web MVC, ни другие компоненты.
    @AutoConfigureTestDatabase
    Это один из самых интересных аспектов JPA теста. Эта конфигурация ищет в classpath одну из поддерживаемых embedded баз данных и переконфигурирует контекст, чтобы DataSource указывал на случайно созданную in-memory базу. Так как я добавил зависимость на H2 базу — то больше делать ничего не нужно, просто наличие этой аннотации автоматически для каждого запуска теста предоставит пустую базу, и это просто невероятно удобно.
    Стоит помнить, что эта база будет полностью пустой, без схемы. Чтобы сгенерить схему, есть пара вариантов.
    1. Использовать фичу Auto DDL из Hibernate. Spring Boot Test автоматически поставит это значение в create-drop, чтобы Hibernate генерировал схему из описание сущностей и удалял ее в конце сессии. Это невероятно мощная фича Hibernate, которая очень полезна для тестов.
    2. Использовать миграции созданные Flyway или Liquibase.


    Подробнее про разные подходы к инициализации базы можно прочитать в документации.
    @AutoConfigureCache
    Просто конфигурирует кэш на использование NoOpCacheManager — т.е. не кешировать ничего. Это полезно, чтобы избежать сюрпризов в тестах.
    @AutoConfigureTestEntityManager
    Добавляет в контекст специальный объект TestEntityManager, который сам по себе интересный зверь. EntityManager это главный класс JPA, который отвечает за добавление сущностей в сессию, удаление и подобными вещами. Только вот когда, например, в работу вступает Hibernate — добавление сущности в сессию не значит, что будет выполнен запрос в базу, а загрузка из сессии не означает, что будет выполнен select запрос. За счет внутренних механизмов Hibernate реальные операции с базой будут выполнятся в подходящий момент, который определит сам фреймворк. Но в тестах может быть необходимость принудительно послать что-то в базу, ведь цель тестов как раз тестировать интеграцию. И TestEntityManager это просто хелпер, который поможет некоторые операции с базой принудительно выполнится — например, persistAndFlush() заставит Hibernate выполнить все запросы.
    @Transactional
    Эта аннотация делает все тесты в классе транзакционными, с автоматическим откатом транзакции по завершению теста. Это просто механизм “очистки” базы перед каждым тестом, ведь иначе пришлось бы вручную удалять данные из каждой таблицы.
    Должен ли тест управлять транзакцией — это не такой простой и очевидный вопрос, как может показаться. Не смотря на удобство “чистого” состояния базы, наличие @Transactionalв тестах может стать неприятным сюрпризом если “боевой” код не начинает транзакцию сам, а требует существующую. Это может привести к тому, что интеграционный тест пройдет, но при выполнении реального кода из контроллера, а не из теста, в сервисе не будет активной транзакции и метод бросит исключение. Хотя это и выглядит опасно, при наличии высокоуровневых тестов UI тестов, транзакционность тестов не так страшна. На моем опыте я видел только однажды, когда при проходящем интеграционном тесте падал продакшен код, который явно требовал наличие существующей транзакции. Но если все же нужно проверять, что сервисы и компоненты сами правильно управляют транзакциями, можно “перекрыть” аннотацию @Transactional на тесте с нужным режимом (например, не начинать транзакцию).


    Интеграционный тест со @SpringBootTest



    Еще хочу отметить, что @DataJpaTest это не уникальный пример фокусного интеграционного теста, еще есть @WebMvcTest@DataMongoTest и много других. Но одной из самых важных тестовых аннотации остается @SpringBootTest, которая запускает для тестов приложение “как есть” — со всеми настроенными компонентами и интеграциями. Возникает логичный вопрос — если можно запустить приложение целиком, зачем делать фокусные DataJpa тесты, например? Я бы сказал, что строгих правил тут снова нет.
    Если возможно запускать приложения каждый раз, изолировать падения в тестах, не перегружать и не переусложнять Setup теста — то конечно можно и нужно использовать @SpringBootTest.
    Однако в реальной жизни, приложения могут требовать много разных настроек, подключатся к разным системам, а я бы не хотел, чтобы мои тесты доступа к БД падали, т.к. не настроено подключение к очереди сообщений. Поэтому важно использовать здравый смысл, и если для того, чтобы заставить тест с @SpringBootTest аннотацией работать нужно замокать половину системы — то есть ли смысл тогда вообще в @SpringBootTest?

    Подготовка данных для теста



    Один из ключевых моментов для тестов, это подготовка данных. Каждый тест должен выполнятся в изоляции, и подготавливать окружение перед запуском, приводя систему в исходное желаемое состояние. Самый простой вариант это сделать — использовать @BeforeEach@BeforeAll аннотации и добавлять записи в базу там, используя репозиторий, EntityManagerили TestEntityManager. Но есть еще один вариант, который позволяет запустить подготовленный скрипт или выполнить нужный SQL-запрос, это аннотация @Sql. Spring Boot Test перед выполнением теста автоматически запустит указанный скрипт, избавив от необходимости добавлять @BeforeAll блок, а об очистке данных позаботиться @Transactional.
    @DataJpaTest
    class JpaCakeFinderTest {
    
        private static final String PROMOTED_CAKE = "Red Velvet";
    
        private static final String NON_PROMOTED_CAKE = "Victoria Sponge";
    
        private CakeFinder finder;
    
        @Autowired
        CakeRepository cakeRepository;
    
        @Autowired
        TestEntityManager testEntityManager;
    
        @BeforeEach
        void setUp() {
            this.testEntityManager.persistAndFlush(CakeEntity.builder().title(PROMOTED_CAKE)
                    .sku("SKU1").price(BigDecimal.TEN).promoted(true).build());
    
            this.testEntityManager.persistAndFlush(CakeEntity.builder().sku("SKU2")
                    .title(NON_PROMOTED_CAKE).price(BigDecimal.ONE).promoted(false).build());
    
            finder = new JpaCakeFinder(cakeRepository);
        }
      ...
    }


    Red-green-refactor цикл



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

    Подготовка данных



    Я использую подход с @BeforeAll / @BeforeEach и вручную создаю, все записи в базе. Пример с @Sql аннотацией вынесен в отдельный класс JpaCakeFinderTestWithScriptSetup, он дублирует тесты, чего быть, разумеется, не должно, и существует с единственной целью продемонстрировать подход.
    Исходное состояние системы — есть две записи в системе, один торт участвует в промоушене и должен быть включен в результат, возвращенный методом, второй — нет.

    Первый тест интеграционный тест



    Первый тест самый простой — findPromotedCakes должен включать описание и цену торта, участвующего в промоушене.

    Red



      @Test
        void shouldReturnPromotedCakes() {
            Iterable<Cake> promotedCakes = finder.findPromotedCakes();
    
            assertThat(promotedCakes).extracting(Cake::getTitle).contains(PROMOTED_CAKE);
            assertThat(promotedCakes).extracting(Cake::getPrice).contains("£10.00");
        }


    Тест, разумеется, падает — дефолтная реализация возвращает пустой Set.

    Green



    Естественным желаем будет сразу писать фильтрацию, делать запрос в базу с where и так далее. Но следуя практике TDD, я должен написать минимальный код чтобы тест прошел. И этот минимальный код — вернуть все записи в базе. Да, так просто и банально.
      public Set<Cake> findPromotedCakes() {
            Spliterator<CakeEntity> cakes = this.cakeRepository.findAll()
                    .spliterator();
    
            return StreamSupport.stream(cakes, false).map(
                    cakeEntity -> new Cake(cakeEntity.title, formatPrice(cakeEntity.price)))
                    .collect(Collectors.toSet());
        }
    
      private String formatPrice(BigDecimal price) {
            return "£" + price.setScale(2, RoundingMode.DOWN).toPlainString();
        }


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


    Refactor



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

    Второй тест



    А вот второй тест уже проверит, что не promoted торт не попадет в результат, возвращаемый findPromotedCakes.
      @Test
        void shouldNotReturnNonPromotedCakes() {
            Iterable<Cake> promotedCakes = finder.findPromotedCakes();
    
            assertThat(promotedCakes).extracting(Cake::getTitle)
                    .doesNotContain(NON_PROMOTED_CAKE);
        }


    Red



    Тест, ожидаемо, падает — в базе две записи и код просто возвращает их всех.

    Green



    И снова можно задуматься — а какой минимальный код можно написать, чтобы тест прошел? Раз уже есть stream и его сборка — можно просто добавить туда filter блок.
      public Set<Cake> findPromotedCakes() {
            Spliterator<CakeEntity> cakes = this.cakeRepository.findAll()
                    .spliterator();
    
            return StreamSupport.stream(cakes, false)
                    .filter(cakeEntity -> cakeEntity.promoted)
                    .map(cakeEntity -> new Cake(cakeEntity.title, formatPrice(cakeEntity.price)))
                    .collect(Collectors.toSet());
        }


    Перезапускаем тесты — интеграционные тесты теперь зеленые. Настал важный момент — за счет комбинации юнит-теста контроллера и интеграционного теста для работы с БД моя фича готова — и UI тест теперь проходит!

    Refactor



    И раз все тесты зеленые — настало время рефакторинга. Думаю, не нужно пояснять, что фильтрация в памяти — не лучшая идея, лучше это делать в базе. Чтобы это сделать, я добавил новый метод в CakesRepository — findByPromotedIsTrue:
    interface CakeRepository extends CrudRepository<CakeEntity, String> {
    
        Iterable<CakeEntity> findByPromotedIsTrue();
    
    }


    Для этого метода Spring Data автоматически сгенерил метод, который выполнит запрос вида select from cakes where promoted = true. Подробнее про генерацию запросов можно почитать в документации к Spring Data.
      public Set<Cake> findPromotedCakes() {
            Spliterator<CakeEntity> cakes = this.cakeRepository.findByPromotedIsTrue()
                    .spliterator();
    
            return StreamSupport.stream(cakes, false).map(
                    cakeEntity -> new Cake(cakeEntity.title, formatPrice(cakeEntity.price)))
                    .collect(Collectors.toSet());
        }


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

    Подключение к production базе



    Чтобы добавить немного “реалистичности” и показать, как можно разделять конфигурацию для тестов и основного приложения, я добавлю конфигурацию доступа к данным для “продакшен” приложения.
    Добавляется все традиционно секцией в application.yml:
    datasource:
      url: jdbc:h2:./data/cake-factory
    


    Это автоматически сохранит данные в файловой системе в папке ./data. Замечу, что в тестах этой папки создано не будет — @DataJpaTest автоматически заменит подключение к файловой базе на случайную базу в памяти благодаря наличию @AutoConfigureTestDatabase аннотации.
    Две полезные вещи, которые могут пригодится — это файлы data.sql и schema.sql. При запуске приложения, Spring Boot проверит наличие этих файлов в ресурсах и выполнит эти скрипты при их наличии. Эта фича может быть полезной при локальной разработке и прототипировании, в реальных базах, разумеется, нужно использовать инструменты миграции.


    Заключение



    Итак, в этот раз я показал как через тесты определить интерфейс сервисе для доступа к данным, как написать интеграционный тест, и как писать минимальный код в TDD цикле.
    В следующей статье я добавлю Spring Security — покажу как тестировать приложение для разных пользователей и ролей и какие инструменты для этого предоставляет Spring, а так же как определять границы теста.

    Источник: https://habr.com/post/433958/?utm_source=habrahabr&utm_medium=rss&utm_campaign=433958


    Смотри также популярное:

    Зачем нужна Java. http://fetisovvs.blogspot.com/2014/07/java.html
    Когда Java наконец помрёт, что с этим делать и что будет с JPoint. https://fetisovvs.blogspot.com/2018/11/java-jpoint-java.html
    Разбор основных концепций параллелизма. http://fetisovvs.blogspot.com/2018/04/java.html
    Первый контакт с «var» в Java 10. http://fetisovvs.blogspot.com/2018/01/var-java-10-java.html
    JAVA 9. Что нового? http://fetisovvs.blogspot.com/2017/10/java-9-java.html
    Руководство по Java 9 для тех, кому приходится работать с legacy-кодом. http://fetisovvs.blogspot.com/2018/08/java-9-legacy-java.html
    Концепции объектно-ориентированного программирования — ООП в Java. http://fetisovvs.blogspot.com/2017/01/java-java.html
    Анимации в Android по полочкам (Часть 1. Базовые анимации). http://fetisovvs.blogspot.com/2018/02/android-1-java.html
    Двести пятьдесят русскоязычных обучающих видео докладов и лекций о Java. http://fetisovvs.blogspot.com/2015/12/java-5-java-java.html
    Абстрактные классы и методы. http://fetisovvs.blogspot.com/2017/02/java.html
    Полное руководство по Java Reflection API. Рефлексия на примерах. http://fetisovvs.blogspot.com/2017/02/java-reflection-api-java.html
    Микросервисы для Java программистов. Практическое введение во фреймворки и контейнеры. http://fetisovvs.blogspot.com/2017/10/java-java.html
    Микросервисы для Java программистов. Практическое введение во фреймворки и контейнеры. (Часть 3). http://fetisovvs.blogspot.com/2017/10/java-3-java.html
    ТОП-3 способа конвертировать массив в ArrayList. Пример на Java. http://fetisovvs.blogspot.com/2016/09/3-arraylist-java-java.html
    Ввод–вывод в Java. http://fetisovvs.blogspot.com/2016/05/java-java_28.html
    Java Challengers #2: Сравнение строк. https://fetisovvs.blogspot.com/2018/11/java-challengers-2-java.html
    Enum-Всемогущий. http://fetisovvs.blogspot.com/2017/02/enum-java.html
    Массивы в Java. Создание и обработка. http://fetisovvs.blogspot.com/2017/10/java-java_18.html
    Arrays, Collections: Алгоритмический минимум. http://fetisovvs.blogspot.com/2017/12/arrays-collections.html
    Популярные методы для работы с Java массивами. http://fetisovvs.blogspot.com/2016/09/java-java_29.html
    Пример использования метода replace в Java. Как заменить символ в строке? http://fetisovvs.blogspot.com/2017/01/replace-java-java.html
    Класс Scanner в Java — описание и пример использования. http://fetisovvs.blogspot.com/2017/01/scanner-java-java.html
    Пример использования метода trim в Java: как удалить пробелы в начале и конце строки? http://fetisovvs.blogspot.com/2017/01/trim-java-java.html
    Spark — Потрясающий веб-микрофреймворк для Java. http://fetisovvs.blogspot.com/2017/10/spark-java-java.html
    Чтение и запись CSV файла с помощью SuperCSV. http://fetisovvs.blogspot.com/2017/01/csv-supercsv-java-java.html
    Конструкция try/catch/finally (исключения). http://fetisovvs.blogspot.com/2017/01/trycatchfinally-java.html
    1000+ часов видео по Java на русском. http://fetisovvs.blogspot.nl/2017/06/1000-java-java.html
    Раздача халявы: нетормозящие треды в Java. Project Loom. http://fetisovvs.blogspot.com/2018/09/java-project-loom-java.html
    Шпаргалка Java программиста 7.1 Типовые задачи: Оптимальный путь преобразования InputStream в строку. http://fetisovvs.blogspot.com/2016/04/java-71-inputstream-java.html
    Шпаргалки Java программиста 10: Lombok. http://fetisovvs.blogspot.nl/2017/12/java-10-lombok-java.html
    Шпаргалки Java программиста 9: Java SE — Шпаргалка для собеседований и повторений. http://fetisovvs.blogspot.com/2017/12/java-9-java-se-java.html
    Шпаргалка Java программиста 8. Библиотеки для работы с Json (Gson, Fastjson,
    LoganSquare, Jackson, JsonPath и другие). http://fetisovvs.blogspot.com/2016/04/java-8-json-gson-fastjson-logansquare.html
    Java 8 и паттерн Стратегия. http://fetisovvs.blogspot.com/2018/03/java-8-java.html
    Java EE Concurency API. http://fetisovvs.blogspot.com/2018/08/java-ee-concurency-api-java.html
    Реализация ООП-наследования в классах, работающих с SQL и MS Entity Framework. http://fetisovvs.blogspot.com/2017/02/sql-ms-entity-framework.html
    Как установить соединение с СУБД MySQL в IntelliJ IDEA в редакции Community. http://fetisovvs.blogspot.com/2016/04/mysql-intellij-idea-community-java.html
    Максимально простой в поддержке способ интеграции java-клиента с java-сервером. http://fetisovvs.blogspot.com/2018/09/java-java-java.html
    Как с помощью maven работать с библиотеками, которых в maven нет. http://fetisovvs.blogspot.com/2017/03/maven-maven-java.html
    Проекты по созданию компиляторов из Java в JavaScript и исполняемые файлы. http://fetisovvs.blogspot.com/2018/01/java-javascript-java.html
    Реактивное программирование с JAX-RS. http://fetisovvs.blogspot.com/2018/09/jax-rs-java.html
    Компактные строки в Java 9. https://fetisovvs.blogspot.com/2018/10/java-9-java.html
    Абстрактный CRUD от репозитория до контроллера: что ещё можно сделать при помощи Spring + Generics. http://fetisovvs.blogspot.com/2018/09/crud-spring-generics-java.html
    Диагностика утечек памяти в Java. http://fetisovvs.blogspot.com/2017/03/java-java_18.html
    Spring AOP и JavaConfig в плагинах для Atlassian Jira. http://fetisovvs.blogspot.com/2018/04/spring-aop-javaconfig-atlassian-jira.html
    Блеск и нищета Java для настольных систем. http://fetisovvs.blogspot.com/2018/04/java-haulmont-java.html
    Разбор задачек от Одноклассников на JPoint 2018. http://fetisovvs.blogspot.com/2018/04/jpoint-2018-java.html
    Программируем… выход из лабиринта. http://fetisovvs.blogspot.com/2015/10/java.html
    Основы работы с IntelliJ IDEA. Интерфейс программы. http://fetisovvs.blogspot.com/2016/09/intellij-idea-java.html
    Ускоряем время сборки и доставки java web приложения. http://fetisovvs.blogspot.com/2018/03/java-web-java.html
    Открытый урок Java Enterprise «CDI in action». http://fetisovvs.blogspot.com/2018/09/java-enterprise-cdi-in-action-java.html
    «Мы все стремимся к сложности, а потом с ней боремся»: интервью с Венкатом Субраманиамом. http://fetisovvs.blogspot.com/2018/09/java_16.html