Pull to refresh

Comments 53

Вот прям то что надо. Автору респект
Внедрение зависимостей — это специальный паттерн, который уменьшает связь между Spring компонентами (то есть между различными POJO).

Я не думаю, что Spring компоненты являются POJO. И насколько мне известно, Spring IOC работает только между бинами, иначе говоря нельзя заинжектить что-то в обычный POJO если он не является бином.
Ну, по факту это обычный класс с аннотациями. Я думаю тут написано это для того чтобы показать что бины — это почти обычные классы. Но возможно лучше удалить эту строку.
Бины могут быть обычными классами.
Т.е. можно создать бин без аннотаций.
Любой POJO может быть бином, но не любой POJO является бином.
Можно создать бин из любого POJO класса, потом его передавать (инжектить) в любой бин.
Для того чтобы инжекция была «прозрачной», то рекомендуется делать её через конструктор.
На крайний случай через метод, но никогда не через поле.
Любой неабстрактный класс с конструктором, который Spring может зарезолвить из контекста можно сделать бином. Другое дело что далеко не всегда так поступать можно, равно как не стоит из POJO делать бин. Это антипаттерн. POJO это контейнер данных. Бин это stateful/stateless объект представляющий наружу некоторую функциональность. Какую функциональность может предоставить POJO? Насчет инъекций через конструктор тоже спорное утверждение. Да, так проще мокать в тестах но цена за это — усложнение графа инициализации бинов. И сидеть решать циклическую зависимость может быть очень невесело. На практике инъекции через поля могут быть допустимы, тем более что наружу не будут торчать кишки сеттеров, вообще не относящихся к бизнес-логике бина.
Как минимум бин настроек может быть POJO :-)

Насчет инжекции через поля
1) Нельзя написать unit-test
2) Сильная привязка к контейнеру бинов

Т.о. наша бизнес-логика может быть приколочена к фреймворку который мы используем.

Это как бы моветон.

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

Отсюда вытекает контраргумент к первому пункту. Инъекции всегда надо отдавать на откуп Спрингу. Начиная со Spring Boot 2.1 ввели очень правильные ограничения на переопределение бинов и т.п. Сейчас юнит-тесты делаются через @ContextConfiguration/@SpringBootTest (в зависимости от желаемого скоупа) и nested static @TestConfiguration класс. Где внутри определяются реализации бинов на замену. Таким образом прекрасно тестируются классы с инъекцией через поля.

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

К чему приводит привязка к фреймворку я вижу сейчас.
Когда то было принято решение использовать «стильно-модно-молодежный» jBoss Seam, теперь чтобы «спрыгнуть» лет 5 уже умершего фреймворка, надо будет затратить не понятно сколько времени.
Я вообще избавился от тестов со спринговым контекстом, и тестирую свои бины так, словно это обычные классы. Вроде пока все идет хорошо :)
Рано или поздно вы столкнетесь с ситуацией что все тесты зеленые а приложение не стартует (к примеру из-за циклических зависимостей). Или еще хуже, стартует на девелопменте а на продакшене нет (реальный случай между прочим)
Ну интеграционные тесты никто не отменял, они тоже нужны.
Как минимум простой тест на поднятие контекста.
Грубо говоря unit-тестами мы проверяем, что логика(алгоритм) правильный.
И если на входе корректные данные, то на выходе получаем корректные данные.
Плюс можем оттестировать поведение при не корректных данных.

Интеграционные тесты показывают как «кубики» работают в связке.

Скажем так разные инструменты для разных задач.
Рано или поздно вы столкнетесь с ситуацией что все тесты зеленые а приложение не стартует (к примеру из-за циклических зависимостей).

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

Или еще хуже, стартует на девелопменте а на продакшене нет (реальный случай между прочим)

