Testen Teil 2 – Software testen

Letztes Jahr (fast) genau zur selben Zeit habe ich einen Blog übers allgemeine Testen von Apps und Programmen geschrieben. Dieses Jahr werde ich ein bisschen näher auf bestimmte Testmethoden eingehen. Wer also noch nichts von Unit-Tests oder Integrations-Tests weiß oder gehört hat, ist hier genau richtig!

Allgemein kann man das reine Softwaretesten in 3 Bereiche eingliedern, welche sich wiederum in einer Pyramide darstellen lassen:

Auf die einzelnen Tests werde ich später noch eingehen, aber den schematischen Aufbau erkläre ich schon hier. Die breite Basis eines jeden Software-Programms sind Unit-Tests. Ohne die sollte nichts laufen. Darauf aufbauend kommen dann Integration-Tests und abschließend UI-Tests. Doch was macht welcher Test genau? Das werde ich in den kommenden Absätzen ein bisschen genauer unter die Lupe nehmen. Es sei noch angemerkt, dass ich meine Tests in diesem Fall auf Android durchführe, und ich werde auch nur dieses OS behandeln, da es sich eher um einen allgemeinen Überblick als um ein Tutorial handelt.

Fangen wir mit den Unit-Tests an. Unit-Tests testen die Grundlagen eines jeden Programms, also Klassen oder z.B. Methoden. Die Tests müssen nicht sehr umfangreich sein, da (abhängig vom Umfang des zu testenden Objekts) meist nur das Ergebnis des entsprechenden Code-Abschnitts getestet wird. Außerdem laufen die Tests sehr schnell, da sie nur den Code testen und sind sehr günstig, da man kein zusätzliches Gerät oder einen Emulator benötigt. Beispiele für Unittest-Tools sind z.B. JUnit. Ein einfaches Beispiel für einen Unit-Test sieht in etwa so aus:

Die zu prüfende Methode addiert einfach die Integer-Variablen „one“ und „two“ und gibt das Ergebnis als „result“ zurück:


public static int testMethod(int one, int two){
int result = one + two;
return result;
}

Die prüfende Methode ruft die Methode „testMethod“ mit den Variablen 1 und 3 auf und erwartet als Ergebnis 4:


@Test
public void checktestMethod() {
assertEquals(4,MainActivity.testMethod(1, 3));
}

Und das Ergebnis der Ausgabe:

JUnit lässt sich sehr leicht in Android integrieren. Man muss lediglich 2 „dependencies“ spezifizieren und schon kann‘s los gehen. Man erstellt eine einfache Java-Klasse im (test)-Ordner und wenn der oder die Tests fertig geschrieben sind, kann man sie einfach per „run“-Befehl ausführen. Falls die Unit-Tests alle positiv sind, kann man sich dem mittleren Teil der Pyramide zuwenden: den Integration-Tests.

Integration-Tests dienen dazu, eben jene grundlegenden Methoden im Zusammenspiel zu testen. Es wird hier nicht mehr die einzelne Funktion überprüft, sondern es können ganze Abfolgen von Befehlen getestet werden. Es können Teile des Systems in den Test miteinbezogen werden, die bei Unit-Tests noch nicht gebraucht oder vorhanden waren. Allerdings sind diese Tests auch aufwändiger, da z.B. da eventuell schon Objekte vorhanden sein müssen, welche zum Ausführen benötigt werden. Diese nennt man dann Mock-Objekte. Die Mock-Objekte dienen zum Ausführen des Test-Codes, da Live-Daten zur Test-Zeit noch nicht zur Verfügung stehen oder nicht verändert werden sollen. Hier kann gezielt darauf getestet werden, ob Funktionsaufrufe oder Bestimmte Befehle korrekt ausgeführt werden.

In meinem Fall habe ich eine App geschrieben, die unterschiedliche Einheiten umrechnen kann. Im Integration-Test überprüfe ich dann, ob mit den eingegebenen Daten dann auch das richtige Ergebnis herauskommt. Das sieht dann in etwa so aus:


@RunWith(RobolectricTestRunner.class)
public class RoboelectricTestExample {
[…]
@Before
public void setUp() {
Activity activity = Robolectric
.buildActivity(ConvertActivity.class).create().get();
convertButton = (Button) activity.findViewById(R.id.convert);
inputText = (EditText) activity.findViewById(R.id.input);
statusText = (TextView) activity.findViewById(R.id.result);
secondActivity = (Button) activity.findViewById(R.id.sec_activity);
unitButton = (Button) activity.findViewById(R.id.unit);
fromUnit = (TextView) activity.findViewById(R.id.fromUnit);
toUnit = (TextView) activity.findViewById(R.id.toUnit);
}
@Test
public void testConvertButton (){
inputText.setText("5");
fromUnit.setText("m");
toUnit.setText("cm");
convertButton.performClick();
assertEquals("Ergebnis: 500.0 cm",statusText.getText());
}
}

In diesem Fall wird der Test mit „Roboelectric“ ausgeführt. Die Notierung @Before beschreibt alle Ausführungen, die vor dem eigentlichen Test gestartet werden sollen. In diesem Fall wird die komplette Activity gestartet und danach werden einige Textfelder und Buttons initialisiert. Dann folgt der eigentliche Test, angeführt mit @Test. Dadurch weiß das Programm, dass dies der zu testende Fall ist. Ich habe dann als Wert 5 und als Einheit „m“ definiert. Die Ausgabe soll dann in „cm“ erfolgen. Wenn der Test erfolgreich ausgeführt wurde, und das Ergebnis übereinstimmt, erscheint wie schon bei den Unit-Tests die Rückmeldung „test passed“ mit „exit code 0“.
Zum Beispiel kann mit dieser Methode auch überprüft werden, ob nach einem Klick auf einen bestimmten Button die richtige View angezeigt wird. Nachdem mit den Integrations-Tests dann schon ein großer Funktionsumfang abgedeckt ist, folgen schlussendlich dann noch UI-Tests, um die Testpyramide in der praktischen Anwendung erfolgreich abzuschließen.

UI-Tests sind am aufwändigsten, da hier zumindest ein Emulator vorhanden sein muss, um den Test auszuführen. Außerdem erfordern UI-Tests auch immer mehr Code als Integration-Tests und diese wiederum mehr als Unit-Tests. Daher benötigt man für UI-Tests auch sehr viel Zeit. Mit UI-Tests kann die Oberfläche, also das „user interface“ einer App getestet werden. Das bedeutet, man überprüft ob z.B. auf eine bestimmte Eingabe und den folgenden Klick auf einen Button die richtigen Schritte passieren. Es gibt mehrere Möglichkeiten, diese Tests durchzuführen. Einerseits kann man die Tests auf einem Emulator oder auf einem realen Device ausführen. Der Vorteil des Emulators ist die Anpassung an verschiedene Betriebssysteme, was auf einem Smartphone z.B. schon schwieriger ist. Dadurch ist ein größerer Umfang der UI-Tests gegeben. Außerdem besteht die Möglichkeit, z.B. mit Espresso auf Android, eine bestimmte Abfolge von Eingaben aufzuzeichnen und immer wieder abzuspielen, wodurch auch schon eine gewisse Automation vorhanden ist.

Auch mit meiner Test-App habe ich einen kleinen UI-Test geprobt. Dazu muss man wissen, dass beim Starten der App zuerst nach der Klasse der umzuwandelnden Größe gefragt wird, und danach die jeweiligen Einheiten abgefragt werden. In untenstehendem Gif kann man dann den Ablauf des Tests verfolgen. Es wird zuerst dreimal das Ändern der Einheiten getestet und anschließend wird die Konvertierung einer Größe getestet.

Man sieht also, man kann seine App oder seinen Code mit vielen Mitteln testen und somit auch „funktionierender“ machen. Die von mir in diesem Blog genannten Wege sind natürlich nur Beispiele, es gibt noch unzählige andere Frameworks, die ähnlich arbeiten bzw. die auf anderen Betriebssystemen laufen. Es ist auch die Frage, ab welcher Teststufe ein externer Tester das Testen übernehmen sollte, da man sich meistens erst in den Code einlesen muss. Wenn allerdings die Entwickler selbst die Tests schreiben, kann es passieren, dass Fehler aus dem Programmcode auch in die Tests übernommen werden.

UI-Tests fallen unter die Kategorie des „grey-box testing“, da man den genauen Code nicht kennt, aber weiß was passieren soll. Daher bieten sich hierfür oft externe Tester an. Unit-Tests und Integration-Tests andererseits gehören zu den „white-box tests“, d.h. der Code ist genau bekannt und kann auch dementsprechend genau getestet werden.

Kurz gesagt, je nach Aufwand den man für das Testen betreibt, bekommt man eine mehr oder weniger fehlerfreie App. Aber wie auch schon im letzten „Test-Blog“ bleibt auch diesmal die Anmerkung übrig: ganz ohne Fehler, sollten sie noch so minimal sein, wird es nie gehen.