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() );
  }
}


Mittwoch, 22. Dezember 2010

Java Generics - wie (typ)sicher kann ich mir sein

Ich habe heute mit einem Kollegen vor einem etwas kniffligen Problem gesessen. Beim Versuch in einer JSF-Seite auf ein Property eines Entities zuzugreifen bekamen wir einen Fehler, da dort anstelle des erwarteten Entities ein BigDecimal-Wert ankam. Es war klar, dass es sich dabei um einen dummen Fehler in unserem Code handeln musste, die Frage war nur, wo der zu suchen war. Auf den ersten und auch auf den zweiten Blick sah alles OK aus. Das Entity kam aus einer java.util.List die über Java Generics typsicher definiert war. Erst nachdem wir an einer passenden Stelle einen Breakpoint eingefügt haben, kamen wir so langsam dahinter was da schief lief.

Um die Sache einfacher erklären zu können hier ein stark vereinfachtes Beispiel:


import java.util.List;

public class MyClass {
  DataProvider dataProvider;

  public MyClass() {
    dataProvider = new DataProviderImpl();
  }

  @SuppressWarnings( "unchecked" )
  public void loadData() {
    List<MyEntity> data = (List<MyEntity>) dataProvider.getData();
    // Do something with the data...
  }
}


Sieht auf den ersten Blick alles korrekt aus, oder? Das haben wir auch gedacht, aber irgendwo musste ja der Fehler stecken.


Quiz-Frage: Welchen Typ haben die Elemente in der List<MyEntity> data?


Man sollte annehmen, dass darin nur Objekte vom Typ MyEntity enthalten sein können, oder? Stimmt aber nicht! In unserem Fall waren darin zur Laufzeit BigDecimal-Werte, weil in unserem DataProvider die Methode fehlerhaft programmiert war. Die eigentliche Fehlerursache war letztendlich ziemlich unspektakulär (wir hatten vergessen bei einer Hibernate SQL-Query den entsprechenden Entity-Typ anzugeben). Viel spannender war die Frage warum wir keine ClassCastException bekommen hatten, die uns wahrscheinlich ziemlich schnell auf die Ursache aufmerksam gemacht hätte.


Um das zu verstehen muss man wissen, dass Java Generics lediglich vom Java Compiler - also zur Kompilierzeit - interpretiert werden. Zur Kompilierzeit kann der Compiler nicht wissen, ob die getData() Methode zur Laufzeit eine Liste mit MyEntity- oder anderen Objekten zurückgibt, da die Methoden-Signatur nur eine einfache List (ohne Generics-Definition) als Rückgabe-Typ definiert. Das einzige was der Compiler tun kann ist uns zu warnen, dass er eine typunsichere Zuweisung gefunden hat. Das hat er in unserem Beispiel sogar getan, aber wir haben mit der @SuppressWarnings Annotation explizit gesagt "Kein Problem, wir wissen was wir tun" (eine fatale Selbstüberschätzung).

Beim Kompilieren - nachdem mögliche Typ-Fehler und/oder -Warnungen ausgewertet wurden - werden die Generics-Angaben aus dem Quell-Code entfernt. Das was nach dem Kompilieren - im Java Byte-Code - von der Zuweisung bestehen bleibt ist das Folgende:

List data = (List) dataProvider.getData();

Also eine Cast-Anweisung die zur Laufzeit keine Auswirkung mehr hat. Daher wird hier auch keine ClassCastException ausgelöst, egal welchen Typ von Elementen die getData() Methode zurückgibt.

Um den gewünschten Cast-Effekt zu erhalten müssen die einzelnen Elemente der Liste gecastet werden.

List data = new ArrayList();
for (Object o : dataProvider.getData()) {
  data.add((MyEntity) o);
}


Dann kann auch die @SuppressWarnings Annotation entfernt werden, und zur Laufzeit fliegt die erwartete ClassCastException, sobald dort was anderes ankommt als erwartet.

Fazit: Die @SuppressWarnings Annotation ist schnell eingefügt (in Eclipse gibt's dafür sogar extra einen Quick Fix). Trotzdem macht es manchmal Sinn den oben beschriebenen "Umweg" in Kauf zu nehmen um wirklich sicher zu sein, dass unerwartete Elemente nicht einfach weiterverarbeitet werden.

In diesem Sinne, frohe Weihnachten...

Freitag, 8. Oktober 2010

Einrichten einer Android Entwicklungsumgebung und Aufsetzen eines Projektes

In diesem und nachfolgenden Beiträgen beschreibe ich die schrittweise Entwicklung einer Android Applikation. Jeder Beitrag wird sich mit einem Aspekt der Android Programmierung beschäftigen. In diesem ersten Beitrag beschreibe ich die Einrichtung der Entwicklungsumgebung sowie das Aufsetzen eines neuen Android Projektes in Eclipse.

Die im Laufe dieses Projektes entwickelte Applikation wird eine Minigolf Ball-Datenbank, so dass ich die Einarbeitung in die Android-Entwicklung mit meinem zweiten großen Hobby, dem professionellen Minigolf-Sport kombinieren kann. 

Um mir erreichbare Ziel zu stecken, werde ich die Entwicklung in kleinschrittige Iterationen unterteilen. Dabei werde ich mir für jede Iteration jeweils ein bewusst kleines Feature auswählen und die entsprechende Umsetzung an dieser Stelle beschreiben. 

Mein Ziel für die erste Iteration:

Einrichtung der Entwicklungsumgebung und Anlage eines Android Projektes

Die Einrichtung der Entwicklungsumgebung, die Installation des Android SDK Starter Packages und des ADT Eclipse-Plugins sowie die Installation der eigentlichen Android Plattformen, ist auf der Android Developer Seite im SDK Installation Guide recht gut beschrieben.

Voraussetzung für die Installation des SDK ist ein Java JDK in der Version 5 oder 6. Weiterhin wird in der Android Anleitung eine bereits vorhandene Eclipse Entwicklungsumgebung in der Version 3.4 oder 3.5 vorausgesetzt. Ich verwende die SpringSource Tool Suite in der Version 2.3.2, die auf Eclipse 3.5 basiert, zusammen mit einem Java 6 JDK.

Die Installation des SDK Starter Packages beschränkt sich auf Download und Entpacken eines ZIP-Archivs. Der Pfad des entpackten SDK Starter Packages wird zur PATH-Umgebungsvariablen hinzugefügt. Das Starter Package enthält lediglich die SDK Tools. Die eigentlichen Android Plattformen, in verschiedenen Versionen, werden im übernächsten Abschnitt installiert.

Nach der Installation des Starter Packages wird zunächst das Android Developer Tools (ADT) Plugin für Eclipse installiert. Im SDK Installation Guide ist eine Eclipse Update Site angegeben, über die das Plugin ohne Probleme installiert werden kann. Nach dem obligatorischen Eclipse-Neustart muss dann in den Preferences im Bereich Android lediglich noch das zuvor entpackten Android SDK Verzeichnis konfiguriert werden.

Schließlich müssen die eigentlichen Android Plattformen - die verschiedenen Android Versionen, die unterstützt werden sollen - heruntergeladen und installiert werden. Die Installation kann direkt aus Eclipse über den Android SDK und AVD Manager (Menü Window) erfolgen. Zur Entwicklung von Android Applikationen muss mindestens eine Android Plattform installiert werden. Im Bereich "Available Packages" des Managers können die gewünschten Komponenten ausgewählt und installiert werden. Ich lade i.d.R. alle verfügbaren Komponenten herunter. Für das hier beschriebene Projekt sollte jedoch die Android Plattform 2.1 Update 1 ausreichen.

Anlage eines neuen Eclipse Projektes

Nachdem Eclipse gestartet, und ggf. ein neuer Workspace angelegt wurde, muss zuerst ein neues Android Projekt angelegt werden. Dazu stellt das ADT Plugin einen Android Project Wizard zur Verfügung der über den üblichen Weg "File -> New -> Project... und dann Android -> Android Project" gestartet wird. Der Wizard fragt die relevanten Parameter des Projektes ab.

Der Projekt Name ist der Name des Eclipse Projektes, der z.B. in der Navigator View angezeigt wird.

Wie bei allen Eclipse Projekten kann das Projektverzeichnis entweder innerhalb des Workspaces (default location) oder an anderer Stelle erzeugt werden. Ich halte meine Sourcen und meinen Workspace lieber in getrennten Verzeichnissen.

Das Build Target definiert die Android Plattform Version für welche die Applikation kompiliert wird.

Der Application Name ist der Name der Applikation, wie er später auf einem Android Gerät im Menü oder im Fenster-Titel angezeigt wird.

Der Package Name bestimmt das Basis Java-Package der Applikation.

Die Minimum SDK Version bestimmt die kleinste Android Plattform Version, auf der die Applikation laufen soll. Die Version wird in Form einer API Level Kennzahl angegeben, die z.B. aus der Liste der Build Targets abgelesen werden kann.

Mit der Option Create Activity kann mit dem Projekt gleichzeitig eine Activity für die Applikation angelegt werden. Was eine Activity genau ist, werde ich im nächsten Beitrag beschreiben. Für den Moment genügt es, wenn wir die Activity als die eigentliche Anwendung verstehen, die beim Aufruf der Applikation gestartet wird.

Für mein Android Projekt wähle ich die folgenden Einstellungen:

Projekt Name: AndroidBallApp
Build Target: Android Plattform 2.1 Update 1
Application Name: BallApp
Package Name: de.mandry.android.ballapp
Create Activity: BallAppActivity
Min SDK Version: 7

Im nächster Schritt fragt der Android Project Wizard, ob auch ein Android Test Projekt angelegt werden soll. In der Android Welt ist der Test zu einer Applikation ebenfalls eine Applikation, die in einem eigenen Projekt, einem Test Projekt, abgelegt wird. Ein Test-Projekt bezieht sich immer auf genau ein Applikations-Projekt. Wird die "Create a Test Project" Checkbox aktiviert, werden die Properties für das Test Projekt automatisch von denen des eigentlichen Anwendungsprojektes abgeleitet und ein leeres Test-Projekt wird erstellt.

Nach dem Klick auf "Finish" werden die beiden Eclipse-Projekte angelegt. Durch die Option "Create Activity" enthält das Projekt "AndroidBallApp" bereits eine minimale aber lauffähige Android Anwendung, die aus Eclipse heraus direkt gestartet werden kann (über das Kontext-Menü des Eclipse Projektes "Run as --> Android Application".

Beim ersten Ausführen einer Android Applikation meldet Eclipse, dass noch kein Target Device vorhanden ist, auf dem die Anwendung ausgeführt werden kann. Es wird angeboten nun ein Android Virtual Device (AVD) zu erstellen. Dazu wird wieder der bereits bekannte SDK und AVD Manager aufgerufen. Der New-Button im Bereich "Virtual Devices" öffnen einen Dialog in dem ein Name sowie eine Reihe von Einstellungen für das virtuelle Gerät definiert werden können. Für den Anfang genügt es neben einem beliebigen Namen die gewünschte Android Plattform (Target) auszuwählen und das virtual Device ansonsten mit den Default Einstellungen anzulegen.

Nach dem Schließen des SDK und AVD Managers muss im Android Device Chooser die Liste der Virtual Devices aktualisiert werden. Danach kann das neu angelegte virtuelle Gerät ausgewählt und die Anwendung darauf deployed und gestartet werden.

Eclipse startet das virtuelle Android Gerät in einem separaten Fenster. Das starten des Devices dauert immer einige Zeit, daher ist es empfehlenswert, das einmal gestartete Device Fenster offen zu lassen. Beim nächsten Ausführen der Applikation aus Eclipse heraus wird dieses dann weiter verwendet und die Anwendung wird lediglich neu deployed und gestartet. Das dauert dann nur wenige Sekunden.



Die gestartete Anwendung ist erwartungsgemäß nicht besonders spannend. Neben dem Anwendungsnamen BallApp, den ich bei Anlage des Android Projektes festgelegt habe, wird lediglich ein Label mit dem Text "Hello World, BallAppActivity!" angezeigt. Das werden wir in einer der nächsten Iterationen ändern.

Donnerstag, 22. Juli 2010

Unit-Test von E-Mail Funktionalität mit Mock JavaMail

Automatisiertes Testen von Java-Code ist meist nicht so einfach. Selbst wenn die erste Hürde genommen ist und man weiß was man testen will, bleibt man oft recht schnell an der Frage hängen, wie das zu tun ist. Richtig schwierig wird es, wenn die zu testende Funktionalität von externen Diensten abhängt. Der Versand von E-Mails fällt in diese Kategorie.

Vor kurzem stand ich im Rahmen eines Projektes vor dem Problem den Versand von E-Mails zu testen. Nach kurzer Recherche bin ich auf das Mock JavaMail Projekt (https://mock-javamail.dev.java.net/) gestoßen.

Die Verwendung von Mock JavaMail ist so einfach wie genial. Es muss lediglich das JAR-Archiv in den Classpath des JUnit-Tests aufgenommen werden und schon ersetzt die Mock-Implementierung den echten Mail-Server. Eine Anpassung der JavaMail-Konfiguration ist nicht notwendig. Im einfachsten Fall kann der fest verdrahtete Mail-Server in der Java-Klasse stehen.

Auf die gesendete E-Mail zugreifen kann man über eine Mailbox-Klasse, über welche zu einer E-Mail-Adresse eine Liste der empfangenen Nachrichten abgerufen werden kann.



  @Test
  public void testSendMail() {
    String subject = "Test E-Mail";
    String message = "Dies ist eine Test E-Mail";
    String recipientEmail = "test@mail.de";
    String senderEmail = "noreply@mail.de";

    mailSender.sendMail(subject, message, recipientEmail, senderEmail);

    try {
      List inbox = Mailbox.get(recipientEmail);

      assertEquals("Mailbox enthält falsche Anzahl E-Mails", 1, inbox.size());

      Message mail = inbox.get(0);

      assertEquals("Empfangene E-Mail hat falschen Betreff", subject,
          mail.getSubject());
      assertTrue("Empfangene E-Mail hat falschen Absender", mail.getFrom()[0]
          .toString().contains(senderEmail));

    } catch (AddressException exc) {
      fail(exc.toString());
    } catch (MessagingException exc) {
      fail(exc.toString());
    }
  }


Neben dem Abfangen und Testen von gesendeten E-Mails kann mit Mock JavaMail auch eine Mailbox simuliert (gemockt) werden, um das Abrufen von E-Mails (z.B. über POP3) zu testen.

Schließlich kann das Framework dazu verwendet werden Fehler beim Senden von E-Mails zu provozieren um so das Fehler-Handling der eigenen Applikation zu testen.


  @Test
  public void testSendMailError() {
    String subject = "Test E-Mail";
    String message = "Dies ist eine Test E-Mail";
    String recipientEmail = "test@mail.de";
    String senderEmail = "noreply@mail.de";

    try {
      Mailbox.get(recipientEmail).setError(true);

      try {
        mailSender.sendMail(subject, message, recipientEmail, senderEmail);
        fail("Fehler beim E-Mail-Versand löst keine RuntimeException aus");
      } catch (RuntimeException exc) {
        // Success
      }

    } catch (AddressException exc) {
      fail(exc.toString());
    }
  }


Ein sehr hilfreiches Test-Framework das man kennen sollte.