Модульное тестирование сервлета 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);

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

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

Другие вопросы по тегам