This page provides a step-by-step tutorial of WS client testing. In the tutorial we will test a simple calculator web service. The source code of the sample application is downloadable from SVN repository.
It's possible to download it using following command svn co https://java-crumbs.svn.sourceforge.net/svnroot/java-crumbs/simple-client-test/tags/simple-client-test-0.22/simple-client-test/ simple-client-test
Let's start with the XSD defining data structures of the service.
<?xml version="1.0" encoding="UTF-8"?> <schema xmlns="http://www.w3.org/2001/XMLSchema" targetNamespace="http://javacrumbs.net/calc" xmlns:tns="http://javacrumbs.net/calc" elementFormDefault="qualified"> <element name="plusRequest"> <complexType> <sequence> <element name="a" type="int" /> <element name="b" type="int" /> </sequence> </complexType> </element> <element name="plusResponse"> <complexType> <sequence> <element name="result" type="int" /> </sequence> </complexType> </element> </schema>
There is one "operation" called plus allowing to add two numbers. The result is returned in the result element.
Spring configuration (client-config.xml) is simple too.
<?xml version="1.0" encoding="UTF-8"?> <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.xsd"> <bean class="net.javacrumbs.calc.Calc"/> <bean id="wsTemplate" class="org.springframework.ws.client.core.WebServiceTemplate"> <property name="defaultUri" value="http://localhost/calc"/> <property name="marshaller" ref="marshaller"/> <property name="unmarshaller" ref="marshaller"/> </bean> <bean id="marshaller" class="org.springframework.oxm.jaxb.Jaxb2Marshaller"> <property name="contextPath" value="net.javacrumbs.calc.model"/> </bean> </beans>
The only noteworthy part is the marshaller. JAXB2 is used and corresponding classes are generated by Maven using following configuration.
The client code is also simple, we just have to create request payload, send it to WS template and extract the response.
public class Calc { @Autowired private WebServiceOperations wsTemplate; public int plus(int a, int b) { PlusRequest request = new PlusRequest(); request.setA(a); request.setB(b); PlusResponse result = (PlusResponse) wsTemplate.marshalSendAndReceive(request); return result.getResult(); } }
Now when the code to be tested is clear, we can focus on the test itself. Let's start with the easiest one
@RunWith(SpringJUnit4ClassRunner.class) //load your standard config @ContextConfiguration(locations={"classpath:client-config.xml"}) //Add the listener (DirtiesContextTestExecutionListener.class, TransactionalTestExecutionListener.class might be needed if @DirtiesContext or @Transactional is used. @TestExecutionListeners({WsMockControlTestExecutionListener.class, DependencyInjectionTestExecutionListener.class}) public class CalcTest { @Autowired private Calc calc; //inject mock control @Autowired private WsMockControl mockControl; @Test public void testSimple() { mockControl.returnResponse("response1.xml"); int result = calc.plus(1, 2); assertEquals(3, result); mockControl.verify(); } }
The test class starts with standard Spring JUnit support. The TestExecutionListener configuration section is the important bit. In addition to standard Spring listeners, WsMockControlTestExecutionListener.class is used. The listener does all the magic - it makes WsMockControl bean available, alters WsTemplate so no server is called and in general, it makes sure that everything works. Please note that DirtiesContextTestExecutionListener.class, TransactionalTestExecutionListener.class might be needed if @DirtiesContext or @Transactional is used.
Once we have WsMockControl, we can write the test. First of all, it's necessary to define which response should be returned from the web service call. That's achieved by mockControl.returnResponse("response1.xml"); call. When we have this it's possible to call the WS client code. The mock infrastructure kicks in and instead of calling remote service, response1.xml is returned.
It's recommended to verify, that the web service was indeed called. It's done by mockControl.verify();
Usually it's useful to verify that the request generated by the client code has expected form. It's easy to achieve this way.
@Test public void testVerifyRequest() { mockControl.expectRequest("request1.xml").returnResponse("response1.xml"); int result = calc.plus(1, 2); assertEquals(3, result); mockControl.verify(); }
Please note that it's possible to define the requests and responses either as full soap messages or as payload only. It depends only on your preference.
We can also make sure the request conforms to XSD.
@Test public void testSchema() { mockControl.validateSchema("xsd/calc.xsd").expectRequest("request1.xml").returnResponse("response1.xml"); int result = calc.plus(1, 2); assertEquals(3, result); mockControl.verify(); }
Even though we could define special request and response file for every possible test it would soon become a maintenance nightmare. Therefore there are few mechanisms that allow us to reuse request and response files. The first one is the $IGNORE placeholder.
It's possible to ignore some parts of the request when comparing it to generated value.
<plusRequest xmlns="http://javacrumbs.net/calc"> <a>${IGNORE}</a> <b>${IGNORE}</b> </plusRequest>
This "template" can be reused for all possible tests. Values of elements a and b are ignored.
Similar mechanism can be used for responses too. In response, it's not possible just to simply ignore some values, we have to generate them. One way it is to use a XSLT template.
<?xml version="1.0" encoding="UTF-8"?> <xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" version="1.0"> <xsl:template match="/"> <plusResponse xmlns="http://javacrumbs.net/calc" xmlns:c="http://javacrumbs.net/calc"> <result><xsl:value-of select="//c:a + //c:b"/></result> </plusResponse> </xsl:template> </xsl:stylesheet>
This template takes value from requests and calculates the response. Here we have in fact implemented the service using the template. In more real-life like scenarions it's useful too. It's possible to generate the response based on the request values. Even though XSLT is quite powerful toll, FreeMarker templates are supported too. You just have to call mockControl.useFreeMarkerTemplateProcessor().
Context Variables is another tool how to parametrize the templates. It's possible to define variables in the test script and use them in the template. Context Variables can be used both in request and response templates.
@Test public void testContext() { mockControl .setTestContextAttribute("a", 1) .setTestContextAttribute("b", 4) .useFreeMarkerTemplateProcessor() .expectRequest("request-context.xml") .returnResponse("response2.xml"); int result = calc.plus(1, 4); assertEquals(5, result); mockControl.verify(); }
FreeMarker template can look like this
<!-- Freemarker template --> <plusRequest xmlns="http://javacrumbs.net/calc"> <a>${a}</a> <b>${b}</b> </plusRequest>
It can be also used in XSLT this way
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" version="1.0"> <xsl:param name="a"/> <xsl:param name="b"/> <xsl:template match="/"> <plusRequest xmlns="http://javacrumbs.net/calc"> <a><xsl:value-of select="$a"/></a> <b><xsl:value-of select="$b"/></b> </plusRequest> </xsl:template> </xsl:stylesheet>
It's also possible to test multiple calls of a WS in one test. The most straightforward approach is this
@Test public void testMultiple() { mockControl.expectRequest("request-ignore.xml").atLeastOnce() .returnResponse("response2.xml") .returnResponse("response3.xml"); assertEquals(5, calc.plus(2, 3)); assertEquals(8, calc.plus(3, 5)); mockControl.verify(); }
You can also define multiple expected requests, it's just necessary to define them in correct order
@Test public void testStrange() { mockControl .expectRequest("request1.xml").returnResponse("response1.xml") .expectRequest("request-ignore.xml").returnResponse("response2.xml") .expectRequest("request1.xml").returnResponse("response1.xml"); assertEquals(3, calc.plus(1, 2)); assertEquals(5, calc.plus(2, 3)); assertEquals(3, calc.plus(1, 2)); mockControl.verify(); }
Unit test support is based on RequestProcessors as the rest of the framework. A RequestProcessor can either validate request, generate response, do nothing or throw an exception. By calling expectRequest, returnResponse, validateSchema and others a new RequestProcessor is created. All request processors used by WsMockControl are implementations of LimitingRequestProcessor. It means that besides their normal functionality they also keep track of number of calls they have processed. Moreover a LimitingRequestProcessor has minimum and maximum number of calls it can process. If less calls were processed an exception is thrown when WsMockControl.verify() was called. All calls over maximum limit are ignored.
By calling atLeastOnce(), times(), ... it is possible to set lower an upper limit on the RequestProcessor inserted last. Default value is one call.
If you find yourself writing tests that result in lot of backend calls, consider using Functional tests instead.