Testowanie JAX-WS Web Services z Apache CXF i JUnit

Z Jacek Laskowski - Wiki Projektanta Java EE

Podczas rozpoznania Apache CXF przyszło mi zetknąć się z problemem uruchomienia testowego usług sieciowych (ang. web services) poza zewnętrznym, jawnie uruchamianym serwerem aplikacyjnym. Pomysł polegał na zestawieniu minimalnej konfiguracji uruchomieniowej do testowania usług, co w zamyśle miałoby uprościć tworzenie testów integracyjnych z JUnit. Najbardziej zależało mi na konfiguracji, w której nie byłoby koniecznym uruchomienie zewnętrznego serwera aplikacyjnego (np. GlassFish, JBoss AS, WAS) czy prostszego funkcjonalnie kontenera servletów (np. Apache Tomcat, Jetty) jawnie, a oparciu testowania na pewnym, automatycznie uruchamianym środowisku uruchomieniowym. Okazało się, że taka cecha istnieje w CXF na bazie wbudowanego kontenera Jetty.

W środowisku swoje miejsce znalazły:

Spis treści

Utworzenie usługi

Zakładam projekt zarządzany przez Apache Maven korzystając ze wsparcia wtyczki m2eclipse. W ten sposób łatwiej jest mi zarządzać zależnościami, jak i całym projektem.

Plik:cxf-testing-maven-project.png

W kolejnym kroku tworzę usługę począwszy od kodu w Javie, tzw. code-first approach lub bottom-up approach.

Interfejs usługi - SEI - pl.japila.cxf.BookstoreSEI

Jakkolwiek liczbę klas możemy sprowadzić do jednej, w której byłyby interfejs usługi i jej implementacja, to zgodnie ze sztuką zaleca się najpierw utworzenie SEI (ang. service endpoint interface), czyli interfejsu usługi opisanego przez interfejs javowy. Oddzielenie interfejsu od implementacji ma tę zaletę, że upraszcza późniejsze tworzenie klienta usługi i to nie dotyczy wyłącznie CXF czy JAX-WS w szczególności, ale ogólnego podejścia do tworzenia oprogramowania "kontraktowo" (na bazie interfejsów).

package pl.japila.cxf;
 
import javax.jws.WebParam;
import javax.jws.WebService;
 
@WebService
public interface BookstoreSEI {
    public boolean isAvailable(@WebParam(name="book") Book book);
}

To właśnie adnotacja @WebService instruuje CXF (i inne implementacje specyfikacji JAX-WS), że mamy do czynienia z usługą sieciową - jej SEI lub implementacją.

Powodem użycia nieobowiązkowej adnotacji @WebParam jest określenie nazwy parametru operacji usługowej, która w przeciwnym przypadku byłaby nazwana arg0. Wynika to z faktu, że skompilowany interfejs javowy nie zawiera informacji o nazwach parametrów metod (w naszym przypadku book) i albo polegamy na domyślnej, stosunkowo tajemniczej nazwie, albo nadajemy ją zgodnie z naszym upodobaniem.

Klasa pomocnicza - pl.japila.cxf.Book

W SEI korzystam z klasy pl.japila.cxf.Book, w której zdefiniowałem prywatny bezparametrowy konstruktor wyłącznie na potrzeby kolejnej specyfikacji JAXB oraz konstruktor publiczny. Tym samym łączę wymagania JAXB oraz wymaganie, aby każdy stworzony jawnie obiekt typu Book miał określone pola title oraz author. Innymi słowy, nie zgadzam się na wystąpienie sytuacji, w której możliwe jest utworzenie książki bez tytułu i autora.

package pl.japila.cxf;
 
public class Book {
    int id;
    String title;
    String author;
 
    // To satisfy JAX-WS runtime
    @SuppressWarnings("unused")
    private Book() {
    }
 
    public Book(String title, String author) {
        this.title = title;
        this.author = author;
    }
 
    public int getId() {
        return id;
    }
 
    public void setId(int id) {
        this.id = id;
    }
 
    public String getTitle() {
        return title;
    }
 
    public void setTitle(String title) {
        this.title = title;
    }
 
    public String getAuthor() {
        return author;
    }
 
    public void setAuthor(String author) {
        this.author = author;
    }
 
}

Implementacja usługi - pl.japila.cxf.impl.BookstoreWJUG

Do kompletu potrzeba mi implementacji usługi - klasy pl.japila.cxf.impl.BookstoreWJUG.

package pl.japila.cxf.impl;
 
import pl.japila.cxf.Book;
import pl.japila.cxf.BookstoreSEI;
 
public class BookstoreWJUG implements BookstoreSEI {
    public boolean isAvailable(Book book) {
        String title = book.getTitle();
        if (title != null && title.toLowerCase().contains("cxf")) {
            return true;
        }
        return false;
    }
}

Warto zwrócić uwagę, że implementacja znajduje się w dedykowanym pakiecie pl.japila.cxf.impl, tak aby możliwe było jego ukrycie, np. przy zastosowaniu OSGi. W końcu implementacja powinna być prywatna, a jedynie interfejs-kontakt publiczny. Raz nauczywszy się nie zapominam o tym.

Konfiguracja Apache CXF - beans.xml

Uruchomienie usługi wymaga opisania jej w pliku konfiguracyjnym Apache CXF - beans.xml. Tak na prawdę, beans.xml definiuje przestrzeń komponentów Spring Framework, a pośrednio - przez jaxws:endpoint, jaxws:client oraz httpj:engine-factory - i Apache CXF.

Istotnymi elementami konfiguracji jest zdefiniowanie punktu serwisowego (przez użycie jaxws:endpoint), klienta (z jaxws:client) oraz niejawnego kontenera webowego (z httpj:engine-factory).

<?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:beans="http://www.springframework.org/schema/beans"
  xmlns:jaxws="http://cxf.apache.org/jaxws"
  xmlns:http="http://cxf.apache.org/transports/http/configuration"
  xmlns:httpj="http://cxf.apache.org/transports/http-jetty/configuration"
  xsi:schemaLocation="
http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
http://cxf.apache.org/jaxws http://cxf.apache.org/schemas/jaxws.xsd
http://cxf.apache.org/transports/http/configuration http://cxf.apache.org/schemas/configuration/http-conf.xsd
http://cxf.apache.org/transports/http-jetty/configuration http://cxf.apache.org/schemas/configuration/http-jetty.xsd
">
  <import
    resource="classpath:META-INF/cxf/cxf.xml" />
  <bean
    id="bookstoreWJUG"
    class="pl.japila.cxf.impl.BookstoreWJUG" />
  <jaxws:endpoint
    id="bookstoreService"
    implementor="#bookstoreWJUG"
    address="http://localhost:9000/bookstore" />
  <jaxws:client
    id="bookstoreClient"
    serviceClass="pl.japila.cxf.BookstoreSEI"
    address="http://localhost:9000/bookstore" />
  <httpj:engine-factory>
    <httpj:engine
      port="9000">
    </httpj:engine>
  </httpj:engine-factory>
</beans>

Więcej informacji na temat elementów jaxws:* znajdziesz w dokumentacji CXF - JAX-WS Configuration lub bezpośrednio w schemacie XML.

Klasa testowa - pl.japila.cxf.BookstoreWjugTest

Z tak przygotowanym projektem jestem gotów do uruchomienia testowego za pomocą JUnit. Tworzę klasę testową pl.japila.cxf.BookstoreWjugTest.

package pl.japila.cxf;
 
import static org.junit.Assert.assertTrue;
 
import org.junit.Before;
import org.junit.Test;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
 
public class BookstoreWjugTest {
 
    ApplicationContext context;
 
    @Before
    public void setUp() throws Exception {
        context = new ClassPathXmlApplicationContext(new String[] { "beans.xml" });
    }
 
    @Test
    public void shouldReturnTrueForCxfBook() throws Exception {
        Book book = new Book("Apache CXF Web Service Development", "Naveen Balani & Rajeev Hathi");
        BookstoreSEI bookstore = (BookstoreSEI) context.getBean("bookstoreClient");
        assertTrue("Book about CXF should be available", bookstore.isAvailable(book));
    }
 
}

W przeciwieństwie do poprzednich bytów projektowych - beans.xml i typów javowych - plik znajduje się w podkatalogu src/test/java (zgodnie z domyślną konfiguracją projektów mavenowych).

Uruchomienie w Eclipse IDE

Kompletny projekt przedstawia się jak na poniższym obrazku.

Plik:cxf-testing-complete-project-in-package-explorer.png

Uruchomienie sprowadza się do uruchomienia klasy testowej BookstoreWjugTest za pomocą Eclipse IDE - Run As > JUnit Test lub korzystając z mvn test. Skoro już korzystam z IDE, uruchamiam testy z jego pomocą.

Plik:cxf-testing-bookstorewjugtest-finished-green.png

W widoku Console możesz prześledzić wykonanie testu.

Apr 29, 2011 3:15:14 PM org.springframework.context.support.AbstractApplicationContext prepareRefresh
INFO: Refreshing org.springframework.context.support.ClassPathXmlApplicationContext@27bc82e7:
startup date [Fri Apr 29 15:15:14 CEST 2011]; root of context hierarchy
Apr 29, 2011 3:15:14 PM org.springframework.beans.factory.xml.XmlBeanDefinitionReader loadBeanDefinitions
INFO: Loading XML bean definitions from class path resource [beans.xml]
Apr 29, 2011 3:15:15 PM org.springframework.beans.factory.xml.XmlBeanDefinitionReader loadBeanDefinitions
INFO: Loading XML bean definitions from class path resource [META-INF/cxf/cxf.xml]
Apr 29, 2011 3:15:16 PM org.springframework.beans.factory.support.DefaultListableBeanFactory preInstantiateSingletons
INFO: Pre-instantiating singletons in org.springframework.beans.factory.support.DefaultListableBeanFactory@57c8b24d:
defining beans [cxf,org.apache.cxf.bus.spring.BusWiringBeanFactoryPostProcessor,org.apache.cxf.bus.spring.Jsr250BeanPostProcessor,
org.apache.cxf.bus.spring.BusExtensionPostProcessor,bookstoreWJUG,bookstoreService,bookstoreClient.proxyFactory,bookstoreClient,
org.apache.cxf.transport.http_jetty.spring.JettySpringTypesFactory,org.apache.cxf.transport.http_jetty.JettyHTTPServerEngineFactory];
root of factory hierarchy
Apr 29, 2011 3:15:16 PM org.apache.cxf.service.factory.ReflectionServiceFactoryBean buildServiceFromClass
INFO: Creating Service {http://impl.cxf.japila.pl/}BookstoreWJUGService from class pl.japila.cxf.BookstoreSEI
Apr 29, 2011 3:15:17 PM org.apache.cxf.endpoint.ServerImpl initDestination
INFO: Setting the server's publish address to be http://localhost:9000/bookstore
SLF4J: Failed to load class "org.slf4j.impl.StaticLoggerBinder".
SLF4J: Defaulting to no-operation (NOP) logger implementation
SLF4J: See http://www.slf4j.org/codes.html#StaticLoggerBinder for further details.
Apr 29, 2011 3:15:17 PM org.apache.cxf.service.factory.ReflectionServiceFactoryBean buildServiceFromClass
INFO: Creating Service {http://cxf.japila.pl/}BookstoreSEIService from class pl.japila.cxf.BookstoreSEI

Testy przechodzą, a ja mogę przejść do kolejnego zadania wokół Apache CXF. Ale o tym w kolejnym artykule.

Gorąco zachęcam do komentowania i przesyłania sugestii do kolejnych produktów literackich mojego pióra na adres jacek@japila.pl.

Osobiste