Search code examples
javaspringintegration-testingjersey-2.0jersey-test-framework

Integration test of Jersey web services using Spring for dependency injection fails to initialize @Context HttpServletRequest


I have jersey web services that uses spring for dependency injection (spring-jersey module) and hibernate for object-relational mapping (ORM). In order to develop integration tests taking into account the following conditions:

  1. Intiailize the test container only once for entire test class
  2. Register custom listeners, filters, servlets etc to the test container
  3. Make sure that @Context HttpServletRequest is not null

According to this https://java.net/jira/browse/JERSEY-2412 Jersey project JIRA task HttpServletRequest is null and that's as shown in the resolution of the task Works as desgined. When running integration tests on Grizzly container, it runs the integration tests on http server hence, any dependence on servlet-based features such as HttpServletRequest, HttpServletResponse, etc. is not available.

There seems to be no standard solution on how to address this issue, and Jersey community is obviously open to such features being developed by contributors as stated in this https://java.net/jira/browse/JERSEY-2417 JIRA ticket. Until this feature is implemented, what are the possible workaround solutions? Based on my research I come cross a few posts that state:

  1. Use external container (What if I don't want too?)
  2. Use jersey's jetty module (What if I don't want to use Jetty?)
  3. SpringMVC specific solutions that do not apply to this project (for we don't use Spring MVC)

So, what's the best solution to successfully run integration tests on jersey-based web servers that uses spring-jersey bridge for dependency injection and rely on Servlet-based features?


Solution

  • This is standard behaviour from Jersey and the to allow servlet-based features such as HttpServletRequest is not available yet. Based on my research, I could achieve the following conditions

    1. Intiailize the test container only once for entire test class
    2. Register custom listeners, filters, servlets etc to the test container
    3. Make sure that @Context HttpServletRequest is not null

    By starting/stopping A grizzly container manually and deploying the grizzly container instance on a custom jersey-based WebappContext. The steps are as follow in case anyone else come across such issue

    1. Create a HttpServer
    2. In @Before create a custom WebappContext which reflects your web.xml and deploy your HttpServer instance using the WebappContext
    3. In order to make sure that Servlet-based features such as HttpServletRequest are available during integration test use the ServletRegistration to load your application inside A Servlet container as shown below

    Step #1

    @RunWith(SpringJUnit4ClassRunner.class)
    @ContextConfiguration(locations = { "classpath:applicationContext.xml" })
    public class ResourceEndpointIntegrationTest{
        @Context
        private HttpServletRequest httpReq;
    
        private static Logger logger = Logger.getLogger(ResourceEndpointIntegrationTest.class);
    
        public static final String BASE_URI = "http://localhost:8989/";
        private static HttpServer server = null;
    
        @BeforeClass
        public static void initTest() {
            RestAssured.baseURI = "http://localhost:8989/";
        }
    ...
    }
    

    Use SpringJUnit4ClassRunner.class and @ContextConfiguration to load the applicationContext.xml for tests. Also declare @Context HttpServletRequest and create an instance of HttpServer to be used later. I use @BeforeClass here for Rest-Assured specific purposes (you don't have to use it), its optional.

    Step #2

    @Before
        public void setup() throws Exception {
            if (server == null) {
                System.out.println("Initializing an instance of Grizzly Container ...");
                final ResourceConfig rc = new ResourceConfig(ResourceEndpointIntegrationTest.class, ..., ..., ...); //update
    
                WebappContext ctx = new WebappContext("IntegrationTestContext");
                            //register your listeners from web.xml in here
                ctx.addListener("com.xxx.yyy.XEndpointServletContextListener");
                            //register your applicationContext.xml here
                ctx.addContextInitParameter("contextConfigLocation", "classpath:applicationContext.xml");
    
                            //ServletRegistration is needed to load the ResourceConfig rc inside ServletContainer or you will have no 
                            //Servlet-based features available 
                ServletRegistration registration = ctx.addServlet("ServletContainer",
                        new ServletContainer(rc));
    
                            //Initialize the Grizzly server passing it base URL
                server = GrizzlyHttpServerFactory.createHttpServer(URI.create(BASE_URI));
    
                            //Deploy the server using our custom context
                ctx.deploy(server);
            }
        }
    

    The setup in @Before is run for every test but, the if condition will force the setup method to act like @BeforeClass allowing us to initialize the server once for the entire test class minus the static nature of @BeforeClass.

    The reason I initialize the server once for all tests inside a test class is because, if we don't then we will have the following workflow

    1. A server is started
    2. Spring starts scanning for beans
    3. Spring does autowired
    4. Spring setups sessionFactory, etc.
    5. The test is run
    6. Spring destroys the context
    7. Server is shutdown
    8. Repeat step #1 to #7 for every test

    Above is very time consuming and technically not feasible, hence initialize container once (which is why I don't extend JerseyTest because I want to control the startup/shutdown of the test container).

    #Step 3

    @AfterClass
        public static void tearDown() throws Exception {
            System.out.println("Integration tests completed. Tear down the server ...");
            if (server != null && server.isStarted()) {
                server.shutdownNow();
                System.out.println("Grizzly instance shutdown completed");
            }
        }
    

    In step 3 we use @AfterClass to shutdown the grizzly instance used for integration testing purposes.

    And a sample test looks as follow

    @Test
        public void testCreateSomethingReturnSuccessfully() {
            JSONObject something = new JSONObject();
            cust.put("name", "integrationTest");
            cust.put("age", 33);
    
            given().
                contentType(ContentType.JSON).
                body(something.toString()).post("/someEndpoint").
            then().
                statusCode(200).
            assertThat().
                body("id", greaterThan(0)).
                body("name", equalTo("integrationTest")).
                body("age", equalTo(33));
        }
    

    (Gradle) Some of the relevent depdendencies

    compile group: 'org.glassfish.jersey.containers', name: 'jersey-container-servlet', version: '2.23.2'
    compile group: 'org.glassfish.jersey.test-framework.providers', name:'jersey-test-framework-provider-grizzly2', version:'2.23.2'
    compile group: 'org.springframework', name:'spring-test', version:'4.3.2.RELEASE'
    compile group: 'io.rest-assured', name:'rest-assured', version:'3.0.1'
    
    
    // Spring
        compile group: 'org.springframework', name: 'spring-core', version: '4.3.2.RELEASE'
        compile group: 'org.springframework', name: 'spring-beans', version: '4.3.2.RELEASE'
        compile group: 'org.springframework', name: 'spring-web', version: '4.3.2.RELEASE'
        compile group: 'org.springframework', name: 'spring-jdbc', version: '4.3.2.RELEASE'
        compile group: 'org.springframework', name: 'spring-orm', version: '4.3.2.RELEASE'
    
        // Jersey-Spring bridge
        compile (group: 'org.glassfish.jersey.ext', name: 'jersey-spring3', version: '2.23.2'){
            exclude group: 'org.springframework', module: 'spring-core'
            exclude group: 'org.springframework', module: 'spring-web'
            exclude group: 'org.springframework', module: 'spring-beans'
            exclude group: 'org.springframework', module: 'spring-jdbc'
            exclude group: 'org.springframework', module: 'spring-orm'
        }
    

    Some of the imports

    import javax.servlet.http.HttpServletRequest;
    import javax.ws.rs.core.Context;
    
    import org.glassfish.grizzly.http.server.HttpServer;
    import org.glassfish.grizzly.servlet.ServletRegistration;
    import org.glassfish.grizzly.servlet.WebappContext;
    import org.glassfish.jersey.grizzly2.httpserver.GrizzlyHttpServerFactory;
    import org.glassfish.jersey.server.ResourceConfig;
    import org.glassfish.jersey.servlet.ServletContainer;
    
    import org.springframework.test.context.ContextConfiguration;
    import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;