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.

5 comments:

Nathan Schrenk said...

"When you learn C, you are introduced to many harsh realities. When you learn Java, you are introduced to XML." -- Ted Dziuba

Trimbo said...

FWIW, this is why you HAVE to use an IDE with Java. I don't even notice the XML so much because IntelliJ deals with autocompletion, validation, etc..

So it makes me wonder, did the XML configs come after IDEs became the norm in Java, or before? I suspect after. We definitely know XML came after in the Microsoft world -- well after Visual C++ / VB were established. In languages where people typically do not use IDEs, you never see people doing configuration with XML.

Also, I tried using annotations for dependency injection with Guice. Spring's ubiquity and the hands-off nature of Spring's XML configuration for DI led me back there. They both have their advantages though.

Unknown said...

Great timing! I'm brand new to Spring and I think I'm facing a similar problem. I need to share a Spring ApplicationContext to inject shared objects with both a Netty server and embedded Jetty server w/ Jersey running in the same process.

I'm guessing a solution similar to yours may be what the doctor ordered!

I have a Stack Overflow question addressing the issue, if you'd like to earn some SO reputation points.

Trimbo said...

Some more information. Here's how to inject mockable objects using a "MockableContext"

Courtesy Colin Vepers in this thread: http://permalink.gmane.org/gmane.comp.java.jersey.user/8431


public class MyIntegrationTest extends JerseyTest {
private MockService mockService;

@Override
protected AppDescriptor configure() {
mockService = new MockService();
MockableContext.addMock("myService", mockService);

return new WebAppDescriptor.Builder("com.shazam.rest")
.contextPath("/root")
.contextParam("contextConfigLocation", "classpath:applicationContext.xml")
.contextParam("contextClass", "com.company.MockableContext")
.servletClass(SpringServlet.class)
.contextListenerClass(ContextLoaderListener.class)
.requestListenerClass(RequestContextListener.class)
.build();
}
}

public class MockableContext extends XmlWebApplicationContext {
private static Map mocks = new HashMap();

@Override
protected DefaultListableBeanFactory createBeanFactory() {
return new MockableBeanFactory();
}

private class MockableBeanFactory extends DefaultListableBeanFactory {
@Override
public Object getBean(String name) throws BeansException {
Object obj = mocks.get(name);
if (obj != null) {
return obj;
}
return super.getBean(name);
}
}

public static void addMock(String id, Object mock) {
mocks.put(id, mock);
}

public static void reset() {
mocks.clear();
}
}

Unknown said...

So...a 55 line XML file to specify three integers and two strings... Awesome.