А какие гарантии тут даст тестирование? Оно только добавит вариантов — на девеломенте и в тестах зеленое, в проде — красное. Или в проде и на девелопменте зеленое, но тесты красные. Тоже реальный случай, если что.
Отлично, только в этом случае вы тестируете не пойми что. Жизненный цикл объекта в JUnit тестах у вас принципиально отличается от того что потом крутится в продакшене. А если еще усугубить это моками то получаются две параллельные вселенные. Если у вас есть бин ну и тестируйте его как бин. Если это не бин (нет никакого Autowiring) то и спорить не о чем.
Не соглашусь. Если в ваш бин внедряется некий интерфейс, то никакой параллельной вселенной не будет — вы тестируете поведение своего бина, а не внедренных зависимостей, а ваш бин создался корректно, получив инжект зависимостей в конструктор, с той лишь разницей, что экземпляры этих зависимостей создал не спринг, а макита.
Отлично, в вашем бине есть @PostConstruct. Ваши действия? Эмулировать поведение Спринга? Или @PostConstruct от лукавого и его на равне с @PreDestroy лучше не использовать? И Value аннотации вешать на методы а не на поля. Ну чтоб уж вообще по-хардкору.
Отлично, в вашем бине есть @PostConstruct. Ваши действия? Эмулировать поведение Спринга?

Вариант дернуть метод помеченный @PostConstruct прямо в ходе теста вами не рассматривается? Это все такой же метод, который можно и нужно протестировать, а не рассчитывать, что Спринг его просто вызовет. Ну, то есть @PostConstruct помогает вам вызвать метод в определенный момент времени, но никак не гарантирует, что он отработает правильно, так что тестировать его все равно придется, а раз так то в чем проблема его вызвать? Разве что эстетические чувства страдают.

на равне с @PreDestroy лучше не использовать?

Ни разу не был нужен — признаюсь, как на духу! В условиях, когда сервис могут снести по kill -9 как-то не пригодился.

И Value аннотации вешать на методы а не на поля

Value аннотации у меня не уходят дальше конфигурации. А в последнее время я от них и вовсе отказался, слишком не гибкий инструмент.
То есть вы за эмуляцию жизненного цикла спринга «в меру своего понимания». Ну ок, ваш выбор.
Вообще-то по-моему очевидно, что я за тестирование своего кода, а не того, как спринг работает.
Вариант дернуть метод помеченный @PostConstruct прямо в ходе теста вами не рассматривается?

Ваше же предложение
Это тоже мое предложение:
Это все такой же метод, который можно и нужно протестировать
Э-э-э зачем мне тестировать ВСЕ приложение, когда мне нужно протестировать только логику работы какого-то класса/метода?!
Для этого unit-тесты само то.
Понятно, что они не покрывают всей функциональности.
Но их писать легче и они дают определенную уверенность, что при внесении изменений что-то где-то не отвалиться.

Интеграционные тесты это совсем другая история.
Причем в основном грустная, т.к. при любом изменении они будут «падать».
Т.к. тестируется не маленькая часть чего-то, а практически все приложение.
А кто писал про ВСЕ приложение? То что можно управлять скопом это новость?

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

Эту «опасность» выявляет первый же запуск приложения — контекст просто не взлетит. Пути решения, вроде, тоже хорошо известны — либо Lazy, либо инжектить как-то иначе. Преимущества работы тестов через Спринг все еще не очевидны.
Основное преимущество — стандартизация подхода. Если это бин, тестим через Спринг контекст. Если не бин — создаем вручну. Логика проста и понятна даже джуниору. Этот подход позволяет сократить стоимость владение, когда два внешне одинаковых класса тестируются по-разному. Спринг контекс отработает все постпроцессоры и даст наглядную картину как оно будет выглядеть в продакшене. Если вы создаете бин вручную и вручную же эмулируете жизненный цикл Спринга то у меня для вас плохие новости…
Как раз основная идея бинов в Spring'е было то, что они ничего не знают о Spring'е.
Как минимум, когда вводили конфигурацию на основе xml.
Потом ввели конфигурацию на основе аннотации и граница была стерта. Бины узнали, что есть Spring.
...
@Autowired
private ApplicationContext applicationContext;
...

Как вершина этого подхода.
Потом сделали конфигурацию на основе классов.
Что позволило опять скрыть от бина существование Spring'а.

В момент когда у вас появляется lifecycle вы уже не можете его игнорировать. Бины в Спринге подчиняются жизненному циклу хотите вы этого или нет. С этого момента у вас по-сути три варианта — 1. писать бин так чтобы жизненный цикл было можно игнорировать (тем самым отказываясь от использования части функциональности спринга), 2. тестировать используя Спринг контекст и делегируя ему нативное управление жизненным циклом либо 3. эмулировать жизненный цикл вручную
Э-э-э мухи отдельно, котлеты отдельно. Жизненный цикл бина это игрушки Spring'а и они меня мало интересуют в рамках бизнес-логики.
Основная задача unit-тестов бинов протестировать, что логика в классе правильная.
Сам бин о своем жизненном цикле знать ничего не должен.
Очень желательно, чтобы он и о Spring ничего не знал.

По хорошему, все описание жизненного цикла бина должно быть в кнофигурационном/ых классе/ах.
Спасибо за «плохие новости», однако все работает — и с постпроцессорами, и с тестированием через «ручное создание». Можете даже еще один минус поставить, возможно вам станет от этого легче.

Кстати, у меня вопрос — нынче тестовый контекст взлетает, когда собираешь не толстый готовый к деплою джарник, а всего-то библиотеку? Пару лет назад, когда выделял общую логику в отдельный джар этот самый тестовый контекст напрочь отказывался взлетать, то ли нервничал, что @SpringBootApplication найти не мог, то ли еще что… А ведь основная масса постпроцессоров именно туда и ушла. Так я от тестового контекста и отказался.
Можно управлять и скопом.
Например скоуп работы с БД, это уже больше 80% приложения.
Остатки, это на http-контроллеры и интеграции со внешними системами.
А если поднимать веб-контекст — то это просто все приложение.

Насчет «циклических зависимостей».
Так в конструктор инжектится интерфейс.
И если создавать бины типа интерфейса, то Spring их замечательно проксирует и никаких проблем с циклическими зависимостями.
Если не секрет то как вы пишете JUnit тесты для БД?
Э-э-э зачем?

Для БД приходиться писать Spring-овые тесты с поднятием контекста БД.

Т.к. в основном используется Spring Data Jpa.
То они нужны, чтобы правильно создать интерфейс для Repository. Ну или протестить Query и/или Query(native=true)
Юнит-тесты — для БД? Простите, а что они должны тестировать???
вот ты придумал сложный запрос с кучей джойнами и агрегаторными функциями. Вот такое и желательно затестировать. Так в впринципе можно разработку запроса вести

Ведь рано или поздно в твой код придет Антон, и ченить попробует в запросе поменять
Э-э-э как бы тестирование запросов это не задача unit-тестирования. Это немного другая задача.
Если действительно «сложный запрос», то для его тестирования нужно
1) Соответствующая БД (заменители типа H2 или Derby не подойдут)
2) Соответствующая структура БД (таблицы, ключи, внешние ключи, триггеры, ограничения и пр)
3) Соответствующие данные (Т.к. в зависимости от данных запрос легко может выдавать все что угодно)

Т.о. для тестирования запроса надо
1) Поднять инстанс БД
2) Накатить структуру БД
3) Накатить данные в БД

Чтобы это сделать быстро и безболезненно, скорее всего, нужен Docker.
В котором поднимается БД и на нее накатывается соответствующий дамп.
И уже к БД в докере делается запрос и проверяются результаты.

Вот это все как-то на unit-тест не похоже ни разу. :-)
Все так, просто этому не место в юнит-тестах. Это вообще слой БД, так что есть вариант запихнуть это все в хранимую процедуру, а из приложения дергать уже ее. С другой стороны в чем-то я понимаю причины возникновения юнит-тестов, которые тестируют запросы — народ просто не знает где и как их еще писать, сами БД это дело никак не поддерживают и не поощряют, вот и оседает тестирование БД в юниттестах.
Насчет «циклических зависимостей».
Так в конструктор инжектится интерфейс.
И если создавать бины типа интерфейса, то Spring их замечательно проксирует и никаких проблем с циклическими зависимостями.

Это либо не так, либо не совсем так — намедни мне удалось создать такую закольцованную структуру. Почесал голову и начал думать, где у меня, собственно, дырка в логике, если получилась такая петля и кто на ком стоял.
Вообще странно.
Насколько я учил.
Проблема с циклическая зависимостью может быть, только если инжектяться классы.
Когда работают через инжектиться все через интерфейсы, то в начале создается прокси, потом он разруливает по конкретным реализациям.

Сам недавно на такое натолкнулся.
Вместо интерфейса указал класс.
Ну, вот набросал, получается.
interface IA {

}

interface IB {

}

@Component
public class IAImpl implements IA {

    public IAImpl(@Autowired IB ib) {
    }
}


@Component
public class IBImpl implements IB {

    public IBImpl(@Autowired IA ia) {
    }
}


При запуске выдает:
***************************
APPLICATION FAILED TO START
***************************

Description:

The dependencies of some of the beans in the application context form a cycle:

┌─────┐
| IAImpl defined in file [G:\.....\IAImpl.class]
↑ ↓
| IBImpl defined in file [G:\.....\IBImpl.class]
└─────┘

А можно и так
A:
public interface IA {
    public void testA();
}
...
public class IAImpl implements IA {
    private IB ib;
    IAImpl(IB ib) {
        this.ib = ib;
    }
    public void testA() {
        System.out.println("TEST A");
        ib.testB();
    }
}

B:
public interface IB {
    public void testB();
}
public class IBImpl implements IB {
    private IA ia;
    IBImpl(IA ia) {
        this.ia = ia;
    }
    public void testB() {
        Random random = new Random();
        if(random.nextBoolean()) {
            System.out.println("TEST B");
        } else {
            ia.testA();
        }
    }
}


А вся «грязь» будет здесь:
@SpringBootApplication
public class Main {
    @Bean
    public IA iaBean(@Lazy IB ibBean) {
        return new IAImpl(ibBean);
    }
    @Bean
    public IB ibBean(IA iaBean) {
        return new IBImpl(iaBean);
    }
    @Bean
    public CommandLineRunner commandLineRunner() {
        return new CommandLineRunner() {
            @Autowired
            private IA ia;
            @Autowired
            private IB ib;
            @Override
            public void run(String... args) throws Exception {
                ia.testA();
                System.out.println("--------------");
                ib.testB();
            }
        };
    }
    public static void main(String[] args) {
        SpringApplication.run(Main.class, args);
    }
}


Т.о. ни IAImpl не знает о реализации IB, ни наоборот IBImpl не знает о реализации IA. Ни оба вместе не знают об Spring'е.
Все знание о поднятие контекста и разрешения зависимостей находиться в конфигурационном файле. Где проблемы «циклических» зависимостей решаются средствами Spring'а
Фактически ваше решение — добавление Lazy в конструктор, а оно сработает и в моем примере:
public IAImpl(@Lazy @Autowired IB ib) {
    }


Выносить это в конфигурацию или нет — вопрос открытый, тут от предпочтений. В данном случае тут по-моему вообще стоит подумать почему возникла кольцевая зависимость? Это ж где-то дырка в логике построения самого приложения, а Lazy просто костыль, который позволяет как-то ехать и не падать на старте.
Скажем так. Проблемы поднятия контекста, когда он локализован только в конфигурационных файлах (классы или xml) видны более явно, чем если бы конфигурация была бы размазана тонким слоем по всему приложению.
И тут уже в зависимости от задачи каждый сам решает как решать данную проблему.
Либо меняя имплементацию сервисов. Либо изменяя правила поднятия контекста.
Вроде все знаю, а повторить все равно приятно.

Это прям можно использовать как критерий правильности фреймворка)

А на сколько ценится такая сертификация не в контексте саморазвития, но у работадателей?

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

По факту при собеседовании если показываешь свои знания, то работодателю глубоко наплевать на твое образование/сертификаты
Ну, справедливости ради, — до 2017 года их можно было сдавать в России. Через любого партнера Pearson VUE, коих хватает.
Pearson VUE is no longer delivering exams for Pivotal. Find more information about Pivotal certification.
Last updated 2017-07-13
Спасибо за перевод!
Весьма полезный материал
CGLib proxy — не встроен в JDK. Используется когда интерфейс объекта недоступен

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

Почему для создания Spring beans рекомендуются интерфейсы?
тут анологично бы добавил, что если написать свой бин пост процессор (или при подключении внешней библиотеки), где будет использоваться стандартная прокся jdk, то если инжекция идет на класс, то все нафиг ломается… Решение — перейти на инжекцию по интерфейсу или использовать cglib

Что такое профили? Какие у них причины использования?
Profile("!test") — я бы такой пример добавил, загружать со всеми прифилями, кроме теста

По поводу скоупов и многопоточности некорректно.

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

Sign up to leave a comment.

Articles