In this post I'd like to present an automated way to implement functional tests for your web services using Spring. The approach relies mainly on Spring TestContext Framework, an embedded Jetty instance and Spring Web Services (you saw it coming, didn't you?).
The outline is as follows: functional tests are implemented using Spring TestContext and JUnit 4 (or any of the other supported testing frameworks). In the ApplicationContext that Spring TestContext creates for the test, we include an embedded Jetty instance that loads and runs the target web service application. The actual test consists simply of invoking that web service through a WebServiceTemplate (Spring-WS) and validating the response with XMLUnit. Finally, we run our tests using Maven (mvn test, simply).
First, let me tell you that if you're not familiar with the Spring TestContext Framework, you don't know what you're missing. I'll quote the reference manual for a quick definition:
The Spring TestContext Framework (located in the org.springframework.test.contextWithout delving into all the features of Spring TestContext here, what you need to know is that the framework creates and caches an ApplicationContext instance -basically, a full-fledged Spring container- for every test class and allows to inject dependencies into your test classes. There are a number of other useful features that we will not use here, but already the features I just mentioned are extremely valuable as you will see next.
package) provides generic, annotation-driven unit and integration testing support that is agnostic of the testing framework in use (...). The TestContext framework also places a great deal of importance on convention over configuration with reasonable defaults that can be overridden via annotation-based configuration.
An Example
Let's illustrate the concept on a simple web service. I'm using Spring-WS to implement the web service to test but the implementation framework is really irrelevant - you can do the same thing with your favorite one.The application that we will be testing is a weather service that receives a request containing the coordinates of a specific location and returns the temperature there.
For example, if you're anxious to know what the temperature is in Easter Island, you can send a SOAP request containing a payload similar to the following:
<WeatherRequest xmlns="http://www.cafebabe.me/weather-service"> <Location> <Longitude>109:25:30W</Longitude> <Latitude>27:9:0S</Latitude> </Location> </WeatherRequest>The response will look something like this:
<WeatherResponse xmlns="http://www.cafebabe.me/weather-service"> <Temperature>30.0</Temperature> </WeatherResponse>Let's set up the testing environment now. First we need an instance of embedded Jetty. We will put this in its own separate bean definition file to make it reusable from different tests:
<?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:context="http://www.springframework.org/schema/context" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-2.5.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-2.5.xsd"> <context:property-placeholder location="classpath:me/cafebabe/weather/ws/jetty.properties" /> <bean id="server" class="org.mortbay.jetty.Server" init-method="start" destroy-method="stop"> <property name="connectors"> <list> <bean id="Connector" class="org.mortbay.jetty.nio.SelectChannelConnector"> <property name="port" value="${jetty.port}" /> </bean> </list> </property> <property name="handler"> <bean id="handlers" class="org.mortbay.jetty.handler.HandlerCollection"> <property name="handlers"> <list> <bean id="contexts" class="org.mortbay.jetty.handler.ContextHandlerCollection"> <property name="handlers"> <list> <bean class="org.mortbay.jetty.webapp.WebAppContext"> <property name="contextPath" value="/${jetty.contextPath}" /> <property name="war" value="src/main/webapp" /> </bean> </list> </property> </bean> </list> </property> </bean> </property> </bean> </beans>Those bean definitions are all what we need to configure and bootstrap a minimal Jetty instance (notice the init-method and destroy-method on the server bean). Jetty will deploy the application directly from the Maven project directories (see the war property), which is particularly nice since the project structure is preserved and no additional configuration is needed.
We will also externalize Jetty properties in a separate file to reference them from our tests:
jetty.port=8282 jetty.contextPath=Now we can write a simple functional test that invokes the weather service and validates the return message. We need to inject a Resource pointing to the file that contains the test request and a WebServiceTemplate to invoke the service. Aside from that there is a simple initialization method to set up the namespaces for XMLUnit:
@RunWith(SpringJUnit4ClassRunner.class) @ContextConfiguration public class WeatherServiceIntegrationTest { @Autowired private WebServiceTemplate webServiceTemplate; @Autowired private Resource request; @BeforeClass public static void setUpNamespaces() throws Exception { Map<String, String> namespaces = new HashMap<String, String>(); namespaces.put("w", "http://www.cafebabe.me/weather-service"); NamespaceContext ctx = new SimpleNamespaceContext(namespaces); XMLUnit.setXpathNamespaceContext(ctx); } @Test public void testInvoke() throws Exception { Source requestSource = new ResourceSource(request); StringResult result = new StringResult(); webServiceTemplate.sendSourceAndReceiveToResult(requestSource, result); XMLAssert.assertXpathExists("/w:WeatherResponse/w:Temperature/text()", result .toString()); } }Notice how in testInvoke we use the convenient ResourceSource and StringResult from Spring-WS to read the request from a resource and to write the response to a String respectively.
The final missing piece is the bean definition file for WeatherServiceIntegrationTest. Since the class is simply annotated with @ContextConfiguration, Spring TestContext will automatically look for a bean definition file named WeatherServiceIntegrationTest-context.xml under the same package. The file simply imports the Jetty bean definition file and defines the WebServiceTemplate and the request Resource beans needed for the test:
<beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-2.5.xsd"> <import resource="jetty.xml"/> <bean id="webServiceTemplate" class="org.springframework.ws.client.core.WebServiceTemplate"> <property name="defaultUri" value="http://localhost:${jetty.port}/${jetty.contextPath}" /> </bean> <bean id="request" class="org.springframework.core.io.ClassPathResource"> <constructor-arg value="me/cafebabe/weather/ws/request.xml"/> </bean> </beans>To sum up, here's what happens when we run "mvn test":
- An application context is created for WeatherServiceIntegrationTest from the WeatherServiceIntegrationTest-context.xml file.
- An embedded Jetty server starts and loads the web service application directly from the Maven project.
- The testInvoke method runs and invokes the web service using a WebServiceTemplate.
- Jetty shuts down gracefully when the test completes.
You can download the sample code here.