Name

Robotium

Homepage

http://code.google.com/p/robotium/

Lizenz

Apache License 2.0

Untersuchte Version

v4.3 vom 8.9.2013

Letzter Untersuchungszeitpunkt

28.2.2014

Kurzbeschreibung

Robotium ist ein Open Source Test-Framework für Android mit dem Benutzereingaben auf der Oberfläche simuliert werden können. Zum Testen ist der Sourcecode nicht zwingend nötig. Es reicht, wenn man Zugriff auf die apk, das zur Installation nutzbare Archiv-File, der zu testenden App hat.
Das Framework beruht auf JUnit.

Fazit

Sobald man das Testprojekt korrekt erstellt hat, ist Robotium intuitiv bedienbar, da die Methoden sprechende Namen haben und zusätzlich im Javadoc dokumentiert sind. Es ist hilfreich, wenn man bereits Erfahrung mit JUnit-Tests hat, da auch hier übliche Methoden wie „assertTrue()“ usw. zum Einsatz kommen.
Views auf der Benutzeroberfläche können einfach über den Text, den sie anzeigen, gefunden werden. Alternativ kann auch das View-Objekt selbst übergeben werden oder der View wird gesucht, indem der Index angegeben wird. Mit Index ist die Position des Views gemeint, d.h. der wievielte er in der Activity ist. Dabei gibt es nicht nur Methoden allgemein für Views, sondern auch speziell für Buttons, EditTexts, usw. Allerdings fehlen einige dieser nützlichen Funktionen für bestimmte Unterklassen. So wird zum Beispiel für einen Klick auf einen ToggleButton nur die Funktion zur Verfügung gestellt, mit der dieser über den Text, mit dem er beschriftet ist, identifiziert und dann angeklickt wird. Wenn er über den Index gefunden werden soll, muss ein kleiner Umweg gegangen werden.
Außerdem können einzelne Screenshots oder auf Wunsch auch ganze Sequenzen erstellt werden. Sie werden auf der externen SD-Karte des Telefons gespeichert. Darum muss die zu testende App in diesem Fall die Permisson haben, in den externen Speicher zu schreiben.

Einsatzgebiete

Robotium kann Benutzereingaben auf der Oberfläche einer Android-App simulieren.
Es kann sowohl zum White- als auch zum Blackboxtesten genutzt werden. Für Blackboxtests wird die apk der zu testenden App benötigt.
Es ist nicht möglich einen Test zu schreiben, der sich über mehrere Apps hinwegzieht, da das Target-Package im Manifest des Testprojekts festgelegt wird.

Einsatzumgebungen

Um das Framework nutzen zu können, muss Eclipse und das Android ADT Plugin installiert sein. Robotium steht als .jar-Datei zur Verfügung und muss in das Android-Test-Projekt eingebunden werden.

Installation

Die Installation gestaltet sich recht einfach, da die jar-Datei auf übliche Art und Weise ins Projekt eingebunden wird. Außerdem steht ein JavaDoc zur Verfügung, das mit eingebunden werden kann.

Testprojekt erstellen

Um ein neues Testprojekt zu erstellen, wählt man in Eclipse unter File → New → Other → Android → Android Test Project. Nachdem ein Name eingegeben und die zu testende App ausgewählt wurde, ist das Projekt erstellt.
Nun muss die robotium-jar eingefügt werden. Dazu die Properties des Testprojekts öffnen und bei „Java Build Path“ „Libraries“ auswählen und die JAR hinzufügen. Außerdem muss im Reiter „Order and Export“ ein Haken bei der neuen JAR gesetzt werden. Danach sollte ein Clean durchgeführt werden.

Dokumentation

Neben dem Javadoc gibt es auf der offiziellen Homepage ein Wiki, in dem einige Anwendungsmöglichkeiten beschrieben werden. Dort gibt es beispielsweise eine FAQ und einen „Getting Started“-Guide.
Des weiteren gibt es eine Tutorialseite, auf der anhand einer Taschenrechnerapp die Vorgehensweise zur Erstellung eines ersten Testprojekts beschrieben wird. Die Erläuterungen stehen als PDFs zur Verfügung. Zusätzlich können die fertigen Projekte, die im Tutorial schrittweise erstellt werden, heruntergeladen werden.
Auf der gleichen Seite wird auch zu einem externen Videotutorial verlinkt, in dem beschrieben wird, wie man ein Testprojekt erstellt. Allerdings ist es schon etwas veraltet, da nicht darauf hingewiesen wird, dass die jar-Datei exportiert werden muss.
Die vielfachen Hilfen vereinfachen die Einarbeitung in das Framework. Das Beispiel auf der Entwicklerseite und das inoffizielle Videotutorial bieten zusammen alle Informationen, die benötigt werden, um eine erste eigene Testklasse zu schreiben. Die Einrichtung wird bei beiden sehr kleinschrittig und einfach erklärt.
Die Dokumentation ist vollständig in Englisch.

Wartung der Projektseite

Das Framework wird ständig weiterentwickelt. Auf der Homepage ist unter „Downloads“ immer die neuste Version verfügbar, aber auch der Versionsverlauf abrufbar.

Nutzergruppen und Support

Im Wiki des Projekts gibt es eine FAQ. Außerdem können im Forum Fragen gestellt werden, die häufig innerhalb einiger Tage von den Entwicklern oder anderen Nutzern beantwortet werden. Jedoch kommt es auch vor, dass Fragen unbeantwortet bleiben.

Intuitive Nutzbarkeit

Sobald man das Testprojekt korrekt erstellt hat, ist Robotium intuitiv bedienbar, da die Methoden sprechende Namen haben und zusätzlich im Javadoc dokumentiert sind.
Views auf der Benutzeroberfläche können einfach über den Text, den sie anzeigen, gefunden werden.

Automatisierung

Die Testergebnisse werden in Eclipse angezeigt und können als XML-Datei exportiert werden.
Zur zusätzlichen Dokumentation der Tests können auf Wunsch Screenshots erstellt werden.
Die Firma Bitbar bietet zudem das kostenpflichtige Tool „testdroid“ an, das automatisiertes Testen ermöglicht. Es kann als kostenlose Demo getestet werden.

Einführendes Beispiel

Vorausgesetzt wird, dass bereits Eclipse und das Android ADT plugin installiert wurden.

Testprojekt erstellen

Um ein neues Testprojekt zu erstellen, wählt man in Eclipse unter File → New → Other → Android → Android Test Project. Nachdem ein Name eingegeben und die zu testende App ausgewählt wurde, ist das Projekt erstellt.
Nun muss die robotium-jar eingefügt werden. Dazu die Properties des Testprojekts öffnen und bei „Java Build Path“ „Libraries“ auswählen und die JAR hinzufügen. Außerdem muss im Reiter „Order and Export“ ein Haken bei der neuen JAR setzen. Danach sollte ein Clean durchgeführt werden.

Testklasse

Als nächstes wird über [Testprojekt] → New → JUnitTestCase ein neuer Test erstellt. Als Supercalss wird android.test.ActivityInstrumentationTestCase2<[Zu testende Klasse]> angegeben.
In der neuen Klasse muss ein default-Konstruktor erstellt werden, der super([zu testende Klasse]) aufruft.
Wie bei JUnit üblich, ist eine setUp- und tearDown-Methode möglich. Sie werden vor, bzw. nach jedem Test ausgeführt. Außerdem können auch bekannte Methoden wie „assertTrue“ oder „assertEquals“ genutzt werden. Robotium bietet allerdings beispielsweise auch die Methode „solo.assertCurrentActivity“ an, mit der überprüft werden kann, ob man sich zur Zeit in der richtigen Aktivität befindet.
Es wird ein Solo-Objekt benötigt, das in der setUp-Methode initialisiert wird. In der tearDown-Methode sollten alle Activities mit der Solo-Methode „finishOpenedActivities“ beendet werden.
Nachdem das alles getan ist, können die Tests geschrieben werden. Die Testnamen müssen grundsätzlich mit „test...“ beginnen, da die Methoden sonst nicht als Tests erkannt werden.
Die Tests können gestartet werden über einen Rechtsklick auf das Projekt → Run As → Android JUnit Test.
Eine erste Testklasse könnte dann beispielsweise so aussehen:
public class FirstTest extends ActivityInstrumentationTestCase2
{ private Solo solo; public FirstTest() { // Die zu testende Activity muss angegeben werden super(Main.class); } protected void setUp() throws Exception { solo = new Solo(getInstrumentation(), getActivity()); } protected void tearDown() throws Exception { // Alle Activities schliessen solo.finishOpenedActivities(); } public void testFirst() { // Momentane Activity pruefen solo.assertCurrentActivity("Nicht in Main", Main.class); // Auf Element der Liste klicken, in dem "Tabs" steht solo.clickOnText("Tabs"); solo.assertCurrentActivity("Nicht in Tabs", Tabs.class); // Simuliert Swipe von rechts nach links und wechselt dadurch die Tabs solo.drag(300, 50, 300, 300, 1); solo.drag(300, 50, 300, 300, 1); boolean checkedBefore = solo.isToggleButtonChecked(0); // Ist der Button gerade "an" oder "aus"? // Klick auf den ersten ToggleButton List toggleButtons = solo.getCurrentViews(ToggleButton.class); solo.clickOnView(toggleButtons.get(0)); // Ueberpruefen, ob sich der Wert geaendert hat boolean checkedAfter = solo.isToggleButtonChecked(0); assertTrue("Der ToggleButton wurde nicht angeklickt", checkedBefore!=checkedAfter); } }
In dem Test wird in die Tabs-Activity der zu testenden App gewechselt und dort mithilfe der drag-Methode vom ersten zum letzten Tab geswipet. Hierbei ist zu beachten, dass die Methode durch die Angabe von absoluten X- und Y-Werten stark von der Auflösung des Testgeräts abhängig ist. Die Daten sollten also möglichst dynamisch an das Gerät angepasst werden. Danach wird auf den ToggleButton, der sich in dem Tab befindet, geklickt. Die meisten Views können auf drei verschiedene Arten abgerufen, angeklickt oder geändert werden. Es kann direkt das Objekt übergeben werden, es kann der Text, den der View darstellt, angegeben werden oder der View kann über die Angabe des Index identifiziert werden. Damit ist gemeint, der wie vielte View das Objekt ist. Da der Text des ToggleButtons von seinem An/Aus-Status abhängig ist, ist es ungünstig darüber zu navigieren. Jedoch bietet Solo keine Methode an, mit der ToggleButtons über den Index oder das eigentliche Objekt angesprochen werden können. Stattdessen gibt es diese nur für allgemeine Views oder normale Buttons. Da aber nicht unbedingt bekannt ist, der wie vielte Button der gesuchte ToggleButton ist, ist es ungünstig diese Funktion zu nutzen. Darum wird in diesem Fall als erstes nach allen ToggleButtons in der Activity gesucht und danach der erste (und einzige) über die Methode „clickOnView“ angeklickt. Zum Schluss wird überprüft, ob der Button-Wert tatsächlich geändert wurde.

Weitere Tests

Alle vorgestellten Testsklassen testen die App „AndroidTest“, die speziell für das Testen entwickelt wurde. Eine Beschreibung der App kann hier gefunden werden.

NewContactTest
In der Klasse „NewContactTest“ wird getestet, ob neue Kontakte über das Formular erfolgreich hinzugefügt werden können, bzw. ob fehlerhafte Daten abgelehnt werden.
Da bei allen Testfällen zuerst aus der Main zum Formular navigiert werden muss, wurde dieser Vorgang in eine eigene Funktion ausgelagert:

private void goToNewContact() {
	solo.clickOnText("Tabs"); // Auf Element der Liste klicken, in dem "Tabs" steht
	solo.assertCurrentActivity("Nicht in Tabs", Tabs.class);
	solo.clickOnButton("neuer Kontakt");
	solo.assertCurrentActivity("Nicht in NewContact", NewContact.class);
}
                
Eigentlich kann die zu testende Activity direkt in der Test-Klasse angegeben werden, sodass nicht zu dieser hin navigiert werden muss. Jedoch ist es bei diesen Tests nötig, dass die vorherige Activity bekannt ist, da nach dem erfolgreichen Speichern wieder zu dieser zurück gewechselt wird. Darum wird in diesem Test trotzdem die Main als zu testende (bzw. „Startactivity“) genutzt.
Um zu testen, ob gültige Daten in der Datenbank gespeichert werden, wurde folgender Test geschrieben:
public void testValidData() {
	// Aufnahme von Screenshots beginnen
	solo.startScreenshotSequence("testNewContact", 50, 250, 100);
	goToNewContact(); // Zur NewContact-Activity navigieren

	String firstname = "Peter";
	String lastname = "Meier2";
	String phone = "0123456789";
	int year = 1971;
	int month = 9;
	int day = 22;
		
	// Werte in die EditText-Felder schreiben
	solo.enterText(0, firstname);
	solo.enterText(1, lastname);
	solo.enterText(2, phone);

	// Geburtsdatum im DatePicker-Dialog angeben
	solo.clickOnButton(0); // Auf den erste Button klicken (Geburtsdatum)
	solo.setDatePicker(0, year, month, day); // Das Datum des DatePickers aendern
	solo.clickOnButton(0); // Auf den Bestaetigungsbutton des Dialogs klicken

	solo.clickOnButton("Kontakt speichern");

	solo.assertCurrentActivity("Nicht zurueck in Tabs", Tabs.class);

	// Die eingegebenen Daten muessten jetzt in der Liste aller Kontakte angezeigt werden.
	// Wenn ein Wert nicht gefunden wird, ist ein Fehler beim Speichern aufgetreten.
	String failMessage = "Der Eintrag wurde nicht gespeichert oder wird nicht korrekt auf der Oberflaeche angezeigt.";
	assertTrue(failMessage + " (Vorname)", solo.searchText(firstname));
	assertTrue(failMessage + " (Nachname)", solo.searchText(lastname));
	assertTrue(failMessage + " (Telefonnr)", solo.searchText(phone));
	String birthdate = createDateString(year, month + 1, day);
	assertTrue(failMessage + " (Geburtsdatum)", solo.searchText(birthdate));
		
	solo.stopScreenshotSequence();  // Aufnahme von Screenshots beenden
}
                
Als erstes wird die Aufnahme von Screenshots begonnen. Mit ihnen kann man den Testverlauf dokumentieren. Sie werden auf der SD-Karte des Geräts, auf dem getestet wird, gespeichert. Darum ist hierfür die Permission „WRITE_EXTERNAL_STORAGE“ in der zu testenden App nötig.
Danach werden die Testdaten in das Formular eingetragen. Die einfachen Textfelder werden über die Methode „enterText()“ befüllt. Danach wird das Geburtsdatum angegeben, indem auf den Button klickt, der das momentan eingestellte Datum darstellt. Daraufhin öffnet sich ein DatePicker, der über die Methode „setDatePicker()“ konfiguriert wird. Nachdem die Daten ohne Beanstandung abgeschickt wurden, wechselt die App zurück in die Tab-Activity. Hier wird nun überprüft, ob die gerade eingegebenen Daten in der Liste wiedergefunden werden.
Zuletzt wird noch die Screenshot-Aufnahme beendet. Die Screenshots werden im Ordner „Robotium-Screenshots“ gespeichert. Solo bietet keine Funktion, mit der die Screenshots gelöscht werden können (z.B. wenn der Testfall erfolgreich war und die Screenshots deswegen nicht benötigt werden). Bei einem erneuten Testdurchlauf werden sie aber überschrieben.

Zusätzlich zum erfolgreichen Speichern der Daten wird auch getestet, was bei ungültigen Daten passiert. Der Vorname eines Kontakts darf nicht leer sein. Dies wird in folgendem Testfall überprüft:
public void testEmptyFirstname() {
	goToNewContact();  // Zur NewContact-Activity navigieren

	solo.enterText(0, "");  // Vorname-Feld suchen und Wert eingeben

	solo.clickOnButton("Kontakt speichern");

	solo.assertCurrentActivity("Nicht mehr in NewContact", NewContact.class);

	// Toast-Message ueberpruefen
	boolean toastShown = solo.waitForText("Es muss ein Vorname angegeben werden.");
	assertTrue("Der Toast wurde nicht angezeigt.", toastShown);
}
                
Da die Daten nicht übernommen werden dürfen, wird auch nicht zurück in die Tab-Activity geleitet. Stattdessen wird ein Toast als Fehlermeldung ausgegeben. Es wird überprüft, ob dieser Toast angezeigt wird, indem die Solo-Methode „waitForText“ genutzt wird.

SaveTestdataTest
Neben der Datenbank wird auch die Speicherung in den SharedPreferences im Testdaten-Tab getestet.
Dazu wurde im ersten Test überprüft, ob die geänderten Daten wieder zurückgesetzt werden, wenn die Activity beendet wird, ohne dass auf den Speichern-Button gedrückt wurde.

public void testChangeButDontSave() {
	goToTestdata();  // zum Testdaten-Tab gehen

	solo.waitForActivity(Main.class, 1500);  // Warten bis Views geladen wurden

	// Bisherige Daten
	List seekBars = solo.getCurrentViews(SeekBar.class);
	int seekbarValueBefore = seekBars.get(0).getProgress();
	boolean toggleButtonCheckedBefore = solo.isToggleButtonChecked(0);
	EditText et_text = solo.getEditText(0);
	String textBefore = et_text.getText().toString();

	// Neue Daten eingeben
	solo.setProgressBar(0, 8);
	List toggleButtons = solo.getCurrentViews(ToggleButton.class);
	solo.clickOnView(toggleButtons.get(0));
	solo.enterText(0, "Test...");

	// Activity verlassen und neu beginnen
	solo.clickOnView(solo.getView(android.R.id.home));
	goToTestdata();

	solo.waitForActivity(Main.class, 1500);

	// "Neue" Werte ueberpruefen
	seekBars = solo.getCurrentViews(SeekBar.class);
	int seekbarValueAfter = seekBars.get(0).getProgress();
	assertEquals("Der Wert der Seekbar wurde geaendert.", seekbarValueBefore, seekbarValueAfter);
	boolean toggleButtonCheckedAfter = solo.isToggleButtonChecked(0);
	assertEquals("Der Wert des ToggleButtons wurde geaendert.", toggleButtonCheckedBefore, toggleButtonCheckedAfter);
	et_text = solo.getEditText(0);
	String textAfter = et_text.getText().toString();
	assertEquals("Der Text wurde geaendert.", textBefore, textAfter);
}
                
Ähnlich wie beim „NewContactTest“ wurde als erstes zur richtigen Activity navigiert, da auch hier die vorherige Activity benötigt wird.
Danach wird mit der Solo-Methode „waitForActivity“ 1,5 Sekunden darauf gewartet, dass die Views geladen wurden. Wird dies nicht getan, werden bei „getCurrentViews“ keine Objekte gefunden.
Nach der Wartezeit werden die aktuellen Daten ausgelesen und gespeichert, um sie später vergleichen zu können. Als nächstes werden neue Werte angegeben. Diese werden aber nicht mit einem Klick auf den Speichern-Button bestätigt. Stattdessen wird die Activity beendet, indem auf den zurück-Button in der ActionBar geklickt wird. Er hat immer die ID „android.R.id.home“ und kann darüber gefunden werden. Von dort aus wird wieder zum Tab navigiert. Es wird wieder kurz auf die Views gewartet. Dann können die zuvor ausgelesenen Werte mit den tatsächlichen momentanen Werten verglichen werden. Sie sollten gleich sein, da die Änderungen verworfen werden sollten.

Im zweiten Testfall wird das Speichern der Daten überprüft.
public void testChangeAndSave() {
	goToTestdata(); // zum Testdaten-Tab gehen

	// Daten festlegen
	int seekbarValue = 2;
	String text = "Mein Test-Text";

	// Neue Daten eingeben und speichern
	solo.setProgressBar(0, seekbarValue);
	List toggleButtons = solo.getCurrentViews(ToggleButton.class);
	solo.clickOnView(toggleButtons.get(0));
	solo.enterText(0, text);
	solo.clickOnButton("Speichern");
		
	// Activity verlassen und neu beginnen
	solo.clickOnView(solo.getView(android.R.id.home));
	goToTestdata();

	solo.waitForActivity(Main.class, 1500);  // Warten bis Views geladen wurden

	// Neue Werte ueberpruefen
	List seekBars = solo.getCurrentViews(SeekBar.class);
	int seekbarValueAfter = seekBars.get(0).getProgress();
	assertEquals("Der Wert der Seekbar wurde nicht gespeichert.", seekbarValue, seekbarValueAfter);
	boolean toggleButtonCheckedAfter = solo.isToggleButtonChecked(0);
	assertEquals("Der Wert des ToggleButtons wurde nicht gespeichert.", true, toggleButtonCheckedAfter);
	EditText et_text = solo.getEditText(0);
	String textAfter = et_text.getText().toString();
	assertEquals("Der Text wurde nicht gespeichert.", text, textAfter);
}
                
Er verläuft ähnlich wie der vorherige Testfall. Es wird zum Tab navigiert, die neuen Daten werden eingegeben und danach über einen Klick auf den „Speichern“-Button bestätigt. Danach wird die Activity beendet und neu gestartet. Hier muss wieder kurz gewartet werden, bevor die angezeigten Daten mit den zuvor eingetragenen Werten verglichen werden.

Blackbox-Testing
Zum Blackbox-Testing wird die APK der zu testenden App benötigt. Außerdem muss sie eine Debug-Signatur haben. Ist dies nicht der Fall, muss sie re-signed werden. Dazu kann das Tool Re-Signer von troido genutzt werden (Download hier).
Danach muss die neu signierte App auf dem Gerät bzw. Emulator installiert werden.
Nun kann das Testprojekt erstellt werden. Dabei muss beim Punkt „Select Test Target“ „This project“ ausgewählt werden.
Bei einem neuen Test Case muss folgendes beachtet werden: Da das Testprojekt nicht die Klassen der zu testenden App kennt, können diese nicht mehr direkt angegeben werden. Darum kann ActivityInstrumentationTestCase2 nicht parametisiert werden. Um die App zu starten, muss jedoch im Konstruktor die Main-Activity angegeben werden. Deshalb muss der Klassenname als String erstellt werden, um dann mit der Methode Class.forName() die Klasse zu finden. Den Namen der Main-Activity erfährt man beim neu-signieren mit Re-Signer.
Die von oben bekannte erste Test-Klasse sieht als Blackbox-Test folgendermaßen aus:

public class BlackboxTest extends ActivityInstrumentationTestCase2 {

	private static final String LAUNCHER_ACTIVITY_FULL_CLASSNAME = "de.hsos.androidtests.Main";

	private Solo solo;

	private static Class launcherActivityClass;

	static {
		try {
			launcherActivityClass = Class.forName(LAUNCHER_ACTIVITY_FULL_CLASSNAME);
		} catch (ClassNotFoundException e) {
			throw new RuntimeException(e);
		}
	}

	public BlackboxTest() {
		// Die zu testende Activity muss angegeben werden
		super(launcherActivityClass);
	}

	protected void setUp() throws Exception {
		solo = new Solo(getInstrumentation(), getActivity());
	}

	protected void tearDown() throws Exception {
		// Alle Activities schliessen
		solo.finishOpenedActivities();
	}

	public void testFirst() {
		// Momentane Activity pruefen
		solo.assertCurrentActivity("Nicht in Main", launcherActivityClass);
		// Auf Element der Liste klicken, in dem "Tabs" steht
		solo.clickOnText("Tabs");
		solo.assertCurrentActivity("Nicht in Tabs", "Tabs");

		// Simuliert Swipe von rechts nach links und wechselt dadurch die Tabs
		solo.drag(300, 50, 300, 300, 1);
		solo.drag(300, 50, 300, 300, 1);

		boolean checkedBefore = solo.isToggleButtonChecked(0); // Ist der Button gerade "an" oder "aus"?

		// Klick auf den ersten ToggleButton
		List toggleButtons = solo.getCurrentViews(ToggleButton.class);
		solo.clickOnView(toggleButtons.get(0));

		// Ueberpruefen, ob sich der Wert geaendert hat
		boolean checkedAfter = solo.isToggleButtonChecked(0);
		assertTrue("Der ToggleButton wurde nicht angeklickt", checkedBefore != checkedAfter);
	}

}