#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 вы можете исключить производственный код для тестирования и заменить их тестовыми реализациями.
Если никакая дополнительная библиотека или фреймворк не могут быть использованы, я бы предпочел альтернативный подход, потому что он наименее сложный в использовании.