I promised to tackle further and more advanced topics relating to the interaction of Spring Boot and Apache CXF in my upcoming blog posts. So in the following we will take a look at testing SOAP web services. How do we test a web service from within a unit test? How do we build integration tests? And isn’t there something in between? OK, let´s get started!
Spring Boot & Apache CXF – Tutorial
Part 1: Spring Boot & Apache CXF – How to SOAP in 2016
Part 2: Spring Boot & Apache CXF – Testing SOAP web services
Part 3: Spring Boot & Apache CXF – XML validation and custom SOAP faults
Part 4: Spring Boot & Apache CXF – Logging & Monitoring with Logback, Elasticsearch, Logstash & Kibana
Part 5: Spring Boot & Apache CXF – SOAP on steroids fueled by cxf-spring-boot-starter
We started our journey in the first blog post of this series, in which we looked at the best way to wire Spring Boot and Apache CXF. We found out how to generate all necessary Java class files based on the WSDL and its bunch of imported XSDs elegantly utilizing the JAX-WS Maven plugin . This way we don´t have to check in generated artifacts into our version control system and we´re always up-to-date regarding our API definition (“contract first”). We also learned how to configure Apache CXF through 100% XML free Spring Java configuration and can now easily fire up a running SOAP endpoint.
But let´s finally start by giving the source folder src/test/ we have ignored so far the attention it deserves and creating some tests. So far we haven´t written any of them – although we should do, especially in this case. The first refactoring will come and SOAP web services can become really complex, so having some good tests in place is inevitable.
Unit tests (aka yxzTest.class)
The following steps are as usual fully reproducible from the Github repository tutorial-soap-spring-boot-cxf . The corresponding project step4_test resides there as well.
Our Endpoint class , which we derived from the generated Service Endpoint Interface (SEI), is just a normal POJO, more precisely a Spring component. So there´s nothing new here. Just instantiate it with the new operator and write your unit tests at a whim.
Since the Endpoint itself shouldn’t contain functional business logic (it is, after all, somewhat “polluted” with infrastructure code), these things are delegated to another component, something called e.g. MyFancyServiceController. Now there´s no real point in testing our WebServiceEndpoint in a completely isolated way, i.e. according to pure testing principles. In most cases you definitely want to add a small piece of Spring and test some rather complex sequence.
To this end, we enhance our example from step 3 with a rudimentary “WeatherServiceController” and configure it as a Spring Bean in a separate ApplicationConfiguration . Through its only implemented method getCityForecastByZIP(ForecastRequest forecastRequest) our WeatherServiceController answers with a valid Weather service XSD compliant response – assisted by the GetCityForecastByZIPOutMapper , which is also new to our project. From our WeatherServiceEndpoint we access the injected WeatherServiceController, so that we finally have some running code we´ll be able to test. Keep in mind that this is only a very simple example implementation. We leave out many things you have to implement in real world projects like complete inbound and outbound transformation, functional plausibility checks, various backend calls, just to mention a few.
Looking at our test class WeatherServiceTest.java , it seems to be implemented in a rather straightforward manner. We only need the two annotations @RunWith(SpringJUnit4ClassRunner.class) and @ContextConfiguration(classes=ApplicationTestConfiguration.class) to successfully initialize our Spring application context, which itself instantiates the two Spring beans WeatcherServiceEndpoint & WeatherServiceController necessary for the test (configured in ApplicationTestConfiguration.java ).
Inside our @Test annotated method, we create an appropriate request and call the corresponding method of our injected (via @Autowired) endpoint:
1@RunWith(SpringJUnit4ClassRunner.class)
2@ContextConfiguration(classes=ApplicationTestConfiguration.class)
3public class WeatherServiceTest {
4
5 @Autowired
6 private WeatherServiceEndpoint weatherServiceEndpoint;
7
8 @Test
9 public void getCityForecastByZIP() throws WeatherException {
10 // Given
11 ForecastRequest forecastRequest = generateDummyRequest();
12
13 // When
14 ForecastReturn forecastReturn = weatherServiceEndpoint.getCityForecastByZIP(forecastRequest);
15
16 // Then
17 assertNotNull(forecastReturn);
18 // many asserts here
19 assertEquals("22%", forecastReturn.getForecastResult().getForecast().get(0).getProbabilityOfPrecipiation().getDaytime());
20 }
21}
If this test finishes in “green”, we know that our endpoint POJO is doing what it is supposed to do. And that´s everything we need to create our unit test.
Integration tests (aka yxzIntegrationTest.class)
Up to this point, there was nothing new regarding testing with Spring. But now it´s getting more interesting, I hope: How do we test the SOAP web services themselves?
Integration tests should really involve as many components as possible inside their execution phase. But because we call many backends inside those tests, the time to execute them quickly adds up – not to mention the execution of more than one integration test. Running those inside our normal build process would really slow down our development process. Therefore we should exclude them from being executed every time someone or something triggers a build – e.g. with the help of the Maven Surefire plugin :
1<plugin> 2 <groupId>org.apache.maven.plugins</groupId> 3 <artifactId>maven-surefire-plugin</artifactId> 4 <configuration> 5 <excludes> 6 <exclude>**/*IntegrationTest.java</exclude> 7 </excludes> 8 </configuration> 9</plugin>
Having this plugin in place, our integration tests won´t run while something like mvn install or mvn package is executed. We are still able to call them manually inside our IDE (or as a background process triggered by something like infinitest ) or automatically, but decoupled from the normal build job on our CI server. You could create a Maven profile for that, which includes the integration tests again and is executed by a separate integration test CI job.
So let´s now look at how to write the integration test itself. The configuration of the necessary SOAP service in client mode is done using the org.apache.cxf.jaxws.JaxWsProxyFactoryBean, to which we forward our Service Endpoint Interface (SEI) via the method setServiceClass(). Additionally we configure the URL where our service would be reached, e.g. by calling it via SoapUI. It can be helpful to provide the base URL which we used to configure the CXFServle as an accessible constant, along with the trailing part, which represents the concrete web service in our WebServiceConfiguration .
As a final step we call the create() method that our configured instance of the JaxWsProxyFactoryBean provides. Cast to our service endpoint interface, this will create our web service client, which provides every method defined inside our WSDL file. Sadly the CXF API doesn´t use the power of generics, so this cast is necessary here. The configuration class WebServiceIntegrationTestConfiguration.java for all our integration tests looks like this:
1@Configuration
2public class WebServiceIntegrationTestConfiguration {
3
4 @Bean
5 public WeatherService weatherServiceIntegrationTestClient() {
6 JaxWsProxyFactoryBean jaxWsProxyFactory = new JaxWsProxyFactoryBean();
7 jaxWsProxyFactory.setServiceClass(WeatherService.class);
8 jaxWsProxyFactory.setAddress("http://localhost:8080" + WebServiceConfiguration.BASE_URL + WebServiceConfiguration.SERVICE_URL);
9 return (WeatherService) jaxWsProxyFactory.create();
10 }
11}
Compared to our unit test, the new class for integration testing WeatherServiceIntegrationTest looks nearly similar. But there are some differences. We configure our WebServiceIntegrationTestConfiguration and inject the service client instead of the endpoint POJO. Everything else remains the same:
1@RunWith(SpringJUnit4ClassRunner.class)
2@ContextConfiguration(classes=WebServiceIntegrationTestConfiguration.class)
3public class WeatherServiceIntegrationTest {
4
5 @Autowired
6 private WeatherService weatherServiceIntegrationTestClient;
7
8 @Test
9 public void getCityForecastByZIP() throws WeatherException {
10 // Given
11 ForecastRequest forecastRequest = generateDummyRequest();
12
13 // When
14 ForecastReturn forecastReturn = weatherServiceIntegrationTestClient.getCityForecastByZIP(forecastRequest);
15
16 // Then
17 assertNotNull(forecastReturn);
18 // many asserts here
19 assertEquals("22%", forecastReturn.getForecastResult().getForecast().get(0).getProbabilityOfPrecipiation().getDaytime());
20 }
21}
If we now run our new integration test, it will fail in most cases, giving us a javax.xml.ws.WebServiceException: Could not send Message […] Caused by: java.net.ConnectException: Connection refused. This is because we didn´t start our SOAP server, which could be done easily with a “Run as…” on SimpleBootCxfApplication.java . As described earlier, the integration test should involve the complete SOAP communication including the XML to Java marshalling and all the backend logic. After starting our SOAP server, the execution of our integration test should give us some green output. 🙂
And please don´t get confused because of this manual step of starting our SOAP server: If we set up our Continuous Integration and Deployment Pipeline correctly, including working stages running our SOAP server, the Integration tests will run automatically at the end of the deployment process.
Single system integration tests (aka yxzSystemTest.class)
But this cannot be everything! In our current project it soon became obvious that the well-known separation into unit and integration tests is not enough. If you look at the development process timeline, you´ll notice that your unit tests check the core functionality (your POJOs) at the very beginning of this process. The integration tests are executed automatically as the last step in your process. E.g. in the last Jenkins job in your pipeline, when everything else is developed, checked into your version control system and got built, provisioned, and deployed. But our gut feeling tells us that we should do something in between, checking as many of the necessary components as possible, to provide our SOAP endpoint later. Late errors that occur in our integration tests are much more expensive than earlier ones.
Based on this observation and using the power of Spring (Boot) , we developed the idea of another variant of tests. These should be executable completely on one system (e.g. your dev machine or CI server), firing up all necessary components at runtime if possible – or at least mocking them out. One can discuss endlessly about names, but we just called them single system integration tests (Java classes have a trailing SystemTest). They are by far the most fascinating technical test variant. We´ll soon see why.
As a preliminary remark, these tests should not be excluded from our normal build process, as they could be executed way faster than integration tests while also being way more stable and independent from other systems. Because they don´t include “IntegrationTest” in their naming, the suggested execution via the Surefire Plugin is also appropriate.
Configuring a single system integration test is mostly identical to the configuration of a usual integration test. But they will mostly differ at the host and port. Because when your CI Pipeline and the corresponding stages are up and running, your single system integration test will run locally, but your integration tests will call remote SOAP endpoints. So although it´s a bit exaggerated to give our example nearly the same configuration class WebServiceSystemTestConfiguration.java as the one configuring integration tests, we will do it anyway. And in real world projects you will for sure need this separation. For our example, we change the port to 8090. In order to give Spring the possibility to inject correctly, we also rename our Bean to weatherServiceSystemTestClient() instead of weatherServiceIntegrationTestClient():
1jaxWsProxyFactory.setAddress("http://localhost:8090" + WebServiceConfiguration.BASE_URL + WebServiceConfiguration.SERVICE_URL);
In contrast to our Integrations tests, we want to fire up our SOAP server before the test´s execution, run all test methods against that server and finally tear it down when all methods are executed. Therefore we need a class that is annotated with @SpringBootApplication . But in contrast to what we´ve done with our SimpleBootCxfApplication in production code under scr/main/java, the imports are different. Our new SimpleBootCxfSystemTestApplication.java imports the configuration class WebServiceSystemTestConfiguration:
1@SpringBootApplication
2@Import(WebServiceSystemTestConfiguration.class)
3public class SimpleBootCxfSystemTestApplication {
4
5 public static void main(String[] args) {
6 SpringApplication.run(SimpleBootCxfSystemTestApplication.class, args);
7 }
8}
Finally we´ll have a look at our actual test class WeatherServiceSystemTest . It makes use of our well-known @RunWith annotation, but instead of using @ContextConfiguration, we type in @SpringApplicationConfiguration, forwarding our aforementioned SimpleBootCxfSystemTestApplicationl.class. Additionally we use the @WebIntegrationTest annotation, which does all the magic for us: It pulls up our SOAP server, so all the methods can use it within their execution. As you can see, we forward our “SystemTest port” 8090 to it – because we configured our single system integration test configuration to use that one.
As a final step, we rename our injected WeatherService bean to “weatherServiceSystemTestClient”, so Spring knows how to autowire correctly. Again, our test case is only slightly different to our other test variants:
1@RunWith(SpringJUnit4ClassRunner.class)
2@SpringApplicationConfiguration(classes=SimpleBootCxfSystemTestApplication.class)
3@WebIntegrationTest("server.port:8090")
4public class WeatherServiceSystemTest {
5
6 @Autowired
7 private WeatherService weatherServiceSystemTestClient;
8
9 @Test
10 public void getCityForecastByZIP() throws WeatherException {
11 // Given
12 ForecastRequest forecastRequest = generateDummyRequest();
13
14 // When
15 ForecastReturn forecastReturn = weatherServiceSystemTestClient.getCityForecastByZIP(forecastRequest);
16
17 // Then
18 assertNotNull(forecastReturn);
19 // many asserts here
20 assertEquals("22%", forecastReturn.getForecastResult().getForecast().get(0).getProbabilityOfPrecipiation().getDaytime());
21 }
22}
Looking at our simple example, the power of those tests is not always obvious. Some of my current project´s team mates initially thought that these couldn’t be that difficult. But they were suprised when they realized what´s behind the scenes. Seeing an entire “enterprisey SOAP endpoint” (like a BiPro web service) including all its components get started inside a test case and thoroughly checked makes everyone enthusiastic. Even the smallest change for the worse inside your complex SOAP endpoint project will make your IDE or CI server show some red light (assuming that you wrote good and meaningful tests, as described by my colleagues in many blog posts, e.g. in this recent one: Writing Better Tests With JUnit ).
How to deal with your test cases
Having looked at all these different kinds of test cases, we should briefly discuss another aspect: No matter what technologies we use to get our SOAP services to live – in the end there are those XML requests that our endpoints have to be able to handle correctly. So for me it´s really reassuring to know that my services are able to handle the XML requests that somebody fires against my web services (which I could easily reconstruct with a client like SoapUI ). Here we come to realize that running automated tests involving these XML requests is inevitable and we want to be able to do so all the time.
This raises the question: Where should we store our XML test files and how can we distribute them to all the test users, versioning them safely? Additionally all XML test files should be marked for update when something inside the API or the WSDL or XML schema changes. Also there shouldn’t be too many copies around that have to be taken care of. Based on those requirements, many tools worth (several) millions, but nevertheless useless, have been sold. This was a painful experience I had when I wrote my diploma thesis many years ago.
So why shouldn’t we put all those heavy tools aside and think about a more radical approach? Maybe one that doesn´t cover all of our requirements a 100 %. But hey! If this means up-to-date test cases, where all the project developers raise the alarm because their IDEs run into red test case execution results or where Jenkins jobs break because of incorrect XML test files, why not?
The idea is simple: We just put all our test files called “someFancyTest.xml” into our version control system inside our project`s folder for test resources – let´s say something beneath src/test/resources/requests – and load them into our ever-growing number of unit, integration and system tests. Inside of them we use the power of the JAX-B Java to XML marshallers to load those files into our test cases. This gives us the opportunity to throw every single XML file also manually against our web service endpoints – e.g. just to get a good gut feeling or to reproduce some errors. An example test case, put somewhere in src/test/resources/requests as XYZ-Testcase.xml, could look like this:
1<?xml version="1.0" encoding="UTF-8"?> 2<soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/" xmlns:gen="http://www.codecentric.de/namespace/weatherservice/general"> 3 <soapenv:Header/> 4 <soapenv:Body> 5 <gen:GetCityForecastByZIP> 6 <gen:ForecastRequest> 7 <gen:ZIP>99425</gen:ZIP> 8 <gen:flagcolor>bluewhite</gen:flagcolor> 9 <gen:productName>ForecastBasic</gen:productName> 10 <gen:ForecastCustomer> 11 <gen:Age>30</gen:Age> 12 <gen:Contribution>5000</gen:Contribution> 13 <gen:MethodOfPayment>Paypal</gen:MethodOfPayment> 14 </gen:ForecastCustomer> 15 </gen:ForecastRequest> 16 </gen:GetCityForecastByZIP> 17 </soapenv:Body> 18</soapenv:Envelope>
But there´s a catch: We cannot use the extremely simplified configuration of the XML to Java marshalling that a web service framework like Spring Boot with Apache CXF provides. We have to make JAX-B work somehow. But this is not overly difficult. We could develop our own helper class that takes over those recurring tasks – or we take a closer look at the class XmlUtils inside our example project. Particularly the method with the – admittedly bulky – name readSoapMessageFromStreamAndUnmarshallBody2Object (InputStream fileStream, Class jaxbClass) provides us with everything that´s needed to do the job.
With the help of the XML parsers distributed with standard JDK it parses our XML files´ InputStream and builds a org.w3c.dom.Document . Therein it searches for the desired contents of the SOAP body needed to marshall it into the forwarded JAX-B POJO – which for sure was generated via the JAX-WS Maven plugin (see part 1 of this tutorial ).
With the resulting object we have our XML test file exactly as we need it inside our test cases. Using these is shown inside the class WeatherServiceXmlFileSystemTest.java , which again displays only few differences to the other test cases:
1@RunWith(SpringJUnit4ClassRunner.class)
2@SpringApplicationConfiguration(classes=SimpleBootCxfSystemTestApplication.class)
3@WebIntegrationTest("server.port:8090")
4public class WeatherServiceXmlFileSystemTest {
5
6 @Autowired
7 private WeatherService weatherServiceSystemTestClient;
8
9 @Value(value="classpath:requests/GetCityForecastByZIPTest.xml")
10 private Resource getCityForecastByZIPTestXml;
11
12 @Test
13 public void getCityForecastByZIP() throws WeatherException, XmlUtilsException, IOException {
14 // Given
15 GetCityForecastByZIP getCityForecastByZIP = XmlUtils.readSoapMessageFromStreamAndUnmarshallBody2Object(getCityForecastByZIPTestXml.getInputStream(), GetCityForecastByZIP.class);
16
17 // When
18 ForecastReturn forecastReturn = weatherServiceSystemTestClient.getCityForecastByZIP(getCityForecastByZIP.getForecastRequest());
19
20 // Then
21 assertNotNull(forecastReturn);
22 // many asserts here
23 assertEquals("22%", forecastReturn.getForecastResult().getForecast().get(0).getProbabilityOfPrecipiation().getDaytime());
24 }
25}
By the way: We don’t have to load the XML test files ourselves. This is handled in a much easier way by Spring´s org.springframework.core.io.Resource . Via the @Value annotation we just have to point it to the right directory where our test file is located. As said above, somewhere beneath src/test/resources/requests. And be sure not to forget the preceding keyword “classpath:”. Then everything should run fine.
Now we saved our developer´s soul: We are able to test our SOAP web services sensibly and in an automated way, covering several development process steps. Maintenance, finding errors and refactoring will be much easier, just to mention a few benefits. In addition we can completely refrain from using expensive and heavy tools. And my favorite point: We document the correct usage of our SOAP web services! Because after all, having those mighty standards of validating data such as WSDL and XSD in place doesn’t mean that there´s no room for interpretation.
But we still haven’t covered everything! Our SOAP responses´ namespace prefixes look terrifying (“ns1”, “ns2”, …), and our big book entitled “Customer´s custom web service specification” requires that we always respond with a XML schema compliant response, even if somebody throws completely nonsensical requests against our web service endpoint. Additionally our Ops people always want to know if our web service is still working, and we want to know what requests our endpoint has to face in detail. We´ll see how to tackle these points in one of the next parts of this blog tutorial series.
More articles
fromJonas Hecht
Your job at codecentric?
Jobs
Agile Developer und Consultant (w/d/m)
Alle Standorte
More articles in this subject area
Discover exciting further topics and let the codecentric world inspire you.
Gemeinsam bessere Projekte umsetzen.
Wir helfen deinem Unternehmen.
Du stehst vor einer großen IT-Herausforderung? Wir sorgen für eine maßgeschneiderte Unterstützung. Informiere dich jetzt.
Hilf uns, noch besser zu werden.
Wir sind immer auf der Suche nach neuen Talenten. Auch für dich ist die passende Stelle dabei.
Blog author
Jonas Hecht
Senior Solution Architect
Do you still have questions? Just send me a message.
Do you still have questions? Just send me a message.