Spring Boot & Apache CXF – XML-Validierung und Custom SOAP Faults

5 Kommentare

XML? Das war doch dieses wunderbar validierbare Datenformat! Einfach gegen das XML-Schema validieren und… ja, was und? Wie sieht die Reaktion darauf denn aus? In den meisten Fällen wollen oder müssen wir genau diese Reaktion in Form eines Custom SOAP Faults gestalten. Aber wie funktioniert das mit Spring Boot & Apache CXF?

Spring Boot & Apache CXF – Tutorial

Part 1: Spring Boot & Apache CXF – SOAP ohne XML?
Part 2: Spring Boot & Apache CXF – SOAP-Webservices testen
Part 3: Spring Boot & Apache CXF – XML-Validierung und Custom SOAP Faults
Part 4: Spring Boot & Apache CXF – Logging & Monitoring mit Logback, Elasticsearch, Logstash & Kibana
Part 5: Spring Boot & Apache CXF – Von 0 auf SOAP mit dem cxf-spring-boot-starter

In den vorangegangenen Artikeln haben wir gelernt, wie man einen SOAP Service mit Spring Boot und Apache CXF bereitstellt und ausführlich testet. Nun wollen wir uns einer eher speziellen Anforderung widmen. So manche 200 Seiten starke Webservice-Spezifikation fordert nämlich (z.B. auch die der BiPro), dass unser Endpoint in jeder Situation mit einer XML-Schema-konformen Antwort reagiert – egal, ob es sich dabei um eine erfolgreiche Verarbeitung des Requests oder um einen Fehler handelt.

Nun ist der SOAP Response bei einer erfolgreichen Verarbeitung immer 100% XML-Schema-konform, wenn wir wie im ersten Teil der Blogserie beschrieben auf Basis generierter Java-Klassen arbeiten, die von der WSDL und den XSDs abgeleitet sind. Um das auszuprobieren, gibt es natürlich wie immer ein neues GitHub-Projekt in unserem Tutorial Repository. 🙂

An dieser Stelle nutzen wir aber vorerst das Projekt des vorangegangenen Tutorial-Parts und starten einfach die SimpleBootCxfApplication per „Run as…“. Sobald unser SOAP Endpoint unter http://localhost:8080/soap-api/WeatherSoapService_1.0 läuft, schicken wir per SoapUI einen validen Request dagegen:

<soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/" xmlns:gen="http://www.codecentric.de/namespace/weatherservice/general">
   <soapenv:Header/>
   <soapenv:Body>
      <gen:GetCityForecastByZIP>
         <gen:ForecastRequest>
            <gen:ZIP>99998</gen:ZIP>
            <gen:flagcolor>bluewhite</gen:flagcolor>
            <gen:productName>ForecastProfessional</gen:productName>
            <gen:ForecastCustomer>
            <gen:Age>30</gen:Age>
            <gen:Contribution>5000</gen:Contribution>
            <gen:MethodOfPayment>Paypal</gen:MethodOfPayment>
            </gen:ForecastCustomer>
         </gen:ForecastRequest>
      </gen:GetCityForecastByZIP>
   </soapenv:Body>
</soapenv:Envelope>

Das Ergebnis ist ein ebenfalls valider SOAP-Response, der in etwa so aussieht:

<soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/">
   <soap:Body>
      <GetCityForecastByZIPResponse xmlns="http://www.codecentric.de/namespace/weatherservice/general" xmlns:ns2="http://www.codecentric.de/namespace/weatherservice/datatypes" xmlns:xmime="http://www.w3.org/2005/05/xmlmime" xmlns:ns4="http://www.codecentric.de/namespace/weatherservice/exception">
         <GetCityForecastByZIPResult>
            <Success>true</Success>
            <State>Deutschland</State>
            <City>Weimar</City>
            <WeatherStationCity>Weimar</WeatherStationCity>
            <ForecastResult>
               <ns2:Forecast>
                  <ns2:Date>2016-06-06T17:17:06.903+02:00</ns2:Date>
                  <ns2:WeatherID>0</ns2:WeatherID>
                  <ns2:Desciption>Vorhersage für Weimar</ns2:Desciption>
                  <ns2:Temperatures>
                     <ns2:MorningLow></ns2:MorningLow>
                     <ns2:DaytimeHigh>90°</ns2:DaytimeHigh>
                  </ns2:Temperatures>
                  <ns2:ProbabilityOfPrecipiation>
                     <ns2:Nighttime>5000%</ns2:Nighttime>
                     <ns2:Daytime>22%</ns2:Daytime>
                  </ns2:ProbabilityOfPrecipiation>
               </ns2:Forecast>
            </ForecastResult>
         </GetCityForecastByZIPResult>
      </GetCityForecastByZIPResponse>
   </soap:Body>
</soap:Envelope>

Standard-SOAP-Faults

Nähert man sich dem Thema das erste Mal, sucht man sicherlich nach Stichwörtern wie „Configure XML schema validation Apache CXF“ o.ä. Die Ergebnisse dieser Suche führen aber meist in die Irre, wie z.B. die Apache CXF FAQ. Denn man findet alle möglichen verschiedenen Varianten, wie man die XML-Schema-Validierung in Apache CXF aktiviert. Und das natürlich nahezu ausschließlich per Spring XML-Konfiguration, die wir ja CXF schon erfolgreich abgewöhnt haben. Das eigentliche Problem ist aber nicht die Aktivierung, sondern die Reaktion auf die Fehler, die bei der Validierung entstehen können. Denn witzigerweise ist die Valdierung in unserem Setup mit Spring Boot und CXF schon aktiviert und schlägt auch voll zu, sobald wir nicht-valides XML an unseren Endpoint schicken.

Im Fehlerfall packt CXF unseren Fehler erstmal in einen standardisierten SOAP Fault. Auch das probieren wir gleich aus. Diesmal schicken wir einen Request an unseren Endpoint, der dem XML-Schema nicht entsprechen dürfte – denn das Root-Element unseres SOAP Bodies muss eigentlich GetCityForecastByZIP heißen (siehe in die WSDL importierte weather-general.xsd). Da wir aber einen Fehler provozieren wollen, nennen wir das Tag einfach mal GetCityForecastByZIPfoo und schicken diese Anfrage gegen unseren Endpoint:

<?xml version="1.0" encoding="UTF-8"?>
<soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/" xmlns:gen="http://www.codecentric.de/namespace/weatherservice/general">
   <soapenv:Header/>
   <soapenv:Body>
      <gen:GetCityForecastByZIPfoo>
         <gen:ZIP>99425</gen:ZIP>
      </gen:GetCityForecastByZIPfoo>
   </soapenv:Body>
</soapenv:Envelope>

Unser gestarteter Endpoint antwortet daraufhin mit folgendem SOAP Response:

<soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/">
   <soap:Body>
      <soap:Fault>
         <faultcode>soap:Client</faultcode>
         <faultstring>Unexpected wrapper element {http://www.codecentric.de/namespace/weatherservice/general}GetCityForecastByZIPfoo found.   Expected {http://www.codecentric.de/namespace/weatherservice/general}GetCityForecastByZIP.</faultstring>
      </soap:Fault>
   </soap:Body>
</soap:Envelope>

Nun definiert aber unsere Web-Service-Spezifikation einen eigenen Exception-Typ für den Fehlerfall – nämlich die WeatherException, beschrieben in der ebenfalls importierten weather-exception.xsd. Sie wird mit dem Tag wsdl:fault in der WSDL an die Operationen gehängt und definiert folgende Elemente:

<s:element name="WeatherException">
    <s:complexType>
        <s:sequence>
            <s:element name="Uuid" type="s:string"/>
            <s:element name="timestamp" type="s:dateTime"/>
            <s:element name="businessErrorId" type="s:string"/>
            <s:element name="bigBusinessErrorCausingMoneyLoss" type="s:boolean"/>
            <s:element name="exceptionDetails" type="s:string"/>
        </s:sequence>
    </s:complexType>
</s:element>

Das Element WeatherException und seine Unterelemente sollen dabei unter dem soap:Fault-Element innerhalb des Tags detail auftauchen, sagt unsere Spezifikation. Solche Vorgaben kommen in ähnlicher Form auch gern in „Enterprise-WSDLs“ vor. Um einen Spezifikations-konformen SOAP Endpoint anbieten zu können, müssen wir die Anforderung umsetzen.

Nicht-XML-Schema-valide vs. invalides XML

Unsere WeatherException als soap:Fault/detail soll übrigens in jedem Fehlerfall zurückgeliefert werden. Es reicht also nicht aus, nur die Fälle abzudecken, in denen es sich bei der Anfrage um nicht-XML-Schema-konforme Requests handelt – sondern eben auch, wenn vollständig kaputtes XML gesendet wird. Beispiele sind hier Anfragen mit defektem XML-Header (fehlende spitze Klammer am Ende):

<?xml version="1.0" encoding="UTF-8"?

…nicht geschlossene Tags irgendwo im Dokument:

<?xml version="1.0" encoding="UTF-8"?>
<soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/" xmlns:gen="http://www.codecentric.de/namespace/weatherservice/general">
   <soapenv:Header/>
   <soapenv:Body>
      notRelevantHere />
   </soapenv:Body>
</soapenv:Envelope>

…kaputte SOAP-Header:

<?xml version="1.0" encoding="UTF-8"?>
<soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/" xmlns:gen="http://www.codecentric.de/namespace/weatherservice/general">
   <soapenv:Header/
   <soapenv:Body>
   ...

und noch einige mehr.

Und hier ist die Vorgabe, diese potentiellen Fehlerquellen vernünftig abzufangen, auch wirklich sinnvoll. Vor allem, wenn wir den Ideen aus Teil 1 des Tutorials folgen. Denn hier verlassen wir uns darauf, XML-Schema-konforme Nachrichten zu verarbeiten – die natürlich auch valides XML an sich sein müssen. Unser Framework reagiert nämlich mit allen möglichen kryptischen Fehlermeldungen, wenn die Anfragen nicht valide sind.

Apache CXF Interceptor Chains

O.K. Wie können wir das Problem lösen? Es gibt wie immer mehrere Wege nach Rom. Aber ich will hier eine Möglichkeit vorschlagen, die sich bis jetzt in Projekten sehr gut bewährt hat. Dazu müssen wir uns kurz die Architektur von Apache CXF anschauen.

In der Architekturdoku wird gezeigt, dass Apache CXF auf nacheinander ablaufende Interceptors setzt. Diese sind wie in einer Kette aufgereit und in Phasen organisiert. Es gibt dabei eine Incoming Interceptor Chain, die an ihrem Ende die eigentliche Web-Service-Implementierung aufruft, sowie eine Outgoing Interceptor Chain, die die Antwortverarbeitung übernimmt. Da ein Bild Konzepte meist viel schneller erklärt, schauen wir uns einfach kurz das folgende an:

apache_cxf_interceptor

Die eingehenden Web-Service-Calls durchlaufen immer diese Ketten. Dabei führt ein Fehler in einer Phase (sprich: in einem Interceptor) der Incoming Chain dazu, dass die Outgoing Chain mit den Fehlerinformationen versorgt wird und dann wieder in entgegengesetzter Richtung abgearbeitet wird. Tritt also z.B. ein Fehler in der Phase UNMARSHAL auf, werden die nachfolgenden Phasen der Incoming Chain nicht mehr aufgerufen. Für Fehlerfälle gibt es übrigens in CXF eine spezielle Outgoing Fault Interceptor Chain, die bei allen Fehlern aufgerufen wird. In alle CXF Interceptor Chains kann man sich „einhängen“ und auf spezifische Ereignisse reagieren.

Dieses Wissen können wir uns zu Nutze machen. Denn wenn eine Anfrage kein korrektes XML ist, fliegt die Incoming Chain spätestens in der Phase UNMARSHAL auf die Nase und ruft die Outgoing Fault Interceptor Chain auf. Wir müssen also nur einen Interceptor implementieren, der möglichst alle Fehler „mitbekommt“ und darauf reagieren kann. Eine ideale Phase dafür wäre z.B. die org.apache.cxf.phase.Phase.PRE_STREAM – dann sind wir so weit wie möglich „vorne“ in der Kette, um praktisch jeden Fehler abzufangen. Unseren Interceptor müssen wir von org.apache.cxf.binding.soap.interceptor.AbstractSoapInterceptor ableiten und die Methode void handleMessage(T message) throws Fault überschreiben. Zusätzlich übergeben wir im Konstruktur per super()-Methode unsere Phase:

public class CustomSoapFaultInterceptor extends AbstractSoapInterceptor {
 
    private static final SoapFrameworkLogger LOG = SoapFrameworkLogger.getLogger(CustomSoapFaultInterceptor.class);
 
    public CustomSoapFaultInterceptor() {
        super(Phase.PRE_STREAM);
    }
 
    @Override
    public void handleMessage(SoapMessage soapMessage) throws Fault {
        Fault fault = (Fault) soapMessage.getContent(Exception.class);
        Throwable faultCause = fault.getCause();
        String faultMessage = fault.getMessage();
 
        if (containsFaultIndicatingNotSchemeCompliantXml(faultCause, faultMessage)) {  
            WeatherSoapFaultHelper.buildWeatherFaultAndSet2SoapMessage(soapMessage, FaultConst.SCHEME_VALIDATION_ERROR);
        }
        else if (containsFaultIndicatingSyntacticallyIncorrectXml(faultCause)) {
            WeatherSoapFaultHelper.buildWeatherFaultAndSet2SoapMessage(soapMessage, FaultConst.SYNTACTICALLY_INCORRECT_XML_ERROR);          
        }
    }
 
    ...

XML-Validierungsfehler erkennen

In der überschriebenen Methode handleMessage(SoapMessage soapMessage) extrahieren wir uns zuerst den faultCause und die faultMessage. Die letztere ist übrigens 1:1 im Standard SOAP Fault im Tag faultstring zu finden. Anhand dieser beiden Variablen können wir erkennen, um welchen Fehler es sich handelt.

Leider bietet die CXF-API uns hierbei keine Hilfe, und wir müssen die Methoden containsFaultIndicatingNotSchemeCompliantXml() und containsFaultIndicatingSyntacticallyIncorrectXml() selber implementieren. Um herauszufinden, wie Apache CXF auf nicht XML-Schema-konformes oder invalides XML reagiert, kann man sich alle möglichen Testfälle erstellen und sie gegen den SOAP Endpoint schicken. Das ist natürlich etwas aufwändig und mühselig. Es gibt aber schon eine ganze Reihe an Testfällen in unserem Beispielprojekt, die wir dazu verwenden können. Probieren wir alle Fälle durch, so kristallisieren sich folgende Muster heraus, die wir in unsere Prüfungsroutinen gießen:

1. nicht XML-Schema konform

Ist die Anfrage kein dem Schema entsprechendes XML, so enthält der faultCause eine javax.xml.bind.UnmarshalException. Zusätzlich prüfen wir noch, ob es ein fehlendes geschlossenes Tag gibt. Dann baut Apache CXF statt der UnmarshalException in die Message ein „Unexpected wrapper element“ ein:

private boolean containsFaultIndicatingNotSchemeCompliantXml(Throwable faultCause, String faultMessage) {
    if(faultCause instanceof UnmarshalException
        // 1.) If the root-Element of the SoapBody is syntactically correct, but not scheme-compliant,
        //      there is no UnmarshalException and we have to look for
        // 2.) Missing / lead to Faults without Causes, but to Messages like "Unexpected wrapper element XYZ found. Expected"
        //      One could argue, that this is syntactically incorrect, but here we just take it as Non-Scheme-compliant
        || isNotNull(faultMessage) && faultMessage.contains("Unexpected wrapper element")) {
        return true;
    }
    return false;
}

2. generell invalides XML

Die Fälle, in denen es sich um invalides XML an sich handelt, sind durch drei mögliche Fehler charakterisiert. Entweder unser faultCause ist eine com.ctc.wstx.exc.WstxException, die gewrappte Cause ist eine com.ctc.wstx.exc.WstxUnexpectedCharException oder die faultCause enthält eine IllegalArgumentException:

private boolean containsFaultIndicatingSyntacticallyIncorrectXml(Throwable faultCause) {
    if(faultCause instanceof WstxException
        // If Xml-Header is invalid, there is a wrapped Cause in the original Cause we have to check
        || isNotNull(faultCause) && faultCause.getCause() instanceof WstxUnexpectedCharException
        || faultCause instanceof IllegalArgumentException) {
        return true;
    }
    return false;
}

Custom SOAP Fault bauen

So weit, so gut. Die Klasse WeatherSoapFaultHelper baut den SOAP Fault nach unseren Wünschen um. Sie nutzt wiederum die Klasse WeatherOutError aus dem Package transformation, um die eigentliche WeatherException zu erstellen, die sich unsere Spezifikation im detail Tag des soap:Fault-Elements wünscht:

private static final de.codecentric.namespace.weatherservice.exception.ObjectFactory objectFactoryDatatypes = new de.codecentric.namespace.weatherservice.exception.ObjectFactory();
 
public static WeatherException createWeatherException(FaultConst faultContent, String originalFaultMessage) {
    // Build SOAP-Fault detail <datatypes:WeatherException>
    WeatherException weatherException = objectFactoryDatatypes.createWeatherException();        
    weatherException.setBigBusinessErrorCausingMoneyLoss(true);
    weatherException.setBusinessErrorId(faultContent.getId());
    weatherException.setExceptionDetails(originalFaultMessage);
    weatherException.setUuid("ExtremeRandomNumber");
    return weatherException;
}

Ein Detail ist noch interessant zu bemerken: Apache CXF schmeißt aus unerfindlichen Gründen das root-Element der Exception bzw. des Stückchens XML weg, was man ihm in das soap:Fault/detail setzen will. Deshalb schauen wir nochmal kurz in die Klasse WeatherSoapFaultHelper (das Exception-Handling ist hier zur besseren Übersicht entfernt):

public static void buildWeatherFaultAndSet2SoapMessage(SoapMessage message, FaultConst faultContent) {
	Fault exceptionFault = (Fault) message.getContent(Exception.class);
	String originalFaultMessage = exceptionFault.getMessage();
	exceptionFault.setMessage(faultContent.getMessage());
	exceptionFault.setDetail(createFaultDetailWithWeatherException(originalFaultMessage, faultContent));
	message.setContent(Exception.class, exceptionFault);
}
 
private static Element createFaultDetailWithWeatherException(String originalFaultMessage,  FaultConst faultContent) {
	Document weatherExcecption = XmlUtils.marhallJaxbElementIntoDocument(WeatherOutError.createWeatherException(faultContent, originalFaultMessage));
	return XmlUtils.appendAsChildElement2NewElement(weatherExcecption);
}

Die Methode buildWeatherFaultAndSet2SoapMessage(SoapMessage message, FaultConst faultContent) extrahiert sich zuerst den org.apache.cxf.interceptor.Fault aus der org.apache.cxf.binding.soap.SoapMessage. Dem Fault kann man jetzt die gewünschte Message sowie das Detail (also unsere WeatherException) setzen. Da die Methode Fault.setDetail() ein org.w3c.dom.Element erwartet, lassen wir unsere erstellte WeatherException in ein org.w3c.dom.Document marshallen (wieder mithilfe der XmlUtils, deren Hilfe wir schon im letzten Artikel benötigten). Dem Ergebnis stellen wir ein pseudo root-Element voran, das dann später von CXF wieder weggeworfen werden kann.

Können wir Tests mit invaliden XML-Requests bauen?

Nun haben wir also eine Implementierung, die invalides XML in all seinen Erscheinungsformen erkennen soll. Außerdem haben wir einen Haufen Testdateien, die wir per Hand gegen die Implementierung (z.B. per SoapUI) schicken können. Doch der Autor kann uns ja ziemlich viel erzählen. 🙂 Und wer sagt schon, dass die Implementierung beim nächsten kleinen Versionssprung von CXF oder genutzten Libraries noch funktioniert? Ist ja doch eine Art Sonderlocke.

Hier kommt das Wissen ins Spiel, das wir im letzten Artikel dieser Serie gewonnen haben – wir schreiben uns einfach automatisiert ausführbare Tests. Am besten Single System Integration Tests, dann wird der Server sogar noch innerhalb der Testausführung hoch- und auch wieder heruntergefahren.

Und wie wir im Abschnitt Umgang mit Testfällen gelesen haben, können wir uns ja sogar die Testdateien laden und direkt in das passende Objekt marshallen lassen. Oder doch nicht?!? Nein, Sie werden es schon erraten haben. Denn wir wollen ja Anfragen verschicken, die kein valides XML enthalten. Gehen wir hier mit JAX-B ran, so fällt uns der Parser mit ähnlichen Exceptions um, wie sie Apache CXF in seinen Outbound Chains im Fehlerfall wirft.

Trotzdem wollen wir automatisiert testen. Das geht natürlich auch. Wir müssen uns dafür nur bewusst machen, was wir hierfür benötigen. Denn im Grunde senden wir einfach über HTTP per POST unsere SOAP-Textnachrichten an den Endpoint. Und einen ausgereiften HTTP-Client vorausgesetzt, können wir das auch für unsere „kaputten“ Testfälle verwenden. Also los! Wir erweitern dazu unsere pom um zwei neue Dependencies: org.apache.httpcomponents.httpclient und org.apache.httpcomponents.fluent-hcs. Bevor wir allerdings unseren HTTP-Client benutzen, schauen wir uns eine SOAP-1.1-konforme Nachricht inklusive aller HTTP-Header an (an die kommen wir z.B. per SoapUI im Reiter „Raw“):

POST http://localhost:8080/soap-api/WeatherSoapService_1.0 HTTP/1.1
Accept-Encoding: gzip,deflate
Content-Type: text/xml;charset=UTF-8
SOAPAction: "http://www.codecentric.de/namespace/weatherservice/GetCityForecastByZIP"
Content-Length: 289
Host: localhost:8080
Connection: Keep-Alive
User-Agent: Apache-HttpClient/4.1.1 (java 1.5)
 
<?xml version="1.0" encoding="UTF-8"?>
<soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/" xmlns:gen="http://www.codecentric.de/namespace/weatherservice/general">
   <soapenv:Header/>
   <soapenv:Body>
      notRelevantHere />
   </soapenv:Body>
</soapenv:Envelope>

Neben dem Content-Type ist hier besonders ein Header-Feld wichtig – nämlich die SOAPAction. Laut SOAP-Spezifikation muss darin die SOAP-Operation beschrieben werden, und unser SOAP Endpoint meckert, wenn wir diese nicht korrekt setzen. Doch welcher Wert muss darin stehen? Das definiert die WSDL im Attribut soapAction des Tags soap:operation innerhalb der wsdl:operation-Definitionen. Nutzen wir also für unsere Tests den HTTP-Client (z.B. per eleganter Fluent-API), so müssen wir den SOAPAction-HTTP-Header richtig setzen. Dazu gehören auch die Quotes, die wir escapen müssen:

Response httpResponseContainer = Request
            .Post("http://localhost:8090/soap-api/WeatherSoapService_1.0")
            .bodyStream(xmlFile, ContentType.create(ContentType.TEXT_XML.getMimeType(), Consts.UTF_8))
            .addHeader("SOAPAction", "\"http://www.codecentric.de/namespace/weatherservice/GetCityForecastByZIP\"")
            .execute();
 
HttpResponse httpResponse = httpResponseContainer.returnResponse();

Diese paar Zeilen Code reichen schon aus, damit wir unseren Endpoint mit invaliden XML-Anfragen traktieren können. Etwas erweitert tut das auch die Klasse SoapRawClient in unserem Beispielprojekt. Sie konfigurieren wir in der WebServiceSystemTestConfiguration als Spring Bean und übergeben ihr dabei unser generiertes Service Endpoint Interface (SEI). Denn aus dem SEI kann die Klasse den SOAPAction-Header dynamisch ableiten. Außerdem liefert der Aufruf der Methode callSoapService(InputStream xmlFile) ein Objekt der Klasse SoapRawClientResponse, das uns die Erstellung der Testfälle nochmals stark vereinfacht.

Single System Integration Tests mit invaliden XML-Requests

Jetzt haben wir das Werkzeug beisammen, um endlich die ersehnten Testfälle schreiben zu können. Dazu setzen wir auf das Wissen aus dem vorangegangenen Artikel über Single System Integration Tests auf (siehe Part 2) – denn diese fahren unseren SOAP Endpoint für die Dauer der Testausführung automatisiert hoch. Zusätzlich wissen wir, wie wir XML-Testfälle elegant per org.springframework.core.io.Resource innerhalb unseres Tests als InputStream bereitstellen können, ohne uns mit dem Filehandling abmühen zu müssen.

Unser Testfall WeatherServiceXmlErrorSystemTest verwendet die gleichen Mechanismen wie der WeatherServiceXmlFileSystemTest aus dem letzten Tutorial. Neben der Injektion des SoapRawClient laden wir unsere Testfälle:

@RunWith(SpringJUnit4ClassRunner.class)
@SpringApplicationConfiguration(classes=SimpleBootCxfSystemTestApplication.class)
@WebIntegrationTest("server.port:8090") 
public class WeatherServiceXmlErrorSystemTest {
 
    @Autowired private SoapRawClient soapRawClient;
 
    @Value(value="classpath:requests/xmlerrors/xmlErrorNotXmlSchemeCompliantUnderRootElementTest.xml")
    private Resource xmlErrorNotXmlSchemeCompliantUnderRootElementTestXml;
 
    @Value(value="classpath:requests/xmlerrors/xmlErrorSoapBodyTagMissingBracketTest.xml")
    private Resource xmlErrorSoapBodyTagMissingBracketTestXml;
 
    // ... and many more

Danach gönnen wir jedem Test-File auch eine eigene Testmethode, die auf eine generalisierte Methode checkXmlError() verweist. Dieser wird das entsprechende Test-File sowie die Art des erwarteten Fehlers übergeben. Der erwartete Fehler wird übrigens in der FaultConst definiert, den wir natürlich auch schon im Abschnitt „XML-Validierungsfehler erkennen“ verwenden, um unseren SOAP Fault aufzubauen:

@Test
public void xmlErrorNotXmlSchemeCompliantUnderRootElementTest() throws InternalBusinessException, IOException {
    checkXMLError(xmlErrorNotXmlSchemeCompliantUnderRootElementTestXml, FaultConst.SCHEME_VALIDATION_ERROR);
}
 
@Test
public void xmlErrorSoapBodyTagMissingBracketTest() throws InternalBusinessException, IOException {
    checkXMLError(xmlErrorSoapBodyTagMissingBracketTestXml, FaultConst.SYNTACTICALLY_INCORRECT_XML_ERROR);
}
 
// ... and many more

In der Methode checkXmlError() können wir unseren gewünschten SOAP Fault nun auf Herz und Nieren prüfen. Unter anderem erwarten wir einen HTTP-Status-Code 500 und dass unsere Message aus der FaultConst im Tag faultstring auftaucht. Hierzu nutzen wir die Hilfsmethode getFaultstringValue() unseres SoapRawClientResponse, die uns den Faultstring aus der Http-Message fischt. Außerdem bietet die Klasse noch eine praktische getUnmarshalledObjectFromSoapMessage(Class jaxbClass), die auch noch unsere WeatherException aus der Http-Message holt. Darauf können wir dann alle nötigen assert-Statements loslassen.

private void checkXmlError(Resource testFile, FaultConst faultContent) throws InternalBusinessException, IOException {
    // When
    SoapRawClientResponse soapRawResponse = soapRawClient.callSoapService(testFile.getInputStream());
 
    // Then
    assertNotNull(soapRawResponse);
    assertEquals("500 Internal Server Error expected", 500, soapRawResponse.getHttpStatusCode());
    assertEquals(faultContent.getMessage(), soapRawResponse.getFaultstringValue());
 
    de.codecentric.namespace.weatherservice.exception.WeatherException weatherException = soapRawResponse.getUnmarshalledObjectFromSoapMessage(de.codecentric.namespace.weatherservice.exception.WeatherException.class);       
    assertNotNull("<soap:Fault><detail> has to contain a de.codecentric.namespace.weatherservice.exception.WeatherException",  weatherException);
 
    assertEquals("ExtremeRandomNumber", weatherException.getUuid());
    assertEquals("The correct BusinessId is missing in WeatherException according to XML-scheme.", faultContent.getId(), weatherException.getBusinessErrorId());
}

Wichtig hierbei: Es hat sich bewährt, hier den vollqualifizierten Namen der Exception (de.codecentric.namespace.weatherservice.exception.WeatherException) anzugeben, da es leicht zu Verwirrung mit der zweiten gleichlautenden Exception (de.codecentric.namespace.weatherservice.WeatherException) kommen kann. Jetzt könnte man einwenden, dass man dann doch bitte schön die Exceptions verschieden benennen sollte. Aber genau so findet man es leider auch in den großen Enterprise-Web-Services vor – wie das Beispiel BiPro zeigt.

Nun haben wir also, was wir brauchen: Unsere Implementierung validiert die XML-Anfragen, und wir bestimmen, was in den SOAP Fault kommt. Gleichzeitig können wir all dies automatisiert testen und sind in der Lage, maschinell beliebig schräge Anfragen gegen unseren SOAP Endpoint zu schicken. Insgesamt ist die Lösung allerdings aus Sicht eines Nutzers von Apache CXF recht komplex. Hier könnten die Entwickler nachlegen und mithilfe einer einfachen Erweiterungsmöglichkeit die Konfiguration und Erstellung von Custom SOAP Faults erleichtern.

Wer hätte es anders gedacht – es bleiben immer noch offene Punkte übrig. 🙂 Seien es Namespaces oder fachliches Monitoring per elastic Stack – in den folgenden Posts schauen wir uns diese Themen an.

Jonas Hecht

Die Überzeugung, dass Softwarearchitektur und Hands-on Entwicklung zusammengehören, führte Jonas zu codecentric. Tiefgreifende Erfahrungen in allen Bereichen der Softwareentwicklung großer Unternehmen treffen auf Leidenschaft für neue Technologien. Im Fokus stand dabei immer die Integration verschiedenster Systeme (und Menschen).

Share on FacebookGoogle+Share on LinkedInTweet about this on TwitterShare on RedditDigg thisShare on StumbleUpon

Kommentare

  • 28. September 2016 von Thomas Arand

    Im Satz

    Unser Testfall WeatherServiceXmlErrorSystemTest verwendet die gleichen Mechanismen wie der WeatherServiceXmlErrorSystemTest aus dem letzten Tutorial.

    gibt es eine Referenz auf denselben Testfall. Ich vermute, dass

    Unser Testfall WeatherServiceXmlErrorSystemTest verwendet die gleichen Mechanismen wie der WeatherServiceXmlFileSystemTest aus dem letzten Tutorial.

    gemeint ist.

  • Hi, ich versuche gerade zu erreichen, dass

    de.codecentric.namespace.*

    gefunden wird… Wäre für einen kleinen Tipp sehr dankbar!

    • OK, die generierten sources sind auch excluded bei mir…

    • Jonas Hecht

      4. November 2016 von Jonas Hecht

      Ich vermute die Frage zielt darauf ab, wie die aus den WSDL/XSDs per JAX-B generierten Klassen in den classpath eingebunden werden – so dass sie von den anderen Klassen genutzt werden können?! Dass die IDE also nichts zu meckern hat…

      Das erledigt das add-source goal des build-helper-maven-plugin, dass den Ordner mit den generierten Klassen als Source Folder hinzufügt. Dieses ist in der pom bereits definiert:

      <plugin>
          <groupId>org.codehaus.mojo</groupId>
          <artifactId>build-helper-maven-plugin</artifactId>
          <executions>
            <execution>
              <id>add-source</id>
              <phase>generate-sources</phase>
              <goals>
                <goal>add-source</goal>
              </goals>
              <configuration>
                <sources>
                  <source>target/generated-sources/wsdlimport/Weather1.0</source>
                </sources>
              </configuration>
            </execution>
          </executions>
      </plugin>

      Je nach IDE muss dazu manchmal nochmal manuell die generate-sources lifecycle ausgeführt werden. In IntelliJ beispielsweise genügt ein Rechtsklick auf den root Projektordner und ein Klick auf Maven/Generate Sources and Update Folders. Das funktioniert sogar manchmal nur manuell – was komisch ist, da die Maven-Integration von IntelliJ eigentlich sehr gut ist.

      In Eclipse führt ein mvn clean eclipse:eclipse meist zum Ziel…

Kommentieren

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert.