Модульное тестирование сервлета JAX-RS/Jersey с инъекциями Guice
У меня есть приложение, которое использует Jersey/JAX-RS для веб-сервисов (аннотации и т. Д.) И Guice для внедрения реализаций сервисов. Мне не очень нравится, как Guice работает напрямую с сервлетами, я предпочитаю Джерси, поэтому мне пришлось немного поработать, чтобы заставить сервисные инъекции работать, так как Guice не создавал мои классы сервлетов, и я не стал не хочу иметь дело с мостом HK2-Guice. Я сделал это, создав класс слушателя (называемый Configuration), который устанавливает инжекторы в статических полях при запуске приложения, а затем вручную вводил инъекции в каждый класс сервлета, создав родительский класс, который расширяет все мои сервлеты конструктором, который содержит следующие:
public MasterServlet() {
// in order for the Guice @Inject annotation to work, we have to create a constructor
// like this and call injectMembers(this) on all our injectors in it
Configuration.getMyServiceInjector().injectMembers(this);
Configuration.getDriverInjector().injectMembers(this);
}
Я знаю, что это довольно странно, но в моих сервлетах это прекрасно работает. Я могу использовать аннотации Guice @Inject на своих сервисах и переключаться между именованными реализациями и так далее. Проблема возникает, когда я иду, чтобы настроить свои модульные тесты. Я использую JerseyTest для выполнения своих тестов, но выполнение теста для моих сервлетов приводит к ошибке 500, когда Guice говорит следующее:
com.google.inject.ConfigurationException: ошибки конфигурации Guice: 1) Не привязана реализация для com.mycompany.MyService. при поиске com.mycompany.MyService для поля в com.mycompany.servlet.TestGetServlet.service(TestGetServlet.java:21) при поиске com.mycompany.servlet.TestGetServlet
Тест выглядит так:
public class TestServletTest extends JerseyTest {
@Test
public void testServletFunctional() {
final String response = target("/testget").request().get(String.class);
assertEquals("get servlet functional", response);
}
@Before
public void setup() {
Configuration configuration = new Configuration();
configuration.contextInitialized(null);
}
@Override
protected Application configure() {
return new ResourceConfig(TestGetServlet.class);
}
}
Вы заметите, что в методе установки я вручную создаю свой класс конфигурации, так как я не могу положиться на тестовый контейнер (Grizzly) для его создания (я получаю исключения NullPointerExceptions без этих двух строк). Подробнее об этом ниже.
И вот тестируемый сервлет:
@Path("/testget")
public class TestGetServlet extends MasterServlet {
@Inject
MyService service;
@GET
@Produces({"text/plain", MediaType.TEXT_PLAIN})
public String testGet() {
//service = Configuration.getServiceInjector().getInstance(MyService.class);
return "get servlet functional";
}
}
Обратите внимание на закомментированную строку в методе testGet()? Если я сделаю это вместо этого и удалю аннотацию @Inject, указанную выше, все будет работать нормально, что указывает на то, что Grizzly не создает мои сервлеты так, как я ожидаю.
Я думаю, что Гризли не знает о Гисе. Кажется, все говорит о том, что Grizzly не видит класс Configuration, несмотря на тот факт, что, поместив его в метод @Before моего теста, он, по крайней мере, будет доступен классам, которые его используют (см.: закомментированная строка в классе TestGetServlet). Я просто не знаю, как это исправить.
1 ответ
Я все еще пытаюсь понять это, но тем временем я переключился с Guice на HK2, что потребовало немного времени, но я подумал, что это может быть полезно для любого, кто столкнется с этой проблемой в будущем.
Я считаю это ответом, потому что, честно говоря, моя попытка обойти мост Guice-HK2, но все же использовать Guice с Джерси, возможно, не была лучшей идеей.
Переключение с Guice на HK2 занимает немного времени, и нет исчерпывающего руководства со всеми ответами. Например, зависимости действительно суетливые. Если вы попытаетесь использовать Джерси 2.27, вы можете столкнуться с известным
java.lang.IllegalStateException: InjectionManagerFactory не найден
ошибка. Джерси 2.27 не имеет обратной совместимости с предыдущими версиями из-за самого HK2. Я все еще работаю над тем, чтобы заставить все это работать, но в то же время мне пришлось понизить все мои зависимости Джерси до 2.26-b06, чтобы HK2 работал должным образом.
К счастью, в Джерси уже реализован набор шаблонов HK2, поэтому все, что вам нужно для работы с инъекциями, - это правильное использование @Contract, @Service (см. Документацию по HK2), а затем два новых класса, которые выглядят так:
public class MyHK2Binder extends AbstractBinder {
@Override
protected void configure() {
// my service here is a singleton, yours might not be, so just omit the call to in()
// also, the order here is switched from Guice! very subtle!
bind(MyServiceImpl.class).to(MyService.class).in(Singleton.class);
}
}
И это:
public class MyResourceConfig extends ResourceConfig {
public MyResourceConfig() {
register(new MyHK2Binder());
packages(true, "com.mycompany");
}
}
Достаточно просто, но это работает только для самого приложения. Тестовый контейнер ничего не знает об этом, поэтому вам нужно самостоятельно переделать Binder и ResourceConfig в своем тестовом классе, например так:
public class TestServletTest extends JerseyTest {
@Test
public void testServletFunctional() {
final String response = target("/testget").request().get(String.class);
assertEquals("get servlet functional", response);
}
@Before
public void setup() {
}
@Override
protected Application configure() {
return new TestServletBinder(TestGetServlet.class);
}
public class TestServletBinder extends ResourceConfig {
public TestServletBinder(Class registeree) {
super(registeree);
register(new MyHK2Binder());
packages(true, "com.mycompany");
}
}
}
Необходимость сделать это на самом деле хорошо, потому что вы можете вместо этого связать Binder для тестового Binder, в котором вы вместо этого связали свой сервис с поддельным сервисом или чем-то еще. Я не сделал этого здесь, но это достаточно легко сделать: замените новый MyHK2Binder() в вызове register() на тот, который вместо этого выполняет привязку следующим образом:
bind(MyTestServiceImpl.class).to(MyService.class).in(Singleton.class);
И вуаля. Очень хорошо. Очевидно, вы могли бы достичь аналогичного результата с помощью именованных привязок, но это прекрасно работает и может быть даже проще и понятнее.
Надеюсь, это поможет кому-то сэкономить часы, которые я потратил, чтобы все заработало.