Создание экземпляра CDI и внедрение компонентов родительско-дочернего типа либо неоднозначно, либо дочерние типы не найдены

#java #cdi

#java #cdi

Вопрос:

У меня есть интерфейс, подобный:

 public interface DateTimeService {
    ZonedDateTime now();

    void fixTo(ZonedDateTime date);

    void forget();
}
 

и у меня есть две его реализации. Один для производства, где fixTo и forget генерирует исключения, и один для тестирования, где мы контролируем время. Тогда у меня есть конфигурация CDI, которая в зависимости от флага создает экземпляр правильного типа.

 @ApplicationScoped
public class Configuration {
    @Produces
    @ApplicationScoped
    public DateTimeService dateTimeService(Configuration config) {
        if (config.isFakeDateTimeServiceEnabled()) {
            return new FakeDateTimeService();
        } else {
            return new DefaultDateTimeService();
        }
    }
}
 

Тем не менее, я хотел удалить fixTo и forget из DateTimeService , поскольку они есть только там, чтобы мы могли контролировать время в тестах. Я создал новый интерфейс, подобный:

 public interface FakeDateTimeService extends DateTimeService {
    // fixto and forget is moved from DateTimeService to here
}
 

У меня есть несколько точек внедрения. Некоторые из них находятся в производственном коде, некоторые — в тестовом коде. В производственном коде я хотел бы иметь доступ DateTimeSerice только к тестовому коду, в тестовом коде я хочу иметь возможность управлять расширенным сервисом.

В коде продукта:

     @Inject
    private DateTimeService dateTimeService;
 

В тестовом коде:

     @Inject
    private FakeDateTimeService dateTimeService;
 

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

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

Я пытался наложить вето на создание компонента, но мне это не удалось.

Я думал, что это сработает / должно сработать, так это создать экземпляр правильного типа и программно добавить его в контекст компонента, но примеры, которые я нашел, были для CDI 2.0, в то время как на данный момент соответствующая часть моего кода застряла на CDI 1.2.

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

В противном случае я собираюсь сдаться и просто жить с DateTimeService методами, которые имеют fixTo and forget .

Комментарии:

1. CDI использует типизированное разрешение, и важным моментом здесь являются типы компонентов — ваш FakeDateTimeService компонент как компонент на самом деле имеет типы {FakeDateTimeService, DateTimeService, Object} (и любой другой расширенный класс или реализованный интерфейс). Как таковой, он может быть введен в любую точку ввода этих типов, отсюда и исключение неоднозначного разрешения. Если я правильно понял ваше намерение, то вы можете ознакомиться с альтернативами CDI и FakeDateTimeService использовать их в качестве включенной альтернативы для тестов, следовательно, заменяя DateTimeService во всех его точках внедрения в тестах.

2. Другой подход может заключаться в том, чтобы иметь расширение CDI и наложить вето на исходный тип и зарегистрировать новый компонент. С CDI 1.2 это было бы несколько сложнее, и, тем не менее, расширение должно быть установлено только для тестов. Я бы предпочел поискать способ заставить это работать с альтернативами.

3. Обязательно выбирайте альтернативы (или, возможно, специализатор), которые видны только в пути к тестовому классу. Наличие переключателей, которые активируют фиктивный компонент в рабочей среде, пугает меня, потому что ошибка может привести к нестабильной или скомпрометированной работе. Если тестовый код, на который вы ссылаетесь, представляет собой только модульные тесты, вы можете даже просто издеваться над DateTimeService .

4. спасибо, ребята. Я понял, почему возникла двусмысленность, я просто не знал, куда идти дальше. Я рассмотрю оба предложенных решения. Хорошего дня!

5. Возможно, вам захочется взглянуть на ioc-unit . Определяя отдельный тестовый код и продуктивный (sut) код, очень легко неявно определить приоритет FakeDateTimeService без альтернатив

Ответ №1:

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

С альтернативами

Для тестирования вы предоставляете собственный beans.xml , который определяет альтернативу для тестирования, которая заменяет производственную. Вам нужно будет синхронизировать их, в результате чего тест beans.xml содержит изменения, необходимые для тестирования. В зависимости от реализации CDI beans.xml можно не учитывать, и @Alternative будет достаточно.

https://docs.oracle.com/javaee/6/tutorial/doc/gjsdf.html

 interface DateTimeService  { ... }

// Active during production, beans.xml does not define alternative
class DefaultDateTimeService implements DateTimeService  { ... }

// Active during test, beans.xml defines alternative
@Alternative
class FakeDateTimeService implements DateTimeService { ... }

// Injection point stays the same.
@Inject
DateTimeService dateTimeService;

// Used in test environment
<beans ... >
    <alternatives>
        <class>FakeDateTimeService</class>
    </alternatives>
</beans>
 

С помощью Arquillian

С помощью Aarquillian вы можете самостоятельно собрать развертывание и исключить реализацию DefaultDateTimeService и заменить ее реализацией FakeDateTimeService. При таком подходе вам не нужно ничего взламывать, потому что во время тестирования в контейнере CDI будет видна только реализация FakeDateTimeService.

http://arquillian.org/guides/getting_started/

С расширением CDI

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

 package test.extension

public class VetoExtension implements Extension {

    public <T> void disableBeans(@Observes ProcessAnnotatedType<T> pat) {
        // type matching
        if (DefaultDateTimeService.class.isAssignableFrom(pat.getAnnotatedType().getJavaClass())) {
            pat.veto();
        }
    }
}

// Define SPI provider file, assuming test/resources is on CDI classpath
src/test/resources/META-INF/services/javax.enterprise.inject.spi.Extension
// Single line you put in there
test.extension.VetoExtension 
 

Deltaspike @Exclude

Deltaspike — это библиотека расширений CDI, которая предоставляет некоторые полезные утилиты для разработки и тестирования CDI, также предоставляется CDITestRunner. Аннотация @Exclude используется расширением, которое выполняет то же самое, что и в примере выше, но уже реализовано для вас.

https://deltaspike.apache.org/

https://deltaspike.apache.org/documentation/test-control.html

https://deltaspike.apache.org/documentation/projectstage.html

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

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

С помощью Deltaspike вы можете исключить производственный код для тестирования и заменить их тестовыми реализациями.

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