The idea of applications that provide their services over the web is anything but new. Chances are quite high that you already implemented such a service through a REST API. But before REST, there was a different approach to providing such services - SOAP. The release of Spring Boot 2.6.0 introduced a new test slice to test the components involved in providing these services. Let’s have a look at it!
What is a Web Service?
In general, a web service is an interface that allows machine-to-machine communication. It is used for data exchange or the execution of functions in another system. In this blog post, we will talk about web services that can be accessed via the SOAP protocol. SOAP itself is a W3C standard that defines the way how the systems communicate with each other. It uses messages in XML format that are sent – mostly, but not exclusively – over HTTP.
Through Spring Web Services, Spring offers a convenient way to develop these kinds of services.
Spring Web Services (Spring-WS) is a product of the Spring community focused on creating document-driven Web services. Spring Web Services aims to facilitate contract-first SOAP service development, allowing for the creation of flexible web services …
As those services are used to integrate different applications, they can make or break our complete software system. To reduce the risk of breaking something we should implement tests to prevent errors or unwanted changes in the interface. Luckily for us, Spring Boot offers a test slice for this purpose – the @WebServiceServerTest
.
What is @WebServiceServerTest
The @WebServiceServerTest
is another one of Spring Boot’s test slices. Like the others, it is used for integration tests that focus on a specific part of your application. In this case the Spring Web Service Endpoints. By annotating a test with @WebServiceServerTest
, the full auto-configuration of the application context is disabled. However, the beans relevant for web services (Endpoints, EndpointInterceptor) are configured. On top of that Spring Boot will also configure a MockWebServiceClient
which can be used in our tests to access the services.
The Example
Before we start the implementation of the tests I will first introduce the web service endpoints that will be tested. This blog post will not contain any details about how the endpoints are implemented, just how they can be tested. If you are interested in implementing a service yourself, have a look at the Resources below.
@Endpoint
public class BookEndpoint {
private static final String NAMESPACE_URI = "http://jschmitz.dev/spring-boot-webserviceservertest/webservice/model";
private final BookRepository bookRepository;
public BookEndpoint(BookRepository bookRepository) {
this.bookRepository = bookRepository;
}
@PayloadRoot(namespace = NAMESPACE_URI, localPart = "getBooksRequest")
@ResponsePayload
public GetBooksResponse getBooks(){
var books = bookRepository.findAll();
var response = new GetBooksResponse();
response.getBooks().addAll(books);
return response;
}
@PayloadRoot(namespace = NAMESPACE_URI, localPart = "getBookRequest")
@ResponsePayload
public GetBookResponse getBookByIsbn(@RequestPayload GetBookRequest request){
var book = bookRepository.findByIsbn(request.getIsbn())
.orElseThrow(() -> new BookNotFoundException("Can not find Book with ISBN '%s'".formatted(request.getIsbn())));
var response = new GetBookResponse();
response.setBook(book);
return response;
}
}
The BookEndpoint
provides methods to retrieve books from the system. You can either retrieve a list of all Books or you can search for a specific book by its ISBN. If the provided ISBN does not match any book, an Exception is thrown. To keep this example as simple as possible the BookRepository
just holds an In-Memory List of books. In a real project, it would probably query a database.
Test Setup
Let’s start by adding a new dependency to our project:
<dependency>
<groupId>org.springframework.ws</groupId>
<artifactId>spring-ws-test</artifactId>
<scope>test</scope>
</dependency>
spring-ws-test
contains classes for testing servers that provide web services, clients that consume web services, and helper classes to create and validate SOAP messages.
With the dependency added we are ready to write our first test.
@WebServiceServerTest
class BookEndpointTest {
@Autowired
private MockWebServiceClient client;
@Test
void returnsListOfBooks() {
final var request = "<js:getBooksRequest"+
" xmlns:js=\"http://jschmitz.dev/spring-boot-webserviceservertest/webservice/model\""+
"/>";
this.client
.sendRequest(withPayload(new StringSource(request)))
.andExpect(noFault());
}
}
- By annotating the test class with
@WebServiceServerTest
Spring Boot will configure all beans related to web services - The
MockWebServiceClient
is a helper class from thespring-ws-test
dependency. We will use it to send requests to our web services and make assertions about the response. - The
RequestCreators::withPayload
method is used to create the content of the message we will send. ResponseMatchers::noFault
is used to make an assertion about the response we receive from the web service. In this case, we expect that the web service will not return an error.
Let’s keep going by extending this test. Currently, we only check that the web service does not return an error. We should also check, that the service returns the data we expect.
@WebServiceServerTest
class BookEndpointTest {
private static final Map<String, String> NAMESPACE_MAPPING = Map.ofEntries(
Map.entry("js", "http://jschmitz.dev/spring-boot-webserviceservertest/webservice/model")
);
@Autowired
private MockWebServiceClient client;
@MockBean
private BookRepository bookRepository;
private static Book createBook(String isbn, String title, String author) {
//helper to create a book
}
@Test
void returnsListOfBooks() {
var testBook = createBook("978-3-86490-525-4", "Spring Boot 2", "Michael Simons");
var testBook2 = createBook("978-3-86490-673-2", "Sustainable Software Architecture", "Carola Lilienthal");
when(bookRepository.findAll()).thenReturn(List.of(testBook, testBook2));
final var request = "<js:getBooksRequest"+
" xmlns:js=\"http://jschmitz.dev/spring-boot-webserviceservertest/webservice/model\""+
"/>";
this.client
.sendRequest(withPayload(new StringSource(request)))
.andExpect(noFault())
.andExpect(xpath("/js:getBooksResponse/js:books[1]/js:isbn", NAMESPACE_MAPPING).evaluatesTo(testBook.getIsbn()))
.andExpect(xpath("/js:getBooksResponse/js:books[1]/js:title", NAMESPACE_MAPPING).evaluatesTo(testBook.getTitle()))
.andExpect(xpath("/js:getBooksResponse/js:books[1]/js:author", NAMESPACE_MAPPING).evaluatesTo(testBook.getAuthor()))
.andExpect(xpath("/js:getBooksResponse/js:books[2]", NAMESPACE_MAPPING).exists());
}
- The mapping of an XML namespace to its URI. This will be used for the evaluation of an XPath later in the test.
- The mock of a
BookRepository
that will be injected into our web service. - We configure the behavior of our
BookRepository
mock. It should always return the list of books we specified. - We use
ResponseMatchers::xpath
to make expectations about the content of the message.
CAUTION: XPaths are one-based!
Conclusion
The WebServiceServerTest
is a neat addition to the existing test slices. In combination with the helper classes provided by springs-ws-test
, testing your web services becomes fairly easy. This is especially true if you already have experience with MockMvcTest
because they have a similar approach.
Resources
- you can find the code of this blog post in this GitHub repository
- have a look at Spring Boot’s documentation for more official information about
@WebServiceServerTest
- do you want to implement your own SOAP web service? Here is Spring’s official guide