Freitag, 27. Mai 2011

Testen von erwarteten Exceptions mit JUnit4

Beim durcharbeiten einer Schulung zum Thema "Microtesting" werde ich gerade auf ein Feature in JUnit4 aufmerksam, welches ich bisher noch nicht kannte.

Wollte ich bisher testen, dass eine Methode eine Exception auslöst, so habe ich in etwa folgenden Test-Code geschrieben:

@Test
public void methodCallShouldThrowNumberFormatException() {
  try {
    objectUnterTest.methodUnderTest();
    fail("Should throw NumberFormatException");
  } catch ( NumberFormatException exc ) {
    // everything is fine
  }
}

Das ist zum einen etwas umständlich, zum anderen kann es je nach verwendeten Code-Analyse Einstellungen auch zu Warnungen führen, dass der Catch-Block nichts tut.

Schöner und deutlich einfacher ist die Verwendung des Attributes "expected" der @Test Annotation.

@Test ( expected = NumberFormatException.class )
public void methodCallShouldThrowNumberFormatException() {
  objectUnterTest.methodUnderTest();
}

Damit teile ich JUnit mit, dass ich bei der Durchführung dieses Tests erwarte dass eine NumberFormatException ausgelöst wird. Ist dies der Fall bewertet JUnit den Test als erfolgreich (grün). Fliegt keine oder eine andere Exception schlägt der Test fehl.

Als weiteres Attribut der @Test Annotation kann eine "timeout" Zeitspanne angegeben werden, nach welcher der Test mit einem Fehler abgebrochen wird. Sicherlich auch nicht falsch, das zu wissen.

Donnerstag, 14. April 2011

Android BallApp - Iteration 3 - Einlesen der Ballliste aus einer JSON-Datei

Ziel der dritten Iteration meiner Android BallApp:

Einlesen der vollständigen Ball-Liste in die SQLite Datenbank

Die vollständige Ball-Liste verwalte ich bisher in einer Excel-Datei.

Frage 1: Wie bekomme ich die Excel-Daten in die Android-Applikation?

Die Antwort ist einfach: Die Excel-Daten werden in eine ASCII-Datei exportiert und als Resource (in Android als Assets bezeichnet) ins Projekt kopiert.

Zur Speicherung der strukturierten Ball-Daten in einer ASCII-Datei wähle ich ein JSON-Datenformat. JSON erlaubt genau wie XML die Definition einer eigenen Datenstruktur, benötigt dafür aber deutlich weniger Platz, da die schließenden Tags wegfallen. Außerdem ist JSON ein gängiges Datei-Format und kann von Android gut verarbeitet werden (wie wir später noch sehen werden).

Die JSON-Daten lassen sich mit Excel gut generieren. Auf Details dazu will ich jetzt hier nicht näher eingehen, wen das interessiert, der möge sich bitte melden. Als kleiner Tipp: die Excel-Funktion VERKETTEN hilft.

Die ASCII-Datei ballliste.json sieht wie folgt aus:

[{hrst: "3D", name: "BOF BM 2007 Catherine Massem", gr: "k", 
         l: "l", s: "7", h: "", gw: "41"},
    {hrst: "3D", name: "BOF BMM 2008 GSP Malonne", gr: "k", 
        l: "l", s: "15", h: "", gw: "42"},
    {hrst: "3D", name: "BOF DkJM 2005 Oliver Rex Wiile", gr: "k", 
        l: "l", s: "1", h: "", gw: "40"},
    ...
    {hrst: "", name: "T4", gr: "k", 
        l: "r", s: "79", h: "", gw: "30"}]

Jedes Ball-Element besteht aus einer komma-separierten Liste von Name-Wert-Paaren und wird begrenzt von geschweiften Klammern. Die eckigen Klammern am Anfang und Ende definieren die Liste der einzelnen Ball-Elemente, ebenfalls komma-separiert.

Die JSON-Datei lege ich im Verzeichnis /assets in meinem Android-Projekt ab.

Frage 2: Wie lese ich die ASCII-Datei ein?

Das Einlesen von Dateien erfolgt in Android weitestgehend analog zu Java - genauso umständlich.

Den Zugriff auf die Asset-Datei erhalte ich über den Android Context, also über meine Activity. Über die Methode getAssets() bekomme ich den AssetManager. Dieser stellt mir wiederum die Methode open(String fileName) zur Verfügung, über die ich Asset-Dateien, also Dateien die im Verzeichnis /assets abgelegt sind, öffnen kann. Ich erhalte als Ergebnis einen InputStream.

Um die Datei einigermaßen komfortabel einlesen zu können, kapsele ich den InputStream in einen InputStreamReader und diesen wiederum in einen BufferedReader. So kann ich den Inhalt der Datei zeilenweise einlesen und in einem StringBuffer sammeln.

private String readJSONBallliste(Context context) {
      StringBuffer content = new StringBuffer();
      try {
        BufferedReader reader = new BufferedReader(
            new InputStreamReader(
                context.getAssets().open("ballliste.json")));
        String nextLine = reader.readLine();
        while (nextLine != null) {
          content.append(nextLine).append("\n");
          nextLine = reader.readLine();
        }
      } catch (IOException e) {
        Log.e(LOG_TAG, "Fehler beim Lesen der JSON-Datei", e);
      }
      return content.toString();
    }

Frage 3: Wie parse ich die JSON-Daten?

Das parsen der JSON-Daten ist in Android relativ einfach. Android bringt dafür die notwendigen Klassen im Package org.json bereits mit.

Bei meinem JSON-String handelt es sich um eine Liste von Objekten. Daher verwende ich ein JSONArray welches ich mit dem JSON-String initialisiere. Das JSONArray stellt die üblichen Methoden eines Arrays bzw. einer Liste bereit, die ich verwenden kann um durch die Liste zu iterieren. Leider implementiert das JSONArray kein Iterator oder Iterable Interface, so dass ich den altmodischen Weg gehen muss.

JSONArray jsonArray = new JSONArray(
        readJSONBallliste(context));
    for (int i = 0; i < jsonArray.length(); i++) {
      JSONObject nextBall = (JSONObject) 
          jsonArray.getJSONObject(i);
      Log.d(LOG_TAG, nextBall.getString("hrst") + 
          " - " + nextBall.getString("name"));
      ...
    }
Über die Methode getJSONObject(int index) lassen sich die einzelnen Ball-Elemente aus dem JSONArray auslesen. Das JSONObject stellt eine Reihe von Methoden zur Verfügung um zu einem Attribut-Namen den entsprechenden Wert abzufragen. Schreiben der JSON-Daten in die SQLite Datenbank Jetzt muss ich nur noch die Daten aus den JSONObjecten verarbeiten indem ich sie in die Datenbank schreibe. Die vollständige Methode dazu sieht wie folgt aus:
private void createData(SQLiteDatabase db) {
      Log.d(LOG_TAG, "Create Data...");
      String insert = "INSERT INTO " + TABLE_BAELLE + 
          " ( " + KEY_BALL_ID + ", " + KEY_BALL_FULL_NAME + 
          " ) VALUES ( ?, ? );";
      try {
        JSONArray jsonArray = new JSONArray(
            readJSONBallliste(context));
        for (int i = 0; i < jsonArray.length(); i++) {
          JSONObject nextBall = (JSONObject) 
              jsonArray.getJSONObject(i);
          Log.d(LOG_TAG, nextBall.getString("hrst") + 
              " - " + nextBall.getString("name"));
          db.execSQL(insert, new String[] { 
              String.valueOf(i), 
              nextBall.getString("name") });
        }
      } catch (JSONException e) {
        Log.e(LOG_TAG, "Fehler beim parsen der JSON-Daten", e);
      }
    }
Ich rufe die Methode beim initialen Anlegen der Datenbank auf, also in der onCreate(SQLiteDatabase db) Methode meines BallDbAdapters. Dem muss ich beim Initialisieren aus der Activity noch die Activity als Context mitgeben, damit ich Zugriff auf den AssetManager habe. Nach dem starten meiner App wird die Ball-Liste in die Datenbank geschrieben und die Liste mit den Ball-Namen wird angezeigt. Der vollständige Source Code kann wieder unter https://github.com/bobbyout/ballapp-android abgerufen werden.

Mittwoch, 6. April 2011

Android BallApp - Iteration 2 - Laden der Ballliste aus einer SQLite Datenbank

Hier nun endlich die nächste Iteration meiner Android BallApp.

Ziel dieser Iteration:

Laden der Ballliste aus einer Datenbank

Mit dieser Iteration werfe ich zunächst meinen ehrgeizigen Ansatz über Bord, meine Applikation test-getrieben zu entwickeln. Stattdessen versuche ich entsprechende Tests im Nachgang zu ergänzen. So fühlt sich die Einarbeitung in komplett neue Themen besser an.

Im Data Storage Developer Guide findet sich eine Dokumentation welche Data Storage Optionen zur Auswahl stehen:

  • Shared Preferences zum Speichern einfacher Daten in Form von Key-Value-Paaren.
  • Internal Storage zum Speichern privater Daten im Gerätespeicher.
  • External Storage zum Speichern öffentlicher Daten in einem externen Speicherbereich, der auch für andere Applikationen zugänglich ist.
  • SQLite Databases zum Speichern strukturierter Daten in einer privaten Datenbank.
  • Network Connection zum Speichern von Daten im Web über einen entsprechenden Network Service.

Für meine Ballliste wähle ich eine SQLite Datenbank. Die Zugriffe auf die Datenbank kapsele ich in einer Klasse BallDbAdapter. Entsprechend der empfohlenen Vorgehensweise im Developer Guide leite ich eine private DatabaseHelper Klasse von SQLiteOpenHelper ab. Darüber erhalte ich innerhalb des BallDbAdapters die eigentliche SQLiteDatabase.

private static class DatabaseHelper extends SQLiteOpenHelper {

    private static final String DATABASE_NAME = "balldb";
    private static final int DATABASE_VERSION = 1;

    DatabaseHelper(Context context) {
      super(context, DATABASE_NAME, null, DATABASE_VERSION);
    }
}

Eine SQLite Datenbank erhält einen eindeutigen Namen (balldb) sowie eine Versionsnummer (1) Auf die gehe ich gleich noch ein.

onCreate

Jetzt, wo ich Zugriff auf eine Datenbank-Instanz habe, stellt sich die Frage, wie definiere ich deren Struktur?
Dazu wird in der DatabaseHelper Klasse die abstrakte Methode onCreate der SQLiteOpenHelper Klasse überschrieben. Diese Methode wird aufgerufen, wenn die Datenbank zum ersten mal geöffnet wird. Hierin erfolgt die Initialisierung einer neuen Datenbank über einen Create String der in meinem Fall zunächst eine Tabelle baelle mit zwei Spalten _id und full_name definiert.

@Override
    public void onCreate(SQLiteDatabase db) {
      db.execSQL(databaseCreateString());
    }

    private String databaseCreateString() {
      StringBuilder builder = new StringBuilder();
      builder.append("CREATE TABLE baelle ( ");
      builder.append("_id INTEGER PRIMARY KEY AUTOINCREMENT, ");
      builder.append("full_name TEXT NOT NULL );");
      return builder.toString();
    }

onUpgrade und onDowngrade

Neben der onCreate Methode definiert die SQLiteOpenHelper Klasse weiterhin die abstrakte Methode onUpgrade die mein DatabaseHelper demnach überschreiben muss. Die onUpgrade Methode wird aufgerufen, wenn die Datenbank Versionsnummer, die im Konstruktor übergeben wird, größer ist als die Versionsnummer der bestehenden Datenbank. Das kann der Fall sein, wenn die Applikation aktualisiert wurde und danach zum ersten Mal wieder gestartet wird. Die Methode bekommt die bestehende und die aktuelle Versionsnummer übergeben. Sie ist dafür zuständig die notwendigen Änderungen durchzuführen um die bestehende Datenbank auf die neue Version anzuheben.

Analog zur onUpgrade Methode stellt die SQLiteOpenHelper Klasse auch eine onDowngrade Methode zur Verfügung. Diese wird dementsprechend aufgerufen wenn die aktuelle Datenbank Version der Applikation kleiner ist als die bereits vorhandene Datenbank. Die Methode ist in der SQLiteOpenHelper Klasse nicht als abstrakt definiert und muss daher auch in meiner DatabaseHelper Klasse nicht implementiert werden.

onOpen

Die onOpen Methode wird aufgerufen wenn die Datenbank geöffnet wird (beim erneuten Aufruf der Applikation). Sie ist ebenfalls nicht als abstrakt definiert.


Definition von Testdaten

Da meine BallApp noch keine Funktionalität besitzt um neue Datensätze zu erzeugen, muss ich nach wie vor ein paar Ball-Datensätze fest hinterlegen. Dafür definiere ich mir eine Methode fillTestData, die ich aus der onCreate Methode aufrufe.


private void fillTestData(SQLiteDatabase db) {
      String insert = "INSERT INTO baelle ( _id, full_name ) " + 
                      "VALUES ( ?, ? );";
      db.execSQL(insert, new String[] { 
            "1", "mg Maier Magic 2" });
      db.execSQL(insert, new String[] { 
            "2", "Nifo 2" });
      db.execSQL(insert, new String[] { 
            "3", "Reisinger Bo 1" });
    }

Damit ich während der weiteren Entwicklung beim Neustart meiner Applikation immer einen sauberen Datenbank-Stand erhalte, egal was ich geändert habe, ergänze ich schließlich noch die Methoden onUpgrade und onOpen. In beiden Methoden lösche ich jeweils die Tabelle baelle und erzeuge sie anschließend neu indem ich die onCreate Methode aufrufe.


@Override
    public void onUpgrade(SQLiteDatabase db, int oldVersion, 
                          int newVersion) {
      Log.w(LOG_TAG, "Upgrading database from version " + 
            oldVersion + " to " + newVersion + ", which will " +
            " destroy all old data");
      db.execSQL("DROP TABLE IF EXISTS baelle");
      onCreate(db);
    }

    @Override
    public void onOpen(SQLiteDatabase db) {
      Log.w(LOG_TAG, "Opening database -> destroy all old " +
            "data");
      db.execSQL("DROP TABLE IF EXISTS baelle");
      onCreate(db);
    }


Öffnen und Schließen der Datenbank

Die Implementierung der privaten DatabaseHelper Klasse ist damit zunächst vollständig. Im BallDbAdapter fehlen allerdings noch ein paar Methoden. Zunächst muss die Datenbank geöffnet werden, um damit zu agieren. Dafür erstelle ich die Methode open. Die Methode initialisiert zunächst eine Instanz des DatabaseHelper. Anschließend wird über diese die eigentliche SQLiteDatabase im Lesemodus (readable) geöffnet und zurückgegeben.

public void open() throws SQLException {
      databaseHelper = new DatabaseHelper(context);
      database = databaseHelper.getReadableDatabase();
    }


Zum Schließen der Datenbank dient die Methode close. Diese ruft lediglich die entsprechende Methode des DatabaseHelper auf.

public void close() {
      databaseHelper.close();
    }


Abfrage der Bälle

Schließlich fehlt mir noch eine Methode, die mir die in der Datenbank gespeicherten Bälle ausliest und zurückliefert.

public Cursor findAllBalls() {
      return database.query(TABLE_BAELLE,
      new String[] { KEY_BALL_ID, KEY_BALL_FULL_NAME },
      null, null, null, null, KEY_BALL_FULL_NAME);
    }

Die query Methode der SQLiteDatabase erwartet die üblichen Teile eines SQL SELECT-Statements in Form von einzelnen Parametern.

  • Als ersten Parameter erhält die Methode den Namen der Tabelle aus welcher die Daten abgefragt werden sollen. In unserem Fall ist das die Tabelle baelle (welche auch sonst, wir haben ja nur die eine). Den Tabellennamen habe ich zwischenzeitlich in eine Konstante TABLE_BAELLE ausgelagert.
  • Der zweite Parameter definiert die zu selektierenden Spalten der Tabelle. Die Spaltennamen werden in Form eines String-Arrays übergeben. Auch für die Spalten habe ich Konstanten definiert.
  • Der dritte Parameter erwartet einen Selection-String, d.h. eine SQL WHERE-Klausel (ohne das Schlüsselwort WHERE). In meinem Fall möchte ich die zu selektierenden Daten nicht einschränken, ich übergebe daher null.
  • Im zuvor beschriebenen Selection-String kann ich ? als Platzhalter für einzelne Argumente verwenden. Die für die Platzhalter zu verwendenden Werte werden im vierten Parameter in Form eines String-Arrays erwartet. Da ich keinen Selection-String benötige, benötige ich auch keine Argumente dafür und übergebe hierfür ebenfalls null.
  • In Parameter fünf kann ich einen SQL GROUP BY Ausdruck definieren (ohne das Schlüsselwort GROUP BY). Ich übergebe ebenfalls null.
  • Parameter sechs erlaubt die Definition einer SQL HAVING Klausel zum vorangegangenen GROUP BY (ohne das Schlüsselwort HAVING). Auch hier übergebe ich null.
  • Der siebte Parameter erwartet schließlich einen ORDER BY Ausdruck (ohne das Schlüsselwort ORDER BY). Hier übergebe ich die Spalte mit dem Ball-Namen.

Die Methode gibt einen Datenbank Cursor zurück, über den die einzelnen Datensätze abgerufen werden können. Diesen Cursor gebe ich als Ergebnis meiner findAllBalls Methode zurück.

Anpassung der BallListActivity

Nachdem ich alle Methoden zum Zugriff auf die Datenbank definiert habe, muss ich sie nur noch verwenden. Ich passe dazu die BallListActivity wie folgt an.

In der onCreate Methode initialisiere ich den BallDbAdapter und öffne darüber die Datenbank.

/** Called when the activity is first created. */
    @Override
    public void onCreate(Bundle savedInstanceState) {
      super.onCreate(savedInstanceState);
      ballDbAdapter = new BallDbAdapter(this);
      ballDbAdapter.open();
      setListAdapter(getBallListAdapter());
    }

In der onDestroy Methode schließe ich die Datenbank wieder.

@Override
    protected void onDestroy() {
      super.onDestroy();
      ballDbAdapter.close();
    }

Schließlich überarbeite ich die getBallListAdapter Methode, so dass diese die Ball-Liste aus der Datenbank lädt. Zuerst hole ich mir über den BallDbAdapter den Cursor mit den Ball-Datensätzen. Anschließend teile ich in der Activity mit, dass sie den Cursor managen soll. D.h. wenn die Activity angehalten wird, gibt sie automatisch den Cursor frei. Wird die Activity fortgesetzt, führt sie automatisch ein Requery auf dem Cursor aus.

Cursor cursor = ballDbAdapter.findAllBalls();
      startManagingCursor(cursor);

Anschließend erzeuge ich einen SimpleCursorAdapter, dem ich die ListItem-Layout Definition sowie den Cursor übergeben. Außerdem muss ich noch das Mapping der Spalten im Cursor auf die View-Elemente im ListItem definieren. Dafür erzeuge ich ein String-Array mit den zu mappenden Cursor-Spalten sowie ein int-Array mit den IDs der View-Elemente. Dazu muss ich zuvor in der Layout-Definition meines Balllist-Items dem TextView Element eine ID zuweisen.

String[] from = new String[] { 
            BallDbAdapter.KEY_BALL_FULL_NAME };
    int[] to = new int[] { R.id.balllist_item_fullname };

    SimpleCursorAdapter baelle = new SimpleCursorAdapter(this, 
        R.layout.balllist_item, cursor, from, to);

Den erzeugten Adapter gebe ich zurück, so dass dieser als ListAdapter der ListActivity übergeben wird.

Das war's. Meine BallApp basiert damit auf einer internen SQLite Datenbank.

Der vollständige Source Code kann unter https://github.com/bobbyout/ballapp-android abgerufen werden.

Freitag, 14. Januar 2011

Android BallApp - Iteration 1 - Einfügen einer ListView

Seit meinem letzten Beitrag „Einrichten einer Android Entwicklungsumgebung und Aufsetzen eines Projektes“ ist eine ganze Weile vergangen. Eigentlich hatte ich vor die darin generierte Projektstruktur etwas zu beschreiben, aber irgendwie macht es mehr Spaß weiter zu entwickeln J. Vielleicht hole ich das später noch mal nach. Heute will ich aber endlich mal richtig loslegen.

Das Ziel für die zweite Iteration:

Anzeige einer Ball-Liste

Im ersten Schritt möchte ich die zuvor angelegte Anwendung um eine einfache Liste mit ein paar Ball-Namen erweitern. Die Ball-Namen werde ich zunächst fix in der Anwendung hinterlegen. Eine dynamische Liste aus einer Datenbank werde ich erst in einer späteren Iteration in Angriff nehmen.

Ich arbeite testgetrieben, daher ist das erste, was ich erstelle ein Test in meinem Test-Projekt. Im Package de.mandry.android.ballapp.test erstelle ich eine neue Klasse. Als Namen der Klasse wähle ich BallListTest. Die Klasse wird abgeleitet vom android.test.ActivityInstrumentationTestCase2 parametrisiert mit meiner Activity BallAppActivity.

Die erzeugte Test-Klasse weist zunächst noch Kompilierungsfehler auf. Die BallAppActivity muss importiert werden. Außerdem muss ein Default-Konstruktor definiert werden, welcher den Konstruktor der Super-Klasse aufruft und diesem das Package „de.mandry.android.ballapp“ sowie die Klasse der zu testenden Activity BallAppActivity.class übergibt.

Die kompilierbare Test-Klasse sieht also wie folgt aus:


package de.mandry.android.ballapp.test;

import android.test.ActivityInstrumentationTestCase2;
import de.mandry.android.ballapp.BallAppActivity;

/**
 * Testet die Ball-Liste des BallApp.
 * @author Torsten Mandry
 */
public class BallListTest extends 
       ActivityInstrumentationTestCase2< BallAppActivity > {

  /**
   * Default Konstruktor.
   */
  public BallListTest() {
    super( "de.mandry.android.ballapp", BallAppActivity.class );
  }

}


Als nächstes ergänze ich eine Setup-Methode, die mir die zu testende BallAppActivity in einer Klassen-Variable ablegt. Dazu rufe ich die getActivity() Methode der Super-Klasse ActivityInstrumentationTestCase2 auf. Über den Konstruktor-Aufruf habe ich der Klasse zuvor mitgeteilt, welchen Activity-Typ ich in diesem Fall zurückerwarte.

  /** Die BallApp Activity unter Test. */
  private BallAppActivity ballAppActivity;


  /**
   * Setup-Methode, holt die Activity.
   */
  @Override
  protected void setUp() throws Exception {
    super.setUp();
    ballAppActivity = this.getActivity();
  }


Da ich die Zeit nicht abwarten kann versuche ich schon mal die Test-Klasse auszuführen (Run As - Android JUnit Test). Falls noch nicht erfolgt startet Eclipse jetzt das bereits im vorangegangenen Beitrag eingerichtete Virtual Device und installiert die BallApp darin. Anschließend versucht es die Tests auszuführen und bricht mit einer Fehlermeldung ab:

Test run failed: Test run incomplete. Expected 1 tests, received 0

OK, ich brauche mindestens einen Test, das macht Sinn! Ich ergänze also einen minimalen Test, der sicherstellt dass die Activity vorhanden ist. Es ist zu beachten, dass das Android JUnit keine Annotationen kennt, wie sie im Java JUnit mittlerweile üblich sind. Das heißt, dass die Test-Methoden einer TestCase-Klasse wie ehemals in Java durch den Präfix „test“ im Methodennamen gekennzeichnet werden.

  /**
   * Initialer Test, stellt sicher dass die BallAppActivity
   * gesetzt ist.
   */
  public void testActivityExists() {
    assertNotNull( ballAppActivity );
  }


Ich starte den Test noch einmal. Die Tests laufen erfolgreich durch und ich erhalte den begehrten grünen Balken. Super!

Dann kann ich jetzt ja endlich anfangen mein erstes Feature zu entwickeln.

Nach dem TDD-Ansatz schreibe zunächst einen weiteren Test. Ich will sicherstellen, dass meine BallApp eine Liste anzeigt. Dazu prüfe ich zunächst, ob meine Activity eine View mit einer entsprechenden Id enthält. Die Id erwarte ich in der generierten Klasse R im AndroidBallApp-Projekt. Die Id ist aktuell noch nicht vorhanden, daher wird mir diese als Kompilierfehler angemahnt.


  /**
   * Stellt sicher, dass eine View BallList existiert.
   */
  public void testViewBallListExists() {
    assertNotNull( ballAppActivity.findViewById( de.mandry.
                       android.ballapp.R.id.ballList ) );
  }


Die Klasse R enthält Konstanten für alle innerhalb eines Projektes verwendeten IDs, Strings, Layouts und Grafikelemente. Sie wird vom Eclipse ADT-Plugin automatisch ergänzt sobald neue Elemente hinzukommen oder wegfallen. So wird z.B. automatisch eine neue ID-Konstante hinzugenommen, sobald ich in einem Layout eine neue View mit einer neuen ID ergänze und dieses speichere. Dieses Vorgehen finde ich ziemlich gut, da ich so gar nicht erst in die Versuchung komme, in meinen Klassen der Einfachheit halber „erst mal“ die Id als String einzutragen.

Die einfachste Möglichkeit den oben geschriebenen Test „grün“ zu bekommen ist, im main-Layout das vorhandene TextView-Element mit einer entsprechenden Id zu versehen.

<TextView android:id="@+id/ballList"
    android:layout_width="fill_parent"
    android:layout_height="wrap_content"
    android:text="@string/hello"
    />


Nach dem Speichern des Layouts verschwindet der Kompilierfehler im Test. Der Test kann erfolgreich ausgeführt werden und ist grün. Der Test ist aber noch nicht vollständig. Ich habe zwar ein View-Objekt mit der entsprechenden Id, es handelt sich aber nicht um eine Liste. Daher ergänze ich den Test um eine Prüfung auf den View-Typ.

  /**
   * Stellt sicher, dass eine View BallList existiert.
   */
  public void testViewBallListExists() {
    View ballListView = ballAppActivity.findViewById( de.mandry.
                            Android.ballapp.R.id.ballList );
    assertNotNull( ballListView );
    assertEquals( ListView.class, ballListView.getClass() );
  }

Damit schlägt der Test wieder fehl und ich habe einen Grund weiter zu machen.

Ich will das TextView-Element im main-Layout durch ein ListView-Element ersetzen. Nähere Einzelheiten zur ListView entnehme ich dem ListView Abschnitt im Hello Views Tutorial der Android Developers Seite.


Das erste was ich lerne ist: So einfach ist das nicht!

ListViews werden anscheinend nicht innerhalb des XML Layouts definiert, sondern programmatisch in der Activity. Das im Tutorial behandelte Beispiel passt ziemlich gut zu meinem heutigen Ziel, also gehe ich frohen Mutes an die Programmierung.

Als erstes lösche ich das TextView-Element aus meinem main-Layout. Das scheine ich ja nicht zu brauchen. Es erscheint der bekannte Kompilierfehler wieder in meinem Test, da ich damit auch gleichzeitig die ID-Konstante aus der R-Klasse entfernt habe. Ich ignoriere das erst mal und mache weiter.

Wie im ListView-Tutorial beschrieben lege ich zuerst eine neue Layout-Definitionsdatei balllist_item.xml an. Für den Anfang übernehme ich die Definition aus dem Tutorial. Dann leite ich meine BallAppActivity von der Superklasse ListActivity ab. Dabei fällt mir der ungeschickte Name meiner Klasse auf, den lasse ich aber zunächst so. Ein entsprechendes Refactoring führe ich später durch, wenn mein Test grün ist.

Den Aufruf setContentView( R.layout.main ) im Konstruktor meiner ListActivity entferne ich. Die ListActivity setze ich laut Tutorial über die Methode setListAdapter(…). Den in dieser Methode zu übergebenen ArrayAdapter erzeuge ich in einer eigenen Methode getBallListAdapter().

Meine Activity-Klasse sieht damit wie folgt aus:

public class BallAppActivity extends ListActivity {
 
  /** Called when the activity is first created. */
  @Override
  public void onCreate( Bundle savedInstanceState ) {
    super.onCreate( savedInstanceState );
    setListAdapter( getBallListAdapter() );
  }

  /**
   * Gibt den BallListAdapter zurück.
   * @return der BallListAdapter.
   */
  private ListAdapter getBallListAdapter() {
    String[] baelle = new String[] {
      "3D type 543 ml",
      "mg Maier Classic 1",
      "Nifo 2"
    };
    return new ArrayAdapter< String >( this,
                  R.layout.balllist_item, baelle );
  }
}


Ich führe das AndroidBallApp-Projekt manuell als Android Application auf dem Virtual Device aus und sehe mir das Ergebnis an. Sieht gut aus!



Mein Test läuft allerdings noch nicht, der Kompilierfehler ist immer noch da (irgendwie verliere ich beim TDD immer wieder die Orientierung L ). Da ich die ListView nun mit der ListActivity schon mitbekomme, habe ich keinen Einfluss auf deren ID. Ich muss mir die ListView auf einem anderen Weg herholen. Ich schreibe meinen Test also wie folgt um:

  /**
   * Stellt sicher, dass eine View BallList existiert.
   */
  public void testViewBallListExists() {
    View ballListView = ballAppActivity.getListView();
    assertNotNull( ballListView );
    assertEquals( ListView.class, ballListView.getClass() );
  }


Der Kompilierfehler ist wieder weg und mein Test ist grün. Fertig!

Nein, da war doch noch was…?! Richtig, ich wollte die Activity noch umbenennen. Also werde ich mal die Eclipse Refactoring Tools zusammen mit Android ausprobieren. Ich wähle den Klassennamen der BallAppActivity aus und wähle über‘s Kontext-Menü „Refactor“ -> „Rename“. Ich gebe den neuen Namen BallListActivity ein und bestätige ihn mit Return. Auf den ersten Blick sieht alles gut aus. Nach dem ausführen der Tests weiß ich es jedoch besser, beide Tests schlagen fehl. Bei genauerem Hinsehen entdecke ich in den Consolen-Ausgaben eine Fehlermeldung:

Error in an XML file: aborting build.

Welche XML-Datei könnte damit gemeint sein? Die Layout-Definitionen sehr wahrscheinlich nicht. Bleibt eigentlich nur noch die AndroidManifest.xml. Bingo! Hier wird nach wie vor auf die BallAppActivity verwiesen, die gibt es nicht mehr. Nachdem dieser Fehler korrigiert ist laufen auch die Tests wieder.

<activity android:name=".BallListActivity"
          android:label="@string/app_name">


Sieht so aus, als wenn ich mein Tagesziel erreicht hätte. Hier noch mal die vollständige BallListActivity und der zugehörige Test.

package de.mandry.android.ballapp;

import android.app.ListActivity;
import android.os.Bundle;
import android.widget.ArrayAdapter;
import android.widget.ListAdapter;

public class BallListActivity extends ListActivity {

  /** Called when the activity is first created. */
  @Override
  public void onCreate( Bundle savedInstanceState ) {
    super.onCreate( savedInstanceState );
    setListAdapter( getBallListAdapter() );
  }

  /**
   * Gibt den BallListAdapter zurück.
   * @return der BallListAdapter.
   */
  private ListAdapter getBallListAdapter() {
    String[] baelle = new String[] {
      "3D type 543 ml",
      "mg Maier Classic 1",
      "Nifo 2"
    };
    return new ArrayAdapter< String >( this,
                   R.layout.balllist_item, baelle );
  }
}



package de.mandry.android.ballapp.test;

import android.test.ActivityInstrumentationTestCase2;
import android.view.View;
import android.widget.ListView;
import de.mandry.android.ballapp.BallListActivity;

/**
 * Testet die Ball-Liste der BallApp.
 * @author Torsten Mandry
 */
public class BallListTest extends
    ActivityInstrumentationTestCase2< BallListActivity > {

  /** Die BallListActivity unter Test. */
  private BallListActivity ballListActivity;

  /**
   * Default Konstruktor.
   */
  public BallListTest() {
    super( "de.mandry.android.ballapp", 
           BallListActivity.class );
  }

  /**
   * Setup-Methode, holt die Activity.
   */
  @Override
  protected void setUp() throws Exception {
    super.setUp();
    ballListActivity = this.getActivity();
  }

  /**
   * Initialer Test, stellt sicher dass die BallAppActivity 
   * gesetzt ist.
   */
  public void testActivityExists() {
    assertNotNull( ballListActivity );
  }

  /**
   * Stellt sicher, dass eine View BallList existiert.
   */
  public void testViewBallListExists() {
    View ballListView = ballListActivity.getListView();
    assertNotNull( ballListView );
    assertEquals( ListView.class, ballListView.getClass() );
  }
}