Skip to content

SS2019: Technologierecherche – Testing

Mario Faske edited this page Apr 30, 2019 · 7 revisions

Softwaretests

A process of analyzing a software item to detect the differences between existing and required conditions (i.e., defects) and to evaluate the features of the software item.

Jeder, der schon einmal ein Stück Software geschrieben hat, hat dieses auch gestestet. Zum Beispiel das Ausgeben von Daten mittels einer print-Funktion, um den Aufruf einer Funktion oder die richtige Übergabe/Weitergabe von Daten, zu testen. Diese Art von Tests werden "manuelle Tests" genannt. Bei größeren Software-Projekten kommt diese Methode aber meist recht schnell an ihre Grenzen, da Abhängigkeiten und/oder andere Interaktionen zwischen Teilen der Software das manuelle Testen sehr zeitaufwendig machen.

Eine andere Methode sind die "automatisierten Tests". Diese Testen die Software mittels vordefinierten Tests an ausgewählten Stellen im Entwicklungsprozess. Im Gegensatz zu den manuellen Tests können automatisierte Tests auch vor der eigentlichen Entwicklung geschrieben werden. Dieses Vorgehen nennt man Test Driven Development (TDD).

Test Driven Development

Bei diesem Vorgehen erstellt der Entwickler jeweils Tests zu Funktionen bevor diese implementiert werden. Zunächst schlagen die Tests fehl und durch eine korrekte Implementierung der zu testenden Funktion muss der Test positiv werden. Sobald die Tests für die Funktion positiv sind, kann der Code refaktorisiert werden und neue Tests für weitere Funktionen geschrieben werden.

Test:

assertEquals(5, sample().num_ways_bottom_up(4))

Zu testender Code:

// Amazon: There's a staircase with N steps, and you can climb 1 or 2 steps at a time. 
// Given N, write a function that returns the number of unique ways you can 
// climb the staircase. The order of the steps matters.

fun num_ways_bottom_up(n: Int):Int{
   if(n==0) return 0
   if(n==1) return 1
   if(n==2) return 2
   var bottom_up = IntArray(n+1)
   bottom_up[0] = 0
   bottom_up[1] = 1
   bottom_up[2] = 2
   for (i in 3..n) {
       bottom_up[i] = bottom_up[i-1] + bottom_up[i-2]
   }
   return bottom_up[n]
}

Weitere Tests:

assertEquals(1, sample().num_ways_bottom_up(1))
assertEquals(0, sample().num_ways_bottom_up(0))
assertEquals(10946, sample().num_ways_bottom_up(20))

So sehen diese Tests in der IntelliJ IDE mit kotlin aus:

Bildschirmfoto 2019-04-30 um 11 49 21

Der eigentlich Test für eine Funktion ist, je nach Programmiersprache (hier Kotlin), meist sehr kurz. Dieser Test überprüft nur eine Eingabe auf eine bestimmte Rückgabe. Es werden aber oft auch unterschiedliche Eingaben überprüft. Wichtig ist, dass Tests nicht nur Rückgaben nach bestimmten Eingaben überprüfen, sondern allgemein die Rückgabe einer Funktion beziehungsweise Unit oder das Verhalten verschiedener Komponenten miteinander beziehungsweise zueinander (dazu später mehr).

Assert

Getestet wird mit Behauptungen(Asserts), welche in den meisten Testing-Libraries ähnliche Syntaxen aufweisen. Das heißt, es wird zum Beispiel behauptet, dass der Rückgabewert einer Funktion bei einem bestimmten Eingabeparameter einem Erwartungswert entspricht. Die folgenden Beispiele werden in JUnit verwendet(Java und Kotlin)

assert(boolean value) Die einfachste Behauptung für Tests. Wenn eine Funktion nur einen boolean Wert zurückliefert, können mit dieser Behauptung, zum Beispiel innerhalb eines if-Statements, weitere Behauptungen mit dieser Vorbedingung aufgestellt werden.

if(assert(isCardDisabled())){
    assertTrue(retainCard(card))
    assertEquals("Card has been retained", briefCustomer(card))
}

assertTrue(boolean condition) & assertFalse(boolen condition) Erweiterung von assert(), wobei hier, ohne ein if-Statement, geprüft werden kann, ob die Rückgabe einer Funktion Wahr oder Falsch ist.

assertEquals(int expected, int actual) Mit dieser Behauptung kann der Rückgabewert einer Funktion auf alle von der Sprache unterstützen Datentypen geprüft werden.

Behaviour Driven Development

Diese Methode erweitert das TDD insofern, dass nicht nur getestet wird, ob eine Funktion korrekt arbeitet, sondern auch welche Tests überhaupt für eine Funktion des Systems notwendig sind. Wie der Name schon sagt, steht bei dieser Methode das Verhalten des Systems in bestimmten Situation im Vordergrund und sollte getestet werden. BDD setzt dabei auf eine ubiquitous (allgegenwärtige) Sprache, so dass eine Kommunikation zwischen Entwicklern oder mit Verantwortlichen, welche nicht das technische Know-How haben, vereinfacht wird. Das Verhalten des Systems wird in Szenarios ausgedrückt, welche Teil einer Story sind, die wiederum eine Funktion des Systems beschreibt.

Story: Account Holder withdraws cash
As an Account Holder
I want to withdraw cash from an ATM
So that I can get money when the bank is closed
Scenario 1: Account has sufficient funds
Given the account balance is \$100
 And the card is valid
 And the machine contains enough money
When the Account Holder requests \$20
Then the ATM should dispense \$20
 And the account balance should be \$80
 And the card should be returned

Quelle: https://airbrake.io/blog/software-design/behavior-driven-development

Das System hat die Funktion, dass der Kontoinhaber Geld abheben kann (Story). Mittels Szenarios wird das Verhalten des Systems bei bestimmten Ereignissen oder Zuständen beschrieben. Oft verhält sich das System unterschiedlich, je nach Ereignis oder Zustand. Deshalb können n-viele Szenarios zu einer Story formuliert werden.

Scenario 2: Account has insufficient funds
Given the account balance is \$10
 And the card is valid
 And the machine contains enough money
When the Account Holder requests \$20
Then the ATM should not dispense any money
 And the ATM should say there are insufficient funds
 And the account balance should be \$20
 And the card should be returned
 
Scenario 3: Card has been disabled
Given the card is disabled
When the Account Holder requests \$20
Then the ATM should retain the card
And the ATM should say the card has been retained

Quelle: https://airbrake.io/blog/software-design/behavior-driven-development

Im folgenden Beispiel wird der Vorteil von BDD noch einmal verdeutlicht.

given("n = 4") {
    it("should be that sample().num_ways_bottom_up(4) = 5") {
        assertEquals(5, sample().num_ways_bottom_up(4))
    }

oder

//Szenario 3
given("the card is disabled") {
    it("should be that retain(card) = true") {
        assertTrue(retain(card))
    }
    it("schould be that briefCustomer(card) = 'Card has been retained'"){
        assertEquals("Card has been retained", briefCustomer(card))
    }

Testing Levels

Beim TDD/BDD wird auf verschiedenen Levels/Ebenen getestet. Auf jeder Ebene wird ein anderer Teil der Software getestet beziehungsweise ein anderer Status des Systems. Dabei haben sich 4 Ebenen etabliert:

Testing Levels

Quelle: https://www.guru99.com/levels-of-testing.html

Unit Testing

Jede Funktion kann als Unit bezeichnet werden und fällt somit unter das Unit Testing. Gemeint ist der kleinste zu testende Teil eines Systems, sprich eine Funktion.

fun sum(x:Int, y:Int):Int{
    return x+y
}
@Test
fun sumTest(){
    assertEquals(5, sum(2,3))
}

Integration Testing

Auf dieser Ebene wird die Kommunikation beziehungsweise das Verhalten von Komponenten eines Systems untereinander getestet. Aber auch das Testen der Kommunikation mit anderen Services oder Software erfolgt auf dieser Ebene.

assertEquals(200, fetch(google.com).statusCode)

assertEquals("Bob", getUserName(localhost:3000/user/1).name)

System Testing

Sobald das System deployed wurde und noch bevor es zum Beispiel dem Kunden übergeben wird, findet auf dieser Ebene ein kompletter System-Test statt. Dabei wird unter anderem die Interaktion aller Komponenten des fertigen Systems getestet. Zudem finden hier auch die Tests für die Performance, Zuverlässigkeit und Sicherheit des Systems statt. Für Web-Applikationen ist zum Beispiel der Load-Test besonders wichtig, da hier festgestellt wird, ob und welche Teile des Systems zu langsam oder, im Falle eines Timeouts, gar nicht geladen werden.

Acceptance Testing

Diese Ebene stellt vor der Auslieferung des Produktes sicher, ob alle Anforderungen des Auftraggebers an das System erfüllt werden. Es gibt zum einen das interne Testen durch Testing-Teams und das externe Testen durch mögliche Nutzer des Systems.

Best Practices

Es gibt eine Reihe von bewerten Verfahren (best practices) in der Welt der Softwaretests. Aus diesen Verfahren ergeben sich auch ein paar Regeln die es gilt zu beachten. Die folgenden Verfahren setzen sich aus verschiedenen Quellen und eigenen Erfahrungen zusammen.

Test early and test often Ob nun manuelle oder automatische Tests, frühe Tests und auch eine gewisse Quantität an Tests bietet die Möglichkeit, schon am Anfang eines Entwicklungsprozesses Mängel des Systems zu erkennen. Dieser Punkt erübrigt sich, sollte TDD oder BDD als Methode gewählt werden.

Dont forget edge cases Tests sollten nicht nur die wahrscheinlichsten Fälle prüfen, sondern auch mögliche fehlerhafte Eingaben der Anfragen. Wenn zum Beispiel eine leeres Array übergeben wird, oder der Nutzer nicht nur Integer Werte eingegeben hat.

Write understandable tests Besonders bei automatisierten Tests sollten die Tests, ähnlich wie der Code, verständlich für andere Entwickler oder Beiteiligte am Prozess geschrieben werden. Hier hilft der Ansatz des BDD, da so natürlichsprachliche Tests geschrieben werden.

Concentrate on critical functions Es ist zeitlich nicht ratsam, für jede Funktion n-viele Tests zu schreiben. Triviale Funktionen wie eine Addition oder ähnlichem bedürfen wenn überhaupt einem einzelnen Test. Kritische Funktionen sollten durch die meisten und ausführlichsten Tests überprüft werden, da diese meist die Integrität und Anforderungen des Systems betreffen.

Microservices and integration tests

Aufgrund ihrer unabhängigen Auslieferung gestalltet sich Integration Tests für Microservices aufwändiger. Eine Interaktion in form von Kommunikation mit einem anderen Microservice lässt sich nicht testen wenn der Microservice noch nicht deployed wurde. Eberhard Wolff beschreibt dafür 2 Ansätze. Quelle: https://microservices-buch.de/

Reference Environment

Eine Möglichkeit, die Wolff beschreibt ist das bereitstellen aller Microservices in ihrer aktuellen Version in einer Referenzumgebung. Das Problem was dabei aber entsteht beziehungsweise entstehen kann ist, dass mehrere Teams ihren oder ihre Microserives mit anderen Microservices testen wollen. Hierbei kann es passieren, dass Tests sich gegenseitig beeinflussen und dadurch fehler entstehen. Das heißt, die Teams müssten sich um die Aktualität und Verfügbarkeit ihrer Microservices kümmern, was zusätzliche Ressourcen kostet.

Stubs

Mit Stubs wird eine Microservice nur simuliert, indem limitierte Funktionen bereitgestellt werden. Ein Stub liefert zum Beispiel nur einen konstanten Wert der getestet werden kann. Wichtig hierbei ist, dass jedes Team einen Stub für seinen Microservice bereitstellt. Zudem sollten alle Stubs die gleiche Technologie verwenden, so dass Teams, welche mehrere Microservices entwickeln, einfacher mehrere Stubs bereitstellen können. Zum Beispiel würde ein Stub Daten eines Nutzers mit einer Bestimmten ID bereitstellen, ohne Datenbankabfrage oder ähnlichem.