Home

 

 

TÜV für Android-Apps


Ein Beitrag von Arno BeckerArno Becker

Wer Apps herausgibt, die nicht fehlerfrei sind, riskiert schlechte Bewertungen im Android Market oder verärgert Kunden. Um dem vorzubeugen sollte man seine Anwendung gründlich testen. Dass man mit wenig Aufwand schon viel erreichen kann, zeigt dieser Artikel.

Dem Thema Testen wird in Softwareprojekten häufig zu wenig Zeit und Beachtung geschenkt. Schlimmer sieht es bei der Entwicklung von mobilen Anwendungen aus. Die Resultate sieht man in den verschiedenen Marktplätzen, über die Apps vertrieben werden. Häufig werden schlechte Bewertungen damit begründet, dass die App nicht fehlerfrei läuft. Schlimmer ist es, wenn man einem Geschäftskunden die fertige Anwendung als Produkt ausliefert und in der Praxis kaum etwas funktioniert.

Dabei lässt sich mit etwas Hintergrundwissen und wenig Aufwand eine Anwendung weitgehend automatisiert testen. Hat man einmal ein Testkonzept umgesetzt, lässt es sich leicht auf jede weitere Anwendung anpassen.

Testprojekt aufsetzen

In das Android-SDK ist eine auf JUnit [1] aufsetzende Testumgebung integriert (Package „android.test“). Damit der Testcode nicht als Teil der Anwendung ausgeliefert wird, schreibt man die Tests in einem separaten Testprojekt neben der eigentlichen Anwendung. Wir legen also zunächst ein Android-Testprojekt an. Man macht dies am besten direkt, wenn man ein neues Projekt anlegt. Im Wizzard zum Anlegen eines neuen Android-Projekts kann man als zweiten Schritt (Schaltfläche „Next >“ drücken) gleich ein Testprojekt anlegen.

Alternativ kann man für bestehende Projekte nachträglich ein Testprojekt anlegen. Dazu klickt man das betreffende Projekt im Package Explorer an und wählt im Kontextmenü der rechten Maustaste die Option Android Tools -> New Test Project…. Abbildung 1 zeigt den Wizzard zum Anlegen des Testprojekts.

Testobjekt anlegen

Abbildung 1 | Ein Testprojekt anlegen

Als Namen für das Testprojekt hat es sich bewährt, den Namen des zu testenden Projekts mit dem Suffix .tests zu verwenden. Als Test Target wählt man das zu testende Projekt. Bei Build Target sollte man den gleichen API Level wie das zu testende Projekt verwenden.

In diesem Artikel schreiben wir einen Test für die Anwendung „Verleihnix“. Sowohl die Verleihnix-Anwendung, als auch das Testprojekt liegen der CD dieses Hefts bei. Verleihnix ermöglicht es sich zu merken, an wen man zu welchem Zeitpunkt was verliehen hat. Die Anwendung zeigt auf der Startseite ein Formular (siehe Abbildung 2). Erfasst man hierüber einen verliehenen Gegenstand, so wird dieser in einer Datenbank gespeichert. Dies wollen wir testen.

Eingabeformular

Abbildung 2 | Eingabeformular der zu testenden Anwendung

Das Anlegen einer neuen Testklasse kann ebenfalls über einen Wizzard erfolgen. Man klickt das Package des Testprojekts an, in dem man die Testklasse erzeugen möchte. Über File -> New -> JUnit Test Case ruft man den Wizzard auf.

Um Activities zu testen wählt man hier „New JUnit 3 test“ aus. Bei „Name“ gibt man den Namen der zu testenden Activity an. Bei „Superclass“ tippt man „android.test.“ ein und drückt auf Browse. Nun bekommt man einige Basis-Testklassen aus dem Package android.test angezeigt. Wir verwenden hier ActivityInstrumentationTestCase2. Diese Klasse bietet die Möglichkeit, eine Activity „fernzusteuern“ und auf ihre Views zuzugreifen. So lassen sich Werte auslesen, Formularfelder füllen und Schaltflächen und Menüoptionen anklicken

Basisklassen für Android-Tests

Es gibt noch eine Vielzahl weiterer Basisklassen für Android JUnit-Tests. Im Falle eines Service, eines Content Providers oder wenn Ressourcen oder das Dateisystem getestet werden sollen, verwendet man andere Basisklassen. Neben der Klasse ActivityInstrumentationTestCase2 kann man folgende Klassen häufig verwenden:

  • ProviderTestCase2: Ermöglicht den Test von Content Providern. Mittels Content Providern läßt sich beispielsweise eine Schnittstelle zu den persistenten Daten einer Anwendung für andere Anwendungen realisieren.

  • ServiceTestCase: Ermöglicht isolierte Tests von Services. Hierüber lässt sich das Verhalten eines Services über seine Schnittstellen in allen Phasen seines Lebenszyklus testen.

  • AndroidTestCase: Ermöglicht das Testen einer Anwendung. Dieser Testfall ermöglicht den Zugriff auf Ressourcen oder auf den Context der Anwendung. Über den Context hat man beispielsweise Zugriff auf das Dateisystem oder die Datenbank einer Anwendung.

Der richtige Context für Tests

Jedoch ist gerade der Context einer Anwendung bisweilen kritisch, wenn es ums Testen geht. Denn die Tests steuern die zu testende Anwendung ja nur fern. Daher arbeitet die Anwendung während der Laufzeit des Tests auf ihren eigenen Ressourcen und Daten und kann diese verändern.

Dies ist aber oft unerwünscht, da man beispielweise die Datenbank fortwährend mit Testdaten vollschreibt, die dann bei Anwendertests stören.

Besser ist es, einen speziellen Context zu verwenden, der für die automatischen Tests mittels JUnit unter anderem eigene Datenbanken und ein eigenes Dateisystem nur für Testzwecke zur Verfügung stellt.

Mittels eines geeigneten Test-Context kann man die Anwendung, bzw. die zu testende Komponente während des Tests isoliert laufen lassen, ihr aber trotzdem Zugriff auf ihre Systemumgebung (Ressourcen, Dateisystem, Datenbanken etc.) gewähren. Drei häufig verwendete Test-Contexte sind:

  • MockContext: Zwar wirft jede seiner Methoden eine UnsupportedOperationException, jedoch kann man durch Überschreiben seiner Methoden den Context einer Anwendung simulieren.

  • IsolatedContext: Verhindert den Zugriff auf einen Großteil der Systemumgebung, gibt dem Betriebssystem jedoch die Möglichkeit, mit dem Context zu kommunizieren. Wird beispielsweise verwendet um Broadcast Receiver und Services zu testen oder um zu prüfen, ob die Anwendung die Berechtigung hat, eine bestimmte URI aufzurufen.

  • RenamingDelegatingContext: Verhindert, dass die Testklasse Daten der Anwendung verändert. Bei Verwendung dieses Contextes wird allen im Test verwendeten Dateien und Datenbanken ein Präfix vorangestellt. So greift ein Testfall beispielsweise nicht auf die Datenbank meine.db zu, sondern auf die strukturell identische test.meine.db. Für den Programmierer des Tests ist das völlig transparent. Er schreibt den Test so, als würde er auf die Original-Datenbestände zugreifen.

Instrumentierung

Android-Tests sind eigenständige Anwendungen, die aber im Prozess der zu testenden Anwendung laufen. Dadurch ist es möglich, auf die Laufzeitumgebung und die Komponenten (Activities, Services, Content Provider) der zu testenden Anwendung zuzugreifen. Das Kontrollieren der Anwendung und der Zugriff auf die Laufzeitumgebung (z.B. auf die Tasten des Emulators) nennt sich „Instrumentierung“. Jede der Test-Basisklassen stellt eine Vielzahl geeigneter Methoden für das Fernsteuern der Anwendung bereit.Der Zugriff auf die Views einer Activity muss dagegen im UI-Thread der zu testenden Anwendung erfolgen. Hierfür stehen unter anderem eine Annotation (UiThreadTest) und die Methode runOnUiThread der Activity zur Verfügung. Die folgende Implementierung eines Oberflächentests verdeutlicht den Unterschied zwischen Instrumentierung und dem direkten Arbeiten im UI-Thread der Anwendung zum Bearbeiten von Oberflächenelementen.

Oberflächentests

Wir beschränken uns hier auf den Test einer einzelnen Activity. Dabei verzichten wir bewusst auf den Einsatz des Testframeworks „Robotium“ [2], da das Prinzip der Instrumentierung verdeutlicht werden soll und Robotium in diesem Heft ein eigener Artikel gewidmet ist.

Listing 1 zeigt das Grundgerüst der Testklasse für das Formular LoanForm der Verleihnix-Anwendung.

Listing 1: Grundgerüst der Testklasse für das Formular „LoanForm“

Listing 1: Grundgerüst einer Activity-Testklasse

public class LoanFormTest extends ActivityInstrumentationTestCase2 {
    private LoanForm mActivity;
    private Spinner mSpinner;
    private EditText mEdtWhom;
    private EditText mEdtAmount;
    private ImageButton mBtnSave;
 
    private String mRecipientVerifyKey;
 
    public LoanFormTest() {
        super("de.visionera.loan", LoanForm.class);
    }
 
    @Override
    public void setUp() throws Exception {
        super.setUp();
 
        mActivity = getActivity();  
 
        mSpinner = (Spinner)mActivity.findViewById(de.visionera.loan.R.id.sp_what);    
        mEdtWhom = (EditText)mActivity.findViewById(de.visionera.loan.R.id.edt_whom);
        mEdtAmount = (EditText)mActivity.findViewById(de.visionera.loan.R.id.edt_amount);
        mBtnSave = (ImageButton)mActivity.findViewById(de.visionera.loan.R.id.bt_save);
    }
 
    @Override
    public void tearDown() throws Exception {        
        super.tearDown();
    }}

Verwendet man den Wizzard zum Anlegen einer Testklasse darf man auf keinen Fall vergessen, den Konstruktor anzupassen, da der Wizzard automatisch einen unpassenden Konstruktor generiert. Die Klasse muss einen parameterlosen Konstruktor haben, der Wizzard legt jedoch einen mit einem Parameter vom Typ String an.

Die super-Methode muss den Namen des zu testenden Packages und die zu testende Klasse hochreichen.

Wie bei normalen JUnit-Tests wird die setUp-Methode vor jeder der noch zu implementierenden Testmethoden aufgerufen. Daher werden hier alle Views der Activity geladen und als Attribute der Klasse zwischengespeichert.

An Schluss jedes Tests wird die tearDown-Methode aufgerufen. Für die beiden gleich zu implementierenden Tests sind jedoch keine Aufräumarbeiten notwendig.

Neustart von Activities

Sieht die Activity nach einem Neustart noch genauso aus wie vorher?

Der letzte Punkt wird oft unterschätzt. Dreht man den Bildschirm vom Hoch- ins Querformat wird jede laufende Activity beendet und neu gestartet. Man kann diesen Mechanismus zwar in der eigenen Anwendung verhindern, dies ist aber oft nicht sinnvoll, da Änderungen der Systemeinstellungen von der Activity nicht bemerkt werden.

Ebenso kann es passieren, dass man mit einer Anwendung arbeitet, aber dann längere Zeit mit einer anderen Anwendung beschäftigt ist. Android behält es sich vor, im Falle knapper Systemressourcen Activities und Services ungefragt zu beenden. Daten in Formular-Eingabefeldern gehen dabei nicht verloren. Sie werden automatisch persistiert, wenn sie eine Id besitzen. Jedoch gehen die in den Attributen der Klasse gespeicherten Werte und die Zustände von einigen View-Elementen verloren und. Dazu gehören beispielweise die in einem Spinner ausgewählten Werte oder die in einem DatePicker oder TimePicker eingestellten Zeitpunkte.

Füllt der Anwender ein Formular aus und dreht anschließend den Bildschirm, wird die Activity im Normalfall beendet und neu gestartet. Springen nun beispielsweise Spinner und DatePicker auf die Default-Werte zurück, weil man vergessen hat, diese in der onRestoreInstanceState- oder onPause-Methode der Activity zu speichern, muss der Anwender die Werte erneut eingeben.

Listing 2: Test auf Erhalt der Zustände einer Activity

private void fillLoanFormForTest() {
    mRecipientVerifyKey = Long.toString({FNAMEL}">System.currentTimeMillis());
 
    mEdtWhom.requestFocus();
    mEdtWhom.setText(mRecipientVerifyKey);
 
    mSpinner.requestFocus();
    mSpinner.setSelection(2); // Geld
 
    mEdtAmount.requestFocus();
        mEdtAmount.setText("7");}
 
@UiThreadTestpublic void testRestartActivity() throws Exception {
    fillLoanFormForTest();
        mEdtAmount.requestFocus();
        mEdtAmount.setText("7")
    mActivity.finish();
    mActivity = getActivity();
 
    final int {FNAMEL}">pos = mSpinner.getSelectedItemPosition();
    final String selection = (String)mSpinner.getItemAtPosition({FNAMEL}">pos);
                assertEquals("Auswahl fehlgeschlagen", "Geld", selection);}

Listing 2 füllt in der Methode fillLoanFormForTest das Formular LoanForm der Verleihnix-Anwendung mit Testwerten. Die eigentliche Testmethode testRestartActivity wird mit UiThreadTest annotiert.

Dadurch läuft die Methode und ihre Untermethodenaufrufe im UI-Thread der zu testenden Anwendung. Nur so erhält man Zugriff auf die View-Elemente. Denn der Test selber läuft in einem anderen Thread (aber im gleichen Prozess) und Android erlaubt Zugriffe auf Views nur innerhalb des UI-Threads.

Jedes View-Element des Formulars (Abbildung 2) wird mittels der Methode requestFocus angesprungen und mit Testwerten gefüllt. Anschließend wird die Activity mit der Methode finish beendet. Gleiches geschieht, wie schon gesagt, wenn man die Bildschirmorientierung ändert oder Android im Falle knapper Systemressourcen die Activity beendet.

Durch den erneuten Zugriff mittels getActivity wird die Activity neu gestartet. Allgemein betrachtet kann nun einiges schiefgehen. Zur Laufzeit der Activity wurde außerhalb der Lifecycle-Methoden beispielsweise

  • ein AsyncTask oder ein ProgressDialog gestartet.

  • Werte in den Attributen der Activity zwischengespeichert.

  • eine Verbindung zu einem Service aufgebaut.

  • Werte von Views, wie z.B. DatePicker oder Spinner verändert.

Vergisst man bei der Implementierung solche Zustände von der sterbenden Activity in die neu erzeugte Activity hinüber zu retten, muss der Test dies bemerken. Der Test aus Listing 2 prüft den letzten Fall aus der obigen Auflistung für den Spinner im Formular. In der Methode fillLoanFormForTest wird im Spinner „Geld“ ausgewählt. Die Activity wird beendet und neu gestartet. Anschließend wird geprüft, ob im Spinner immer noch „Geld“ ausgewählt ist. Hat man den Zustand des Spinners bei der Implementierung nicht persistiert, geht er beim Neustart der Activity verloren und der Test schlägt fehl.

Funktionale Tests

Der zweite Test füllt das Formular und simuliert einen Druck auf die Schaltfläche „Speichern“. Das ausgefüllte Formular wird daraufhin in der Datenbank gespeichert. Anschließend wird geprüft, ob der Datensatz auch wirklich in der Datenbank angelegt wurde.

Listing 3: Speichern des Formulars LoanForm testen

public void testSaveLoadItem() throws Exception {
    mActivity.runOnUiThread(            new Runnable() {
        public void run() {
            fillLoanFormForTest();
            mBtnSave.requestFocus();
        }
    });
 
    SystemClock.{FNAMEL}">sleep(1000);
    sendKeys(KeyEvent.KEYCODE_DPAD_CENTER);
 
    final LoanDatabase db = new LoanDatabase(getInstrumentation().getTargetContext());
    final Cursor c = db.getReadableDatabase().rawQuery("Select * from " + 
    LoanItemTbl.TABLE_NAME, null);
 
    mActivity.startManagingCursor(c);
    c.moveToLast();
 
    final String what = c.getString(c.getColumnIndexOrThrow(LoanItemColumns.LOANITEMTYPE));
    final String whom = c.getString(c.getColumnIndexOrThrow(LoanItemColumns.RECIPIENT));
 
    assertEquals("Art wurde nicht gespeichert", "Geld", what);
    assertEquals("Empfaenger wurde nicht gespeichert", mRecipientVerifyKey, whom);}

In Listing 3 wird auf die Annotation UiThreadTest verzichtet. Das Füllen des Formulars und das Fokussieren der Schaltfläche muss im UI-Thread erfolgen. Daher wird der auszuführende Oberflächencode in der run-Methode eines Runnables implementiert. Das Runnable wird mittels der Methode runOnUiThread im UI-Thread ausgeführt. Aufgrund der Nebenläufigkeit wird mittels des Aufrufs von SystemClock.sleep(1000) gewartet, bis das Formular mit Testwerten gefüllt wurde. Erst dann wird der Druck auf die Schaltfläche ausgeführt. Dies erfolgt durch Aufruf von sendKeys. Diese Methode kann nicht im UI-Thread der zu testenden Anwendung ausgeführt werden, daher kann die Methode testSaveLoadForm nicht mit der Annotation UiThreadTest annotiert werden.

Der Code aus Listing 3 dient nur zur Demonstration des Zugriffs auf Views in einem Test ohne die Annotation UiThreadTest und kann stark optimiert werden.

Der Test selbst erfolgt dann durch Auslesen der gespeicherten Werte aus der Datenbank. Die Datenbankimplementierung braucht den Context der zu testenden Anwendung, welcher im Konstruktor übergeben wird. Mittels getInstrumentation().getTargetContext() bekommt man Zugriff darauf.

Der restliche Code dient zum Auslesen des letzten Datenbankeintrags, welcher mit den zuvor eingetragenen Formularwerten verglichen wird.

Verwenden eines Test-Context

Der Test in Listing 3 hat einen Nachteil. Er verändert die Daten der Anwendung. Meist möchte man JUnit-Tests und Anwendertests trennen und auf separaten Datenbeständen durchführen.

Wie oben schon gesagt, muss man in solchen Fällen einen Test-Context verwenden, der unter anderem eine eigene Datenbank verwendet. Leider verwendet die Test-Basisklasse ActivityInstrumentationTestCase2 den Context der zu testenden Anwendung. Dieser lässt sich nicht über Methoden dieser Klasse austauschen. Um auf einer separaten Testdatenbank zu arbeiten, müssen wir eine andere Test-Basisklasse wählen, die mit einem Test-Context arbeitet. Da die Verleihnix-Anwendung einen Content Provider besitzt, zeigen wir daran, wie ein Test mit einer Basisklasse mit Test-Context implementiert wird.

Test von Content Providern

Um Content Provider zu testen, verwendet man die Klasse ProviderTestCase2. Listing 4 zeigt das Grundgerüst der Klasse. Testwerte werden am Anfang der Klasse als Konstanten gespeichert. In der setUp-Methode wird ein Testdatensatz in die Datenbank geschrieben. Dazu wird ein Exemplar von LoanDatabase erzeugt. Als Parameter für den Konstruktor wird nun mittels getMockContext() ein Exemplar von IsolatedContext übergeben. Der IsolatedContext sorgt dafür, dass alle Datenbankoperationen nicht auf der Datenbank der zu testenden Anwendung („loan.db“), sondern auf einer parallelen Instanz mit Namen „test.loan.db“ erfolgen.

Listing 4: Grundgerüst der Content Provider-Testklasse

public class LoanProviderTest extends ProviderTestCase2 {
 
    private long mTestLoanId;
    private static final String TEST_ITEM = "Testgegenstand";
    private static final String TEST_LOANDATE = 
        Long.toString({FNAMEL}">System.currentTimeMillis());
    private static final String TEST_LOANTYPE = "Geld";
    private static final String TEST_RECIPIENT = "Deutsche Bank";
 
    public LoanProviderTest() {
        super(LoanListProvider.class, "de.visionera.loan.db");}
 
    protected void setUp() throws Exception {
        super.setUp();
 
        final SQLiteDatabase dbCon = new LoanDatabase(getMockContext()).getWritableDatabase();
 
        try {
            final ContentValues dataSet = new ContentValues();
            dataSet.put(LoanItemColumns.LOANITEM, TEST_ITEM);
            dataSet.put(LoanItemColumns.LOANDATE, TEST_LOANDATE);
            dataSet.put(LoanItemColumns.LOANITEMTYPE, TEST_LOANTYPE);
            dataSet.put(LoanItemColumns.RECIPIENT, TEST_RECIPIENT);
 
            mTestLoanId = dbCon.insertOrThrow(LoanItemTbl.TABLE_NAME, null, dataSet);
        } finally {
            if( dbCon != null ) {
                dbCon.close();
            }
        }
    }
 
    protected void tearDown() throws Exception {
        super.tearDown();
    }}

Der Content Provider der Verleihnix-Anwendung stellt lediglich Anfragemethoden zur Verfügung. Das Verändern von Datensätzen ist nicht erlaubt. Tests von Content Providern sollten nicht nur die implementierten Methoden testen, sondern auch prüfen, ob die „verbotenen“ Methoden auch wirklich nicht implementiert wurden. Listing 5 zeigt den Quellcode beider Testmethoden.

Listing 5: Testen von Zugriffsmethoden des Content Providers.

public class LoanProviderTest extends ProviderTestCase2 {
 
    private long mTestLoanId;
    private static final String TEST_ITEM = "Testgegenstand";
    private static final String TEST_LOANDATE = 
        Long.toString({FNAMEL}">System.currentTimeMillis());
    private static final String TEST_LOANTYPE = "Geld";
    private static final String TEST_RECIPIENT = "Deutsche Bank";
 
    public LoanProviderTest() {
        super(LoanListProvider.class, "de.visionera.loan.db");}
 
    protected void setUp() throws Exception {
        super.setUp();
 
        final SQLiteDatabase dbCon = new LoanDatabase(getMockContext()).getWritableDatabase();
 
        try {
            final ContentValues dataSet = new ContentValues();
            dataSet.put(LoanItemColumns.LOANITEM, TEST_ITEM);
            dataSet.put(LoanItemColumns.LOANDATE, TEST_LOANDATE);
            dataSet.put(LoanItemColumns.LOANITEMTYPE, TEST_LOANTYPE);
            dataSet.put(LoanItemColumns.RECIPIENT, TEST_RECIPIENT);
 
            mTestLoanId = dbCon.insertOrThrow(LoanItemTbl.TABLE_NAME, null, dataSet);
        } finally {
            if( dbCon != null ) {
                dbCon.close();
            }
        }
    }
 
    protected void tearDown() throws Exception {
        super.tearDown();
    }}

Die Testmethode testLoanProviderQuery führt eine Query über den Content Provider aus. Die Methode getProvider liefert automatisch einen IsolatedContext, der wie in der vorherigen Methode auf der Datenbank „test.loan.db“ arbeitet.

Die Id des zuvor gespeicherten Datensatzes ist bekannt und es werden alle in LoanItemColumns definierten Spalten der Tabelle außer der Id angefragt.

Da der Content Provider jedoch nur drei Spalten liefert, wird auf die korrekte Anzahl der Spalten geprüft. Die verbleibenden zwei Spalten werden auf korrekten Inhalt hin geprüft.

Die zweite Testmethode stellt sicher, dass der Content Provider keine Update-Funktionalität implementiert. Jeder Aufruf der Methode update muss eine UnsupportedOperationException (UOE) zur Folge haben. Daher wird im Catch-Block auf diesen Exception-Typ geprüft. Wurde die update-Methode doch implementiert oder wirft keine oder eine andere Exception als die UOE, schlägt der Test fehl.

Damit kennen wir nun die wichtigsten Grundlagen für automatisches Testen von Android-Anwendungen. Die Testanwendung lässt sich auch mittels Ant-Skripten von der Kommandozeile aus starten, so dass eine Integration von Android-Tests in eine Continous Integration-Umgebung (CI-Umgebung) wie zum Beispiel Hudson kein Problem ist [3].

Für Nicht-Android-Klassen, also Klassen, die keinen Android-Code enthalten, nimmt man am besten normale JUnit-Tests. Diese Testklassen werden in einem "android-freien" Java-Projekt erstellt.

Stresstests

Auch automatische Tests garantieren keine fehlerfreien Anwendungen. Ein Stresstest hilft, verborgene Fehler zu finden. Das Android-SDK stellt hierfür das Kommandozeilen-Programm Monkey zur Verfügung, welches sich im /tools-Verzeichnis befindet.

Die Wirkung von Monkey kann man sich als einen Affen vorstellen, der auf der Tastatur eines Android-Geräts rumspringt. Man kann eine bestimmte Anzahl von Zufallsereignissen angeben die während des Tests ausgeführt werden sollen. Beispiele für diese Ereignisse sind: Views aktivieren, Menüoptionen auswählen, Schaltflächen und Listenelemente anklicken, Bildschirm drehen etc.

Man kann sich leicht vorstellen, wie das System unter Last geraten kann, wenn sich hinter Schaltflächen und Menüoptionen rechen- und zeitintensive Operationen wie Location Based Services, Netzwerkoperationen oder komplexe Berechnungen verbergen.

Monkey wird über die ADB-Shell (ADB: Android Debug Bridge) gestartet. Als Parameter sollte mindestens ein Packagename der Anwendung (-p mein.package.name) angegeben werden, unter dem sich mindestens eine Activity befindet. Andernfalls testet Monkey nicht die zu testende Anwendung, sondern alle Anwendungen auf dem Gerät, ausgehend vom Startbildschirm.

Als zweiter Parameter wird eine Anzahl von zufällig auszuführenden Ereignissen definiert. Ein sinnvoller Wert ist hier 500 oder größer. Der Kommandozeilenaufruf für die Verleihnix-Anwendung sieht folgendermaßen aus:

adb shell monkey –p de.visionera.loan 500

Man kann im Emulator verfolgen, was Monkey mit der Anwendung macht. Tritt ein Fehler auf, wird dieser in der Konsole ausgegeben. Alternativ kann man in Eclipse einen Blick in die LogCat werfen.

Möchte man, dass nicht nur Fehler, sondern auch Informationen über die ausgeführten Ereignisse in der Konsole ausgegeben werden, muss als zusätzlichen Kommandozeilenparameter –v eingeben. Durch mehrfache Angabe von –v hintereinander wird Monkey zunehmend geschwätziger.

Ein wichtiger Parameter ist –s. Das „s“ steht für „seed“ und ist die Wurzel für den Zufallszahlengenerator, den Monkey verwendet. Gibt man über diesen Parameter immer die gleiche Zahl als Wurzel vor, führt Monkey immer die gleiche Folge von Ereignissen aus. So werden Tests reproduzierbar. Denn nicht immer ist ganz klar, was Monkey genau gemacht hat, bevor ein Fehler aufgetreten ist. Ein Beispiel:

adb shell monkey –v –v –s 12345 –p de.visionera.loan 500

Es gibt eine Reihe weiterer Parameter, die in der Android-Dokumentation beschrieben sind [4].

Anwender-Tests

Neben den automatisch ausführbaren Tests sollte man auch gründliche Anwendertests durchführen.

Da es viele verschiedene Android-Geräte mit unterschiedlichen Bildschirmauflösungen gibt, gilt es beim Anwendertest nicht nur Fehler zu finden, sondern auch auf Bedienbarkeit und Aussehen der Oberflächen zu achten. Für jede Activity sollte man mindestens testen, ob sie in Hoch- und Querformat gut aussieht und ob alle Views angezeigt werden.

Sinnvoll ist es auch, sich verschiedene Android Virtual Devices (AVD) anzulegen. Diese sollten sich mindestens in den Bildschirmgrößen unterscheiden. So sieht man, ob eine Anwendung auf einem Gerät mit kleinem Bildschirm genauso gut funktioniert, wie auf einem großen Bildschirm. Evtl. stellt man nun erst fest, dass man zwei getrennte Layouts für unterschiedliche Bildschirmauflösungen erstellen muss.

Fazit

Das Thema „Testen“ ist damit noch lange nicht abgeschlossen. Was funktionale und automatische Tests angeht hat dieser Artikel jedoch eine erste Einführung gegeben. Auf Basis dieser Informationen lassen sich eigene Komponententests schreiben und das Package „android.test“ des SDK nach und nach erkunden.

Quellen:

[1] JUnit-Projektseite http://www.junit.org/
[2] Robotium http://code.google.com/p/robotium/
[3] Automatische Tests mit Ant ausführen http://developer.android.com/guide/developing/testing/testing_otheride.html#RunTestsAnt
[4] Basic Use of Monkey http://developer.android.com/guide/developing/tools/monkey.html
[5] „Android 2 – Grundlagen und Programmierung“ von Arno Becker und Marcus Pant. Mai 2010, dpunkt-Verlag, 411 Seiten