Q Q Q Q Q Q Q Q Q Q Q Q Q Q Q
blog-icon Blog

5 Min. Lesezeit

Exploratives Open-Source-Software Testing mit Spock

Exploratives Softwaretesting ist eine Technik, die jeder agile Entwickler kennen sollte. Es geht darum, die eigene Anwendung ohne einen vorgegebenen Ablaufplan auf Herz und Nieren zu testen. Obwohl dies auf den ersten Blick wie ein zufälliges Freistil-Testen wirkt, erweist es sich in den Händen eines erfahrenen Entwicklers als mächtiges Werkzeug, um Bugs und unerwünschtes Verhalten aufzuspüren.

In diesem Artikel geht es im Speziellen um das explorative Testen von Open-Source-Softwarekomponenten, Bibliotheken oder ganzen Frameworks. Warum sollte man das tun? Bedenken sie Folgendes: Der Anteil an selbst geschriebenem Code in einer modernen Anwendung liegt meist nur zwischen 3 und 10 Prozent der gesamten Bytecode-Instruktionen. Der Rest besteht in der Regel aus Open-Source-Bibliotheken und -Frameworks von Drittanbietern, die von der Anwendung genutzt werden – wie etwa Apache Commons oder das Spring-Framework.

Doch wie entscheiden sie als Softwareentwickler oder -architekt, welche Open-Source-Komponente sie für eine bestimmte Anforderung einsetzen? Woher wissen sie, ob das schicke Framework, über das sie in einer Fachzeitschrift gelesen haben, wirklich zu Ihren Anforderungen passt? Und wie evaluieren sie, wie eine Bibliothek am besten in Ihre Anwendung integriert werden kann?

 

Hier kommt das explorative Testen von Open-Source-Software ins Spiel. Zusammenfassend verfolgt das explorative Testen von Open-Source-Komponenten folgende Ziele:

  • Ein Verständnis dafür entwickeln, wie die Bibliothek oder das Framework funktioniert, wie die Schnittstelle aussieht und welche Funktionalitäten implementiert sind: Das Ziel ist es, die Funktionsweise der Open-Source-Komponente tiefgehend zu erforschen und bisher unentdeckte Features zu finden.
  • Die Open-Source-Software dazu bringen, ihre Fähigkeiten zu demonstrieren: Dies liefert den Nachweis, dass die Software genau die Funktion erfüllt, für die sie entwickelt wurde, und dass sie Ihre Anforderungen abdeckt.
  • Bugs finden oder die Performance analysieren: Die Grenzen der Open-Source-Komponente austesten und gezielt potenzielle Schwachstellen und Schwachpunkte ansteuern.
  • Als Absicherung bei Updates der Bibliothek auf eine neue Version dienen: Dies ermöglicht eine einfachere Wartung Ihrer Anwendung und ihrer Abhängigkeiten. Die explorativen Tests decken Regressionen auf, die eine neue Version eventuell mit sich bringt.

Szenariobasiertes Software-Exploring

Wenn sie eine neue Open-Source-Komponente in Ihrem Projekt einsetzen möchten, haben sie meist schon eine grobe Vorstellung davon, was sie von deren Nutzung erwarten. Die Idee des szenariobasierten Software-Explorings lautet daher: Beschreiben sie Ihre Visionen und Erwartungen in Form von Nutzungsszenarien. Diese Szenarien sind Ihre Landkarte für das unerschlossene Terrain der Bibliotheksfunktionen und führen sie durch den Testprozess.

Im Allgemeinen erfüllt ein nützliches Szenario einen oder mehrere der folgenden Punkte:

  • Erzählt eine User Story und beschreibt die Anforderungen
  • Demonstriert, wie eine bestimmte Funktionalität funktioniert
  • Demonstriert ein Integrationsszenario
  • Beschreibt Warnhinweise und Dinge, die schiefgehen können

Exploratives Testen mit Spock

Natürlich können sie explorative Tests auch mit traditionellen xUnit-Frameworks wie JUnit oder TestNG schreiben. Warum also stattdessen Spock verwenden? Das Spock Framework eignet sich meiner Meinung nach viel besser für explorative Tests, da es das szenariobasierte Testen von Natur aus unterstützt:

  • Spezifikation als Dokumentation
  • Reduzierte, elegante und ausdrucksstarke Syntax der Spezifikationssprache
  • Integrierte Unterstützung für Stubbing und Mocking
  • Gute Integration in IDEs und Build-Tools

Die folgenden Abschnitte verdeutlichen diese Punkte im Detail, indem Spock verwendet wird, um explorative Tests für die Serialisierungsbibliothek Kryo zu schreiben.

Spezifikation als Dokumentation

Das Gute an Spock ist, dass es die Verwendung von natürlicher Sprache in den Spezifikationsklassen erlaubt. Schauen sie sich das folgende Beispiel an. Derzeit testet es noch nichts, es ist reine Dokumentation. Selbst wenn sie Spock überhaupt nicht kennen, werden sie allein durch das Lesen der Spezifikation verstehen, was der Test tun soll. Großartig!

@Title('Exploratory test for the shallow/deep copy functionality of Kryo')
@Narrative('''
   Making object copies in Java is an often required functionality.
   Writing explicit copy constructors is fast but also laborious.
   Instead the Java Serialization is often misused to make copies.
   Kryo performs fast automatic deep and shallow copying by copying
   from object to object.
''')
class KryoShallowAndDeepCopySpec extends Specification {

    @Subject
    def kryo = new Kryo()

    def "Making a shallow copy of a semi complex POJO"() {
        given: "a single semi complex POJO instance"

        when: "we make a shallow copy using Kryo"

        then: "the object is a copy, all nested instances are references"
    }

    def "Making a deep copy of a semi complex POJO construct"() {
        given: "a semi complex POJO instance construct"

        when: "we make a deep copy using Kryo"

        then: "the object and all nested instances are copies"
    }
}

Reduzierte, elegante und hochgradig ausdrucksstarke Sprachsyntax

Die reduzierte Syntax von Spock resultiert hauptsächlich aus den Wurzeln in Groovy. Explorative Tests profitieren stark davon, da es Ihnen hilft, sich auf das Wesentliche zu konzentrieren: die Open-Source-Komponente, die sie erkunden möchten. Darüber hinaus bringt Spock eine eigene DSL mit, die Ihre Spezifikation noch ausdrucksstärker macht. Jede Feature-Methode in einer Spezifikation ist in sogenannte Blöcke unterteilt.

Blocks2Phases

Siehe Spock Primer für weitere Details.

Diese Blöcke ermöglichen es Ihnen nicht nur, die verschiedenen Phasen Ihres Tests auszudrücken. Durch die Verwendung dieser Blöcke können sie demonstrieren, wie eine Open-Source-Komponente funktioniert und wie sie in Ihre Codebasis integriert werden kann. Der setup:- oder given:-Block bereitet die benötigten Eingabedaten mithilfe von Klassen aus Ihrer Anwendungsdomäne vor. Die when:- und then:-Blöcke zeigen die Funktionsweise einer bestimmten Komponente auf, indem sie mit dem Testobjekt interagieren und das gewünschte Verhalten überprüfen. Dank der Groovy-Natur von Spock müssen Ihre Zusicherungen (Assertions) lediglich zu true oder false auswerten. Und für das letzte Quäntchen Ausdrucksstärke können sie Ihre gewohnten Hamcrest-Matcher verwenden.

def "Deserialize a GCustomer object from a temporary data file"() {
    given: "a Kryo input for a temporary data file"
    def input = new Input(new FileInputStream(temporaryFile))

    when: "we deserialize the object"
    def customer = kryo.readObject input, GCustomer

    then: "the customer POJO is initialized correctly"
    customer
    expect customer, notNullValue()

    customer.name == 'Mr. Spock'
    expect customer.name, equalTo('Mr. Spock')
}

Unterstützung für Stubbing und Mocking

Das Spock Framework bringt auch eine eigene Unterstützung für Mocks und Stubs mit, um interaktionsbasiertes Testen zu ermöglichen. Diese Testtechnik konzentriert sich auf das Verhalten des zu testenden Objekts und hilft uns zu erkunden, wie eine Komponente mit ihren Kollaborateuren interagiert (indem sie Methoden auf ihnen aufruft) und wie dies das Verhalten der Komponente beeinflusst. Da Mocks oder Stubs für jedes Interface und fast jede Klasse definiert werden können, müssen sie Fake-Objekte nicht mehr manuell implementieren. Stubs bieten lediglich die Möglichkeit, vordefinierte Antworten für definierte Interaktionen zurückzugeben, während Mocks zusätzlich die Überprüfung dieser Interaktionen ermöglichen.

def "Explore writing an object using a Serializer mock"() {
    given: "a Kryo Serializer mock and dummy output"
    def serializer = Mock(Serializer)
    def output = new Output(new byte[1])

    when: "serializing the string Mr. Spock"
    kryo.writeObject(output, 'Mr. Spock', serializer)

    then: "we expect 1 interaction with the serializer"
    1 * serializer.write(kryo, output, 'Mr. Spock')
}

def "Explore reading an object using a Serializer stub"() {
    given: "a dummy input and a Kryo Serializer stub"
    def input = new Input([1] as byte[])
    def serializr = Stub(Serializer)

    and: "a stubbed Customer response"
    serializr.read(kryo, _, GCustomer) >> new GCustomer(name: 'Mr. Spock')

    when: "deserializing the input"
    def customer = kryo.readObject(input, GCustomer, serializr)

    then: "we get Mr. Spock again"
    customer.name == 'Mr. Spock'
}

Gute Integration in IDEs und Build-Tools

Eine gute Integration in Entwicklungsumgebungen (IDEs) und Build-Tools ist ein wichtiges Merkmal, da unsere explorativen Tests ein fester Bestandteil der Codebasis unserer Anwendung sein sollen. Glücklicherweise ist die Spock-Unterstützung bereits sehr gut – hauptsächlich, weil Spock-Tests im Grunde in JUnit-Tests übersetzt werden. Um ordentliches Syntax-Highlighting und Code-Vervollständigung zu erhalten, können sie dedizierte Plugins für Ihre bevorzugte IDE installieren. Für IntelliJ gibt es das Spock Framework Enhancements Plugin. Für Eclipse steht das Spock Plugin zur Verfügung.

Die Integration in Build-Tools ist ebenfalls recht unkompliziert. Wenn sie Gradle für Ihren Build verwenden, müssen sie lediglich die korrekten Abhängigkeiten angeben und das Groovy-Plugin anwenden, wie im folgenden Ausschnitt gezeigt.

apply plugin: 'groovy'

dependencies {
    // mandatory dependencies for using Spock
    compile 'org.codehaus.groovy:groovy-all:2.4.1'
    testCompile 'org.spockframework:spock-core:1.0-groovy-2.4'
    testCompile 'junit:junit:4.12'
    testCompile 'org.mockito:mockito-all:1.10.19'

    // optional dependencies for using Spock
    // only necessary if Hamcrest matchers are used
    testCompile 'org.hamcrest:hamcrest-core:1.3' 
    // allows mocking of classes (in addition to interfaces)
    testRuntime 'cglib:cglib-nodep:3.1'
    // allows mocking of classes without default constructor
    testRuntime 'org.objenesis:objenesis:2.1'
}

Für Maven müssen sie etwas mehr tun, als nur die erforderlichen Spock-Abhängigkeiten in Ihrer pom.xml anzugeben. Da Spock-Tests in Groovy geschrieben sind, müssen sie auch das GMavenPlus Plugin in Ihren Build einbinden, damit Ihre Spock-Tests kompiliert werden. Möglicherweise müssen sie auch die Konfiguration des Surefire-Plugins anpassen, um **/*Spec.java als gültige Tests einzuschließen.

<build>
    <plugins>
        <!-- Mandatory plugins for using Spock -->
        <plugin>
            <groupId>org.codehaus.gmavenplus</groupId>
            <artifactId>gmavenplus-plugin</artifactId>
            <version>1.4</version>
            <executions>
                <execution>
                    <goals>
                        <goal>compile</goal>
                        <goal>testCompile</goal>
                    </goals>
                </execution>
            </executions>
        </plugin>

        <!-- Optional plugins for using Spock -->
        <plugin>
            <artifactId>maven-surefire-plugin</artifactId>
            <version>2.6</version>
            <configuration>
                <useFile>false</useFile>
                <includes>
                    <include>**/*Spec.java</include>
                    <include>**/*Test.java</include>
                </includes>
            </configuration>
        </plugin>
    </plugins>
</build>

<dependencies>
    <!-- Mandatory dependencies for using Spock -->
    <dependency>
        <groupId>org.spockframework</groupId>
        <artifactId>spock-core</artifactId>
        <version>1.0-groovy-2.4</version>
        <scope>test</scope>
    </dependency>

    <!-- Optional dependencies for using Spock -->
    <dependency>
        <groupId>org.codehaus.groovy</groupId>
        <artifactId>groovy-all</artifactId>
        <version>2.4.1</version>
    </dependency>
    <dependency>
        <groupId>org.codehaus.groovy</groupId>
        <artifactId>groovy-backports-compat23</artifactId>
        <version>2.3.7</version>
    </dependency>
    <dependency>
        <groupId>junit</groupId>
        <artifactId>junit</artifactId>
        <version>4.12</version>
        <scope>test</scope>
    </dependency>
    <dependency>
        <groupId>org.mockito</groupId>
        <artifactId>mockito-all</artifactId>
        <version>1.10.19</version>
        <scope>test</scope>
    </dependency>
    <dependency>
        <groupId>org.hamcrest</groupId>
        <artifactId>hamcrest-core</artifactId>
        <version>1.3</version>
        <scope>test</scope>
    </dependency>
    <dependency>
        <groupId>cglib</groupId>
        <artifactId>cglib-nodep</artifactId>
        <version>3.1</version>
        <scope>test</scope>
    </dependency>
    <dependency>
        <groupId>org.objenesis</groupId>
        <artifactId>objenesis</artifactId>
        <version>2.1</version>
        <scope>test</scope>
    </dependency>
</dependencies>

Referenzen

Ein Beitrag von

Mario-Leander Reimer

ist Managing Director / CTO bei QAware. Er ist Spezialist für den Entwurf, die Umsetzung und den Betrieb von verteilten System- und Softwarearchitekturen auf Basis von Open-Source Bausteinen. [...]