Automatisierte Integrationstests für Webservices

Einen Webservice im Integrationstest anzusprechen hat vieles mit dem berüchtigten Schuss auf ein bewegtes Ziel gemeinsam: der Webservice ist aus Sicht des Tests schwer berechenbar und flüchtig. Netzwerkprobleme, eine Änderungen an der Datenbasis (z.B. durch vergessene „Aufräumarbeiten“ nach einem Test) oder die Änderung der Schnittstelle können zum ungewollten Scheitern der Integrationstests führen. Vor allem wenn sich die Daten des Webservices oft ändern, wie es bei Information Retrieval- oder PIM Systemen der Fall ist, kann der Aufwand der betrieben werden muss um die Tests laufend anzupassen bald die durch eine Automatisierung erhoffte Zeitersparnis übersteigen.

Aber selbst wenn der Webservice recht statisch ist, ist die Zeit für einen HTTP Round-Trip, der vielleicht auch noch eine Datenbankquery anstößt, ein unliebsamer Flaschenhals, der die Testausführung ausbremst. Mit den richtigen Techniken und Tools kann man jedoch auch flüchtige Webservices zähmen und so den Aufwand automatisierter Tests auf ein Minimum beschränken.

Klassische Ansätze

Der klassische Ansatz einen Webservice für Testing in den Griff zu bekommen, ist es einen Testwebservice zu entwickeln. Wie ein Test Double Objekt implementiert dieser Webservice die gleiche Schnittstelle die der richtige Webservice. Ein entscheidender Unterschied zum Produktionssystem ist die Berechenbarkeit: der Nutzer kann sich darauf verlassen, dass die Antworten des Testwebservice wesentlich weniger Änderungen unterworfen sind als die Antworten des Produktivsystems.

Bei schlanken Systemen und einem Service ohne Seiteneffekten kann hierfür die gleiche Software wie für das Produktivsystem mit einigen repräsentativen Datensätzen des Produktivsystems verwendet werden. Aber auch bei Webservices, für die dieses Vorgehen nicht praktikbel ist, etwa weil der Webservice extern kontrolliert oder schwer anzupassen ist kann ein Webservice mit einem gleichen Interface, aber einer „falschen“, also z.B. hartcodierten Implementierung angebunden werden. Tools wie SoapUI bieten hierbei eine Unterstützung um etwa aus der WSDL Datei eines SOAP Webservices relativ schnell einen funktionierenden Testservice zu generieren. Für moderne REST Webservices bietet sich hier aber auch die Verwendung von Microframeworks wie Sinatra oder Flask an.

Leider unterliegen diese Testwebservice Instanzen wenn sie von mehreren Entwicklern benutzt werden immer noch einiger Einschränkungen: So können die Tests weiterhin fehlschlagen, wenn Entwickler ihrer Pflicht nach ihren Tests aufzuräumen nicht nachkommen, oder etwa die Netzwerkverbindung zum Testwebservice unzuverlässig ist. Bei leichtgewichtigen Webservices (also z.B. den test doubles in Sinatra/Flask), können aber auch diese Netzwerkprobleme umgangen werden, wenn die Testservices auf den Rechnern der Entwickler lokal ausführbar sind.

Damit ein solcher Testwebservice auch nach Weiterentwicklungen und Datenänderungen repräsentativ für den Produtktivwebservice ist, muss sicher gestellt sein, dass sich beide Webservices auch gleich verhalten. Auch hierfür kann ein automatisierter Test verwendet werden. Martin Fowler nennt diese Art des Tests, die prüft ob das Test Double noch dem Webservice entspricht, für den er einsteht einen „Integration Contract Test„. Auf diese Weise fällt es leicht Diskrepanzen zwischen beiden Implementierungen des Webservices zu erkennen und sie von wirklichen Regressionen zu unterscheiden. Schlägt der Integration Contract Test fehl, ist klar dass, und wie der Testwebservice angepasst werden muss.

Ein moderner Ansatz: Record and Replay

Ein modernerer Ansatz verspricht die Aufwände zur Erstellung und Aktualisierung des Testwebservices auf ein Minimum zu reduzieren und dennoch die Vorteile eines lokal auf dem Entwicklerrechner ausführbaren Testwebservices zu bieten. Bei diesem „Record and Replay“ genannten Ansatz wird der Netzwerkverkehr eines Testfalls bei der ersten Ausführung abgegriffen und die Antworten des Webservices werden aufgezeichnet. Der Aufwand einen derartigen Test zu erzeugen ist dabei kaum größer als der eines Integrationstests gegen ein Produktivsystem. Der Folgende Integrationstest verwendet eine „ProductRepository“ implementierende Klasse, die beispielsweise über einen REST Webservice über HTTP mit einem Remote Rechner kommuniziert.

@Category(IntegrationTests.class)
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration({"file:src/main/webapp/WEB-INF/spring-context.xml"})
public class ProductRepositoryTest {
    @Autowired
    public ProductRepository productRepository;

    @Test
    public void getProductByMaterialNumber() throws Exception {
        Product product = productRepository.getByMaterialNumber("1234567");
        assertThat(product, is(notNullValue())); 
        // ...
    }
    // ...
}

Die Aufzeichnung des Webservices mit einem Tool wie dem frei verfügbarem Betamax ist denkbar einfach – Betamax verwendet eine JUnit 4 Rule und Metadaten aus Annotationen zur Steuerung der Aufzeichnung und Wiedergabe des HTTP Traffic:

@Category(IntegrationTests.class)
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration({"file:src/main/webapp/WEB-INF/spring-context.xml"})
public class ProductRepositoryTest {
    @Autowired
    public ProductRepository productRepository;

    @Rule 
    public Recorder recorder = new Recorder();

    @Test
    @Betamax(tape = "getProductByMaterialNumber")
    public void getProductByMaterialNumber() throws Exception {
        Product product = productRepository.getByMaterialNumber("1234567");
        assertThat(product, is(notNullValue())); 
        // ...
    }
    // ...
}

Es setzt sich hierzu als Proxy zwischen den Client und den Server um Request und Response mitzuschneiden. Die Aufzeichnung findet standardmäßig in das Verzeichnis

/src/test/resources/betamax/tapes

statt. Das Tool erstellt dabei für jedes sog. Tape eine einfach lesbare YAML mit dem als „tape“ Parameter angegebenen Namen. In dieser Datei wird jeder Request, der an eine bestimmte URI mit einer bestimmten HTTP Methode gesendet wird mitsamt seiner Header- und Body-Daten abgelegt. Allerdings können so auch Daten wie Passwörter unverschlüsselt auf der Festplatte landen, weshalb beim Senden von sensiblen Daten besondere Vorsicht durch den Entwickler gefragt ist.

Ist einmal ein Tape für einen Testfall erzeugt, verhindert Betamax eine erneute Anfrage an den „richtigen“ Webservice, und gibt direkt die im Tape gespeicherten Antworten zurück. Eine erneute Aufnahme ist denkbar einfach, durch das Löschen eines bestehenden „Tapes“ zu erreichen. Um eine Divergenz zwischen Der Aufzeichnung und dem Webservice feststellen zu können, bietet sich auch hier ein Integration Contract Test an, der regelmäßig die Antworten beider Webservices vergleicht. Der gefühlt größte Vorteil ist jedoch der Gewinn an Geschwindigkeit: Eine Test Suite die in voller Integration mit dem Server im Schnitt rund 44 Sekunden lief läuft mit Betamax in etwa 8 Sekunden.

Fazit

In unseren Tests funktioniert die Aufzeichnung mit Betamax für RESTFul Webservices hervorragend. In Punkten Eleganz und Einfachheit – vor allem wenn es darum geht einen bestehenden Integrationstest gegen ein Livesystem robuster zu machen – ist es die bisher beste uns bekannte Lösung.

Bei der Anbindung von SOAP Webservices haben wir aber leider festgestellt, dass zumindest Betamax bisher nicht funktioniert. Der Body des Request in dem der SOAP Befehl im XML Format übermittelt wird kann nämlich bisher nicht als Kriterium für ein Matching von Aufzeichnung auf Request verwendet werden. Ein Feature, welches bereits in der aktuellen Entwicklungsversion vorhanden, aber noch nicht ganz ausgereift ist.

Hinterlasse eine Antwort

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

*

*

Du kannst folgende HTML-Tags benutzen: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <s> <strike> <strong>