Sunday, May 06, 2012

Jersey Tests with Embedded Jetty and Spring

One of the bittersweet things about using Java is that there's a library for everything.   The good is that if you need to do something, there's a library.  The bad is that the documentation is typically terrible, and it can take hours (or days!) to figure out how the hell to make something work.  Today's lesson in this is testing Jersey services when you're deploying on Embedded Jetty and Spring.

The problem of using this combo is that we don't use web.xml for configuring things, it's all done with Spring.  And I want to test with InMemoryTestContainer.  So how is it done?

Let's say you have this service:


1    package com.trimbo.web.api; 
2     
3    import freemarker.template.Configuration; 
4    import freemarker.template.DefaultObjectWrapper; 
5    import freemarker.template.Template; 
6    import freemarker.template.TemplateException; 
7    import org.springframework.context.annotation.Scope; 
8    import org.springframework.stereotype.Component; 
9     
10   import javax.ws.rs.GET; 
11   import javax.ws.rs.Path; 
12   import javax.ws.rs.Produces; 
13   import javax.ws.rs.core.MediaType; 
14   import java.io.IOException; 
15   import java.io.StringWriter; 
16    
17   @Path("/jobs") 
18   @Component 
19   @Scope("prototype") 
20   public class TestService extends JerseySpringServlet { 
21       @GET 
22       @Path("/") 
23       @Produces(MediaType.APPLICATION_JSON) 
24       public String index() { 
25           return("{ 'a': 'test' }"); 
26       } 
27   }  

Then you have this configuration in Spring
1    xml version="1.0" encoding="UTF-8"?> 
2    <beans xmlns="http://www.springframework.org/schema/beans" 
3           xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 
4           xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd"> 
5        <beans xmlns="http://www.springframework.org/schema/beans" 
6               xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 
7               xmlns:context="http://www.springframework.org/schema/context" 
8               xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd"> 
9            <context:component-scan base-package="com.trimbo.web.api" /> 
10           <bean id="JettyServer" class="org.mortbay.jetty.Server" 
11                 init-method="start" 
12                 destroy-method="stop"> 
13               <property name="connectors"> 
14                   <list> 
15                       <bean id="connector" class="org.mortbay.jetty.nio.SelectChannelConnector"> 
16                           <property name="port" value="8050"/> 
17                           <property name="maxIdleTime" value="30000"/> 
18                           <property name="acceptors" value="10"/> 
19                       bean> 
20                   list> 
21               property> 
22               <property name="handlers"> 
23                   <list> 
24                       <bean class="org.mortbay.jetty.servlet.Context"> 
25                           <property name="contextPath" value="/"/> 
26                           <property name="servletHandler"> 
27                               <bean class="org.mortbay.jetty.servlet.ServletHandler"> 
28                                   <property name="servlets"> 
29                                       <list> 
30                                           <bean class="org.mortbay.jetty.servlet.ServletHolder"> 
31                                               <property name="servlet"> 
32                                                   <bean class="com.trimbo.web.api.JerseySpringServlet" /> 
33                                               property> 
34                                               <property name="name" value="jersey_api"/> 
35                                           bean> 
36                                       list> 
37                                   property> 
38                                   <property name="servletMappings"> 
39                                       <list> 
40                                           <bean class="org.mortbay.jetty.servlet.ServletMapping"> 
41                                               <property name="pathSpec" value="/api/*"/> 
42                                               <property name="servletName" value="jersey_api"/> 
43                                           bean> 
44                                       list> 
45                                   property> 
46                               bean> 
47                           property> 
48                       bean> 
49                       <bean class="org.mortbay.jetty.handler.DefaultHandler" /> 
50                       <bean class="org.mortbay.jetty.handler.RequestLogHandler" /> 
51                   list> 
52               property> 
53           bean> 
54       beans> 
55   beans>

And here is your glue class for that JerseySpringServlet, which injects the App context.
1    package com.trimbo.web.api; 
2     
3    import com.sun.jersey.spi.spring.container.servlet.SpringServlet; 
4    import org.springframework.beans.factory.annotation.Autowired; 
5    import org.springframework.context.ApplicationContext; 
6    import org.springframework.context.ConfigurableApplicationContext; 
7     
8    public class JerseySpringServlet extends SpringServlet 
9    { 
10       @Autowired 
11       private ApplicationContext applicationContext; 
12    
13       @Override 
14       protected ConfigurableApplicationContext getContext() 
15       { 
16           return (ConfigurableApplicationContext) this.applicationContext; 
17       } 
18   } 
Then this would be your test class that uses InMemoryTestContainer
1    package com.trimbo.web.api; 
2     
3    import com.sun.jersey.api.client.ClientResponse; 
4    import com.sun.jersey.api.client.WebResource; 
5    import com.sun.jersey.test.framework.AppDescriptor; 
6    import com.sun.jersey.test.framework.JerseyTest; 
7    import com.sun.jersey.test.framework.WebAppDescriptor; 
8    import com.sun.jersey.test.framework.spi.container.TestContainerFactory; 
9    import com.sun.jersey.test.framework.spi.container.inmemory.InMemoryTestContainerFactory; 
10   import org.junit.Test; 
11   import org.springframework.web.context.ContextLoaderListener; 
12    
13   import static junit.framework.Assert.assertEquals; 
14    
15   public class TestServiceTest extends JerseyTest { 
16       @Override 
17       protected TestContainerFactory getTestContainerFactory() { 
18           return new InMemoryTestContainerFactory(); 
19       } 
20    
21    
22       @Override 
23       protected AppDescriptor configure() { 
24           return 
25                   new WebAppDescriptor.Builder("com.trimbo.web.api") 
26                           .contextPath("/") 
27                           .contextParam("contextConfigLocation", "classpath:service-spring-config.xml") 
28                           .contextListenerClass(ContextLoaderListener.class) 
29                           .build(); 
30       } 
31    
32       @Test 
33       public void testIndex() throws Exception { 
34           WebResource resource = resource().path("/jobs"); 
35           ClientResponse resp = resource.get(ClientResponse.class); 
36           assertEquals(200, resp.getStatus()); 
37       } 
38   } 
This basically configures an App Descriptor with the prefix of our package, the "/" path in web, and uses our spring config as the context.  Note that the TestContainerFactory is our InMemoryTestContainerFactory.
Basically I'm writing this down for myself.  Hopefully it helps someone else too.