zurück zum Artikel

(Echte) Unit Tests für Android

Stefan Nägele
(Echte) Unit Tests für Android

Unit-Tests sollen die kleinsten Softwarebausteine überprüfen. Sie sind schnell angewandt und liefern ein direktes Feedback. Robolectric hilft bei der Umsetzung unter Android.

Das Android SDK enthält die Testing Support Library, die aber bei der Umsetzung echter Unit-Tests nicht hilft, weil das Ausführen von Testläufen ist nicht ohne weiteres möglich ist. Zwar können Entwickler Java-Code ohne Abhängigkeiten zum Android-SDK mit JUnit innerhalb der Java Virtual Machine (JVM) testen, aber sie müssen Android-spezifische Tests immer auf einem Emulator oder einem Gerät durchführen.

Der TDD-Ansatz (Test-Driven Development) sieht folgendermaßen aus:

Da die Tests auf einem Gerät oder Emulator erfolgen müssen, wobei schon die Datenübertragung langwierig ist, ist das Vorgehen zeitintensiv. Nutzer verbringen bei einem kleinen Funktionstest lange Zeit damit auf die Testausführung zu warten, statt ihren eigentlichen Job erledigen zu können.

Gut für Integrationstests ist die Testing Support Library geeignet, um beispielsweise langlaufende Operationen wie REST-Aufrufe zu verifizieren. Beim schnellen Test einzelner Funktionen stößt die Bibliothek jedoch an ihre Grenzen.

An dem Punkt kommt Robolectric ins Spiel, eine Open-Source-Bibliothek, die Android-Code innerhalb der JVM ohne Android-Umgebung testen kann. Abhängigkeiten erfolgen als "Mock"-Aufrufe: Das Tool ersetzt die Android-Laufzeitumgebung durch sinnvolle Funktionsaufrufe und Rückgabewerte gegen die Android-API.

Bei Robolectric heißen die Aufrufe Shadows. Sie implementieren die Android-Funktionen bestmöglich in normalem Java. Die Dokumentation zu Robolectric ist jedoch recht spartanisch. Daher erläutert dieser Artikel die Anwendung des Tools innerhalb einer App unter Nutzung von Android Studio.

Gradle hat sich als das Build-Management-Tool für Android etabliert. Dementsprechend müssen Entwickler darin die ersten Weichen stellen, um Robolectric einbinden zu können. Dazu fügen sie in der App die Robolectric-Abhängigkeit in das Modul Gradle File (nicht Gradle Settings File) ein:

dependencies {
testCompile 'org.robolectric:robolectric:3.0'
}
Die Dependency-Sektion von build.gradle unterhalb des eigentlichen App-Moduls benötigt die Robolectric-Abhängigkeit(Abb. 1).

Die Dependency-Sektion von build.gradle unterhalb des eigentlichen App-Moduls benötigt die Robolectric-Abhängigkeit(Abb. 1).

Die Robolectric-Bibliothek sollte die Schlüsselwörter testCompile und nicht androidTestCompile erhalten. Ansonsten versucht Gradle (und damit Android Studio), sämtliche Robolectric-Tests auf einem Emulator oder Gerät auszuführen, was fehlschlagen würde.

Seit Version 2.0 legt Android Studio die Projektstruktur für Tests beim Erstellen eines neuen Projekts erfreulicherweise automatisch an und hebt sie farblich hervor. In bereits existierenden Apps müssen Entwickler gegebenenfalls folgende Ordner separat anlegen:

Anlegen eines JUnit-4-Tests aus einer Klasse heraus (Abb. 3)

Anlegen eines JUnit-4-Tests aus einer Klasse heraus (Abb. 3)

Nach der erfolgreichen Einbindung der Robolectric-Bibliothek per Gradle und der Konfiguration von Android Studio lässt sich der erste Test anlegen. Dazu öffnen Entwickler eine beliebige Klasse in Android Studio und verwenden standardmäßig das Tastaturkürzel Strg + Shift + T .

Als Testbibliothek kommt JUnit 4 zum Einsatz. Der Testordner sollte den Namen ./src/test haben. Durch die Verwendung des oben genannten Tastenkürzels sollte Android Studio automatisch eine entsprechende JUnit-Run-Konfiguration angelegt haben, wie Abbildung 4 zeigt.

Vollständige JUnit-Robolectric-Run-Konfiguration (Abb. 4)

Vollständige JUnit-Robolectric-Run-Konfiguration (Abb. 4)

Neben dem angegebenen Testklassennamen mit entsprechendem Paketnamen sollte ein besonderes Augenmerk auf folgenden Parametern liegen:

Eine Testklasse sollte mit Robolectric folgendermaßen aussehen:

package com.example.sn.myapplication;
import android.app.Activity;

import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.robolectric.Robolectric;
import org.robolectric.RobolectricGradleTestRunner;
import org.robolectric.annotation.Config;

import static org.junit.Assert.assertEquals;

@RunWith(RobolectricGradleTestRunner.class)
@Config(constants = BuildConfig.class, sdk = 21)
public class MainActivityTest {

private Activity cut;

@Before
public void setUp() throws Exception {
cut = Robolectric.setupActivity(MainActivity.class);
}

@Test
public void assertActivityTitle() throws Exception {
String expectedTitle = cut.getString(R.string.app_name);
assertEquals("Activity title did not match",
expectedTitle,
cut.getTitle().toString());
}
}

Der Befehl @RunWith(RobolectricGradleTestRunner) legt den Runner fest. Sollte Maven statt Gradle zum Einsatz kommen, heißt die passende Klasse RobolectricTestRunner. Alternativ lässt sich eine eigene Testklasse von der Basisklasse Runner ableiten.

Im Befehl @Config(constants, sdk) ist der Parameter constants erforderlich, um die von Android generierte BuildConfig.class anzugeben, damit der Test auf Ressourcen wie String-IDs aus den Assets der App zugreifen kann.

Wer eine Klasse auf SDK-Kompatibilität überprüft, kann das SDK-Level manuell setzen. Robolectric unterstützt mit Version 3.1 Android 6 (Marshmallow). Eine Gradle-Run-Konfiguration ermöglicht den Start aller Tests auf einen Schlag.

Die angezeigten Skriptparameter in der Gradle-Run-Konfiguration legen fest, dass bereits ausgeführter, unveränderter Code erneut getestet wird (Abb. 5).

Die angezeigten Skriptparameter in der Gradle-Run-Konfiguration legen fest, dass bereits ausgeführter, unveränderter Code erneut getestet wird (Abb. 5).

Der Befehl zum manuellen Start von Tests sieht folgendermaßen aus:

$ gradlew cleanTest test

Flavors ermöglichen die Implementierung und Verwaltung mehrerer Versionen einer App in Android Studio – beispielsweise für verschiedene Zielgeräte oder Releases.

Folgender Code zeigt die Implementierung anhand einer freien und einer kostenpflichtigen Version einer App:

android {
compileSdkVersion 23
buildToolsVersion "23.0.1"

defaultConfig {
applicationId "com.example.sn.myapplication"
minSdkVersion 15
targetSdkVersion 23
versionCode 1
versionName "1.0"
}
productFlavors {
pro {
applicationId = "com.example.sn.myapplication.pro"
}
free {
applicationId = "com.example.sn.myapplication.free"
}
}
}

Beim Einsatz von Flavors schlägt die Verwendung von Robolectric ohne Anpassungen mit einem Stacktrace folgender Art fehl:

android.content.res.Resources$NotFoundException: \
no such label \
com.example.sn.myapplication.free:string/app_name

at org.robolectric.util.ActivityController.\
getActivityTitle(ActivityController.java:104)
at org.robolectric.util.ActivityController.\
attach(ActivityController.java:49)
at org.robolectric.util.ActivityController$1.\
run(ActivityController.java:121)
at org.robolectric.shadows.ShadowLooper.\
runPaused(ShadowLooper.java:304)
at org.robolectric.shadows.CoreShadowsAdapter$2.\
runPaused(CoreShadowsAdapter.java:45)
at org.robolectric.util.ActivityController.\
create(ActivityController.java:118)
at org.robolectric.util.ActivityController.\
create(ActivityController.java:129)
at org.robolectric.util.ActivityController.\
setup(ActivityController.java:210)
at org.robolectric.Robolectric.\
setupActivity(Robolectric.java:46)Solution

Grund dafür ist, dass die korrekte Auflösung der Application ID des Projekts nicht möglich ist, weil es für jeden Flavor eine separate ID gibt. Damit Robolectric die App ID weiterhin auflösen kann, lässt sich seit Robolectric 3.0 ein packageName innerhalb der @Config() Annotation angeben:

package com.example.sn.myapplication;
import android.app.Activity;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.robolectric.Robolectric;
import org.robolectric.RobolectricGradleTestRunner;
import org.robolectric.annotation.Config;

@RunWith(RobolectricGradleTestRunner.class)
@Config(constants = BuildConfig.class, sdk = 21,
packageName ="com.example.sn.myapplication")
public class MainActivityTest {
private Activity cut;
@Before
public void setUp() throws Exception {
cut = Robolectric.setupActivity(MainActivity.class);
}
@Test
public void assertActivityTitle() throws Exception {...}
}

Jede Robolectric-Klasse muss packageName enthalten, unabhängig davon ob die Testklasse sich auf einen Flavor bezieht oder nicht.

In Android Studio 2.0 können Entwickler über die Build Variants entscheiden, welchen Flavor die IDE für den Build und das Deployment auf einem Emulator oder Gerät berücksichtigen soll.

Struktur der Quellcode-Verzeichnisse bei der Verwendung von Flavors (Abb. 6)

Struktur der Quellcode-Verzeichnisse bei der Verwendung von Flavors (Abb. 6)

Die Ordner mit dem Testcode müssen den Namen des jeweiligen Flavors beinhalten:

Struktur der Testquellverzeichnisse bei der Verwendung von Flavors (Abb. 7)

Struktur der Testquellverzeichnisse bei der Verwendung von Flavors (Abb. 7)

Durch die Build Variants interpretiert Android Studio neben dem Produktivcode auch die Quellen für die Tests und hebt sie farblich hervor.

Mit der bisherigen Konfiguration lassen sich nur die Testklassen aus den Ordnern ./src/test, ./src/testFree und ./src/androidTest ausführen, also der gemeinsame Code aus den Haupttests und die Sourcen des jeweiligen Flavors.

Bei Flavors dürfen gleichnamige Klassen nur auftreten, wenn sie ausschließlich in Flavors, nicht in src/main/java, definiert wurden. Nur Ressourcen wie XMLs oder Bilder sind von der Einschränkung ausgenommen. Analog verhält es sich mit den Tests:

  1. Existiert eine MainActivityPro.java und eine MainActivityFree.java, muss es analog die Testklassen MainActivityProTest und MainActivityFreeTest geben.
  2. Existiert eine MainActivity, die sich nur durch eine "geflavorte" XML-Ressource unterscheidet, empfiehlt sich ebenfalls die Einführung der separaten Klassen MainActivityProTest und MainActivityFreeTest.
Testklassen für den Flavor "free" sind durch die gewählte Build-Variante bisher ausgegraut und von Android Studio aus nicht ausführbar (Abb. 8).

Testklassen für den Flavor "free" sind durch die gewählte Build-Variante bisher ausgegraut und von Android Studio aus nicht ausführbar (Abb. 8).
Anlegen eines Flavor Tests (Abb. 09)

Anlegen eines Flavor Tests (Abb. 09)

Zum einfachen Hinzufügen einer Testklasse bietet sich das Tastenkürzel Strg +Shift + T an.

Bei der Auswahl des Testverzeichnisses steht durch die gewählte Build-Variante nur der Flavor "Pro" zur Auswahl (Abb. 10).

Bei der Auswahl des Testverzeichnisses steht durch die gewählte Build-Variante nur der Flavor "Pro" zur Auswahl (Abb. 10).

Nach der Bestätigung durch den OK-Button können Entwickler neben den normalen Testordnern zusätzlich den Ordner des jeweiligen Flavors auswählen, wenn sie Build Variants auf den gewünschten Flavor gesetzt haben.

In Android Studio 2.0 können Entwickler mit der Blank Activity eine standardmäßige Activity anlegen, die über einen FloatingActionButton mit einem Mail-Icon verfügt. Die lässt sich für das folgende Beispielszenario in zwei Flavors mit unterschiedlich gefärbten Mail-Buttons aufteilen.

Beide MainActivity-Varianten haben denselben "Hello World"-Text, der für das Testszenario mit Robolectric in ./src/test zu finden ist, während für den Button unterschiedliche Varianten als MainActivityFreeTest in ./src/testFree und eine MainActivityFreePro in ./src/testPro existieren.

Die Free-Version hat einen Button in der Farbe rosa (Abb. 11).

Die Free-Version hat einen Button in der Farbe rosa (Abb. 11).
Die Pro-Variante zeigt einen roten Button (Abb. 12).

Die Pro-Variante zeigt einen roten Button (Abb. 12).

Eine passende Testklasse, die auch beim Einsatz mit Flavors ausführbar bleibt, sieht folgendermaßen aus:

package com.example.sn.myapplication;

import android.app.Activity;
import android.content.res.ColorStateList;
import android.graphics.Color;
import android.graphics.drawable.Drawable;
import android.support.design.widget.FloatingActionButton;

import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.robolectric.Robolectric;
import org.robolectric.RobolectricGradleTestRunner;
import org.robolectric.annotation.Config;

import static org.junit.Assert.assertEquals;

@RunWith(RobolectricGradleTestRunner.class)
@Config(constants = BuildConfig.class, sdk = 21,
packageName = "com.example.sn.myapplication")
public class MainActivityProTest { 
private static final int RED_COLOR =
Color.parseColor("#A2232F");
private Activity cut;
  @Before
public void setUp() throws Exception {
cut = Robolectric.setupActivity(MainActivity.class);
}
  @Test
public void assertEmailButtonColor() throws Exception {
FloatingActionButton mailButton =
(FloatingActionButton)cut.findViewById(R.id.fab);
ColorStateList list = mailButton.getBackgroundTintList();
int defaultColor = list != null ?
list.getDefaultColor() : 0;
assertEquals("Color does not match", RED_COLOR,
defaultColor);
}
}

Der Code in ./src/testFree/MainActivityFreeTest testet analog auf einen rosa Farbcode. Damit er ausführbar ist, darf der passende packageName nicht fehlen.

Die folgende Liste zeigt die wichtigsten Werkzeuge von Robolectric und ihre typischen Einsatzbereiche:

Das Layout einer App lässt sich mit Robolectric spielerisch überprüfen. Entwickler müssen dazu die Activity über die statischen Aufrufe von org.robolectric.Robolectric initialisieren, um anschließend mit gewöhnlichen Android-API-Aufrufen die jeweiligen Views mit Komponenten füllen zu können:

activity = Robolectric.setupActivity(MainActivity.class);
FloatingActionButton floatButton =
(FloatingActionButton) cut.findViewById(R.id.fab);
assertThat(floatButton, notNullValue());

// Of course you can assert a View's children as well.
FrameLayout layout =
(FrameLayout) activity.findViewById(R.id.cardFrameLayout);
View contentView = shadowOf(activity).getContentView();
ListView listView =
(ListView) layout.findViewById(R.id.list);

assertNotNull(layout);
assertNotNull(contentView);
assertThat(contentView, instanceOf(RelativeLayout.class));
assertThat(contentView.getPaddingBottom(), is(16));
assertThat(listView.getOnItemClickListener(),
instanceOf(MyFragment.class));

Der Versuch, nicht definierte Views aus dem Layout der zu testenden Activity zu laden, gibt null zurück.

Manchmal ist es nötig, auf Ressourcen oder Assets einer App zuzugreifen oder sie zu übergeben – beipsielsweise für Bitmap-Operationen innerhalb einer eigenen BitmapHelper-Klasse. Die Testing Support Library ist deutlich schlechter aufgestellt als Robolectric, da sie keine Ressourcen laden kann. Auch Mockito stößt in solchen Szenarien häufig an seine Grenzen, da viele Methoden der Android-API als final deklariert sind.

Der Java-Modifikator final ist für Mockito ein Problem, weil das Testframework zu mockende Methoden überschreibt, was bei finalen Klassen verboten ist. Daher sind Android-Tests nur mit Mockito nicht möglich, sondern zusätzliche Testbibliotheken wie PowerMock notwendig, die aber mit ihren Möglichkeiten nicht mit den Shadow-Klassen auf JVM-Ebene von Robolectric mithalten können.

Mit Hilfe eines Shadow-Android-Kontexts über org.robolectric.RuntimeEnvironment oder org.robolectric.Shadows lässt sich das Problem des festgelegten Android SDKs elegant lösen:

resources = RuntimeEnvironment.application.getResources();

// Loading bitmap without device test
Bitmap original =
BitmapFactory.decodeResource(resources,
R.raw.small_bitmap);
Bitmap rounded = ImageHelper.getRoundedCornerBitmap(original);
ShadowBitmap shadowRounded = shadowOf(rounded);

assertThat(rounded.getDensity(), is(original.getDensity()));
assertThat(Bitmap.Config.ARGB_8888,
is(shadowRounded.getConfig()));

Beim Verwenden einer kontextgetriebene Klasse wie Activity erfolgt der Zugriff äquivalent zu normalem Android-Code:

activity = Robolectric.setupActivity(MainActivity.class);
String appName = activity.getString(R.string.app_name);
assertThat(activity.getTitle().toString(), equalTo(appName));

Robolectric kann auch UI-Aufrufe ausführen, ohne die App auf einem Emulator oder Endgerät zu starten. Auf diese Weise können Tester überprüfen, ob die einzelnen Views eines Layouts die Nutzeraktionen korrekt umsetzen und ob andere Views richtig antworten. Zudem können sie den planmäßigen Start von Intents und Services prüfen.

Der folgende Code fügt als Reaktion auf unterschiedliche Button-Klicks die zugeordneten Zahlen von rechts nach links in eine TextView ein oder löscht sie von links nach rechts wieder. Der Test ist ohne eine Android-Laufzeitumgebung möglich.

activity = Robolectric.setupActivity(MainActivity.class);
key1 = (TextView) activity.findViewById(R.id.btn1);
key2 = (TextView) activity.findViewById(R.id.btn2);
timerValue = (TextView) activity.findViewById(R.id.timer);
deleteButton =
(ImageView) activity.findViewById(R.id.btndelete);

key1.performClick();
assertThat(timerValue.getText().toString(),
equalTo("0h 00min 01s"));

key2.performClick();
assertThat(timerValue.getText().toString(),
equalTo("0h 00min 12s"));

deleteButton.performClick();
assertThat(timerValue.getText().toString(),
equalTo("0h 00min 01s"));


Robolectric erlaubt auch die Instrumentierung des Lebenszyklus einer Anwendung. Das Überprüfen einer App auf die gewünschten Verhaltensweisen während ihres Lifecycle ist eine große Erleichterung:

// by controller
ActivityController<MainActivity> controller;
MainActivity activity;

controller.create().start().resume();
activity = controller.get();

// by shadowOf()
shadowOf(activity).recreate();
Der folgende Code überprüft verschiedene Themes
mit SharedPreferences
@Test
public void validateThemes() throws Exception {

// default is dark theme
assertThat(R.id.layoutDark,
is(shadowOf(activity).getContentView().getId()));
assertThat(R.id.layoutLight,
not(shadowOf(activity).getContentView().getId()));

SharedPreferences preferences =
activity.getPreferences(Context.MODE_PRIVATE);
SharedPreferences.Editor edit = preferences.edit();
edit.putString(Configuration.THEME_TYPE, "").apply();

assertThat(activity.getPreferences(Context.MODE_PRIVATE)
.getString(Configuration.THEME_TYPE, null), is(""));

shadowOf(activity).recreate();
assertThat(R.id.layoutDark,
not(shadowOf(activity).getContentView().getId()));
assertThat(R.id.layoutLight,
is(shadowOf(activity).getContentView().getId()));
}

Intents dienen zum Starten neuer Activities. Mit ihnen können Tester überprüfen, dass eine Activity mit einem vordefinierten Intent aktiviert wurde, dass die von der Activity gelieferten Ergebnisse stimmen und dass sie für bestimmte Aktionen beendet wird.

Mögliche Szenarien sind beispielsweise der Aufruf einer externen Anwendung wie Google Maps oder YouTube. Folgendes Listing überprüft die korrekte Ausführung eines Telefonanrufs:

phoneButton.performClick();
Intent nextStartedActivity =
shadowOf(activity).getNextStartedActivity();
String stringUri = shadowOf(nextStartedActivity).toURI();

assertThat(nextStartedActivity.getAction(),
equalTo(Intent.ACTION_DIAL));
assertThat(stringUri, equalTo("555 22040-700"));
Der folgende Code überprüft das Beenden einer
Activity mit dem korrekten Rückgabewert:
// Calling activity sets result
okButton.performClick();

assertThat(activity.isFinishing(), is(true));
Intent resultIntent = shadowOf(activity).getResultIntent();

String timerValue = resultIntent.getStringExtra(TIMESET);
assertThat(timerValue, is("0h 00min 00s"));

Analog können Tester überprüfen, dass eine Activity einen passenden Intent als Ergebnis zurückliefert:

TextView timer = (TextView) 
layout.findViewById(R.id.fragment_timer_timertextview);
timer.performClick();

String defaultValue = "0h 00min 00s";

Intent requestIntent =
new Intent(activity, CustomTimePickerActivity.class);
Intent resultIntent = new Intent();
resultIntent.putExtra(TIMESET, defaultValue);

shadowOf(activity).receiveResult(requestIntent,
Activity.RESULT_OK, resultIntent);
assertThat(timer.getText().toString(), is(defaultValue));

Was sich auf Activities bezieht, lässt sich größtenteils ebenso auf Fragments anwenden, auch wenn die Initialisierung etwas anders erfolgt:

// activity initialization
activity = Robolectric.setupActivity(MainActivity.class)
// fragment initialization 
fragment = new MyFragment();
FragmentTestUtil.startVisibleFragment(fragment);

Analog zum ActivityController gibt es in Robolectric 3.0 einen FragmentController. Er ist allerdings derzeit fehlerbehaftet, sodass Tester stattdessen die Robolectric-Klasse FragmentTestUtil einsetzen sollten. Allerdings müssen sie die Lifecycle-Operationen damit manuell ausführen.

Eclipsesource

(Bild: Eclipsesource)

Robolectric lässt sich mit Mockito kombinieren, um beispielsweise die Aufrufe von ListAdapters und Fragments zu testen. Das Einbinden Mockitos in Gradle erfolgt über folgende Zeile:

testCompile 'org.mockito:mockito-all:1.10.19'

Um ein abgeleitetes PreferenceFragment mit der Kombination der Werkzeuge zu testen, bedarf es weniger, sich wiederholender Zeilen. Ein Manko von Robolectric 3.0 ist, dass der in einer XML definierte Standardwert für Preferences nicht zurückgegeben wird, somit lassen sich nur die Aufrufe und Änderungen testen lassen:

private SettingsFragment cut;
@Before
public void setUp() throws Exception {
cut = Mockito.spy(SettingsFragment.class);
}

@Test
public void onCreate() throws Exception {
FragmentTestUtil.startVisibleFragment(cut);
Mockito.verify(cut).addPreferencesFromResource(R.xml.preferences);
}

@Test
public void loadedPreferences() throws Exception {
FragmentTestUtil.startVisibleFragment(cut);
Context context =
RuntimeEnvironment.application.getApplicationContext();

assertPreference(CheckBoxPreference.class,
context.getString(R.string.pref_vibration_key),
context.getString(R.string.pref_vibration_title),
context.getString(R.string.pref_vibration_summary));
}

private void
assertPreference(Class<? extends Preference>
preferenceType, String key, String title,
String summary)
{
Preference preference = cut.findPreference(key);
assertThat(preference, notNullValue());
assertThat(preference, instanceOf(preferenceType));
assertThat(preference.getTitle().toString(),
equalTo(title));
    assertThat(preference.getSummary().toString(),
equalTo(summary));
}

Folgendes Listing testet schnell ladende und damit flüssig arbeitende ListViews unter Verwendung des ViewHolder-Pattern [1]:

@Override
public View getView(int position, View convertView,
ViewGroup parent) {
ViewHolder viewHolder;
if (convertView == null) {
convertView = View.inflate(context,
R.layout.intro_tut_listitem,
null);
viewHolder = new ViewHolder();
viewHolder.itemImage =
(ImageView) convertView.findViewById(R.id.itemImage);
viewHolder.itemDescription =
(TextView) convertView.findViewById(R.id.itemDescription);
convertView.setTag(viewHolder);
} else {
viewHolder = (ViewHolder) convertView.getTag();
}
setItemImageBitmap(position, viewHolder);
viewHolder.itemDescription.
    setText(items.get(position).getDescription());
setTextAppearanceTheme(viewHolder);
  return convertView;
}

static class ViewHolder {
ImageView itemImage;
TextView itemDescription;
}

Der darauf angewandte Test sieht folgendermaßen aus:

@Test
public void getViewConvertViewNotNull() throws Exception {

LinearLayout convertView = Mockito.mock(LinearLayout.class);
ImageView itemImage = Mockito.mock(ImageView.class);
TextView itemDescription = Mockito.mock(TextView.class);
  ListAdapter.ViewHolder viewHolder = 
    Mockito.spy(ListAdapter.ViewHolder.class);
viewHolder.itemImage = itemImage;
viewHolder.itemDescription = itemDescription;
  Mockito.when(convertView.getTag()).thenReturn(viewHolder);
  listAdapter.getView(0, convertView, null);
  Mockito.verify(convertView).getTag();
Mockito.verify(convertView, never()).findViewById(anyInt());
Mockito.verify(itemDescription).setText(DEFAULT_ITEM_VALUE);
Mockito.verify(itemDescription).
setTextAppearance(any(Context.class), anyInt());
Mockito.verify(convertView, never()).setTag(any());
}

Robolectric ist ein mächtiges Framework, um Code, der die Android-API verwendet, innerhalb einer JVM ohne Emulator oder Endgerät zu testen. Es eignet sich für gängige Unit-Tests, da es das Testen kleiner Funktionseinheiten ermöglicht und somit Entwicklern direktes Feedback gibt.

Robolectric "mockt" nicht einfach die Funktionen Androids, sondern bildet ein repräsentatives Abbild für die spätere Ausführung auf einem Endgerät. Dabei ermöglicht das Werkzeug sogar Lifecycle-Instrumentierung und Interaktionen mit dem Benutzerinterface.

An der Testautomatisierung und deren Dokumentation unter Android laufen die Arbeiten erst seit kurzer Zeit. Daher sind die Konfiguration und Anwendung von Robolectric teilweise lückenhaft. Bei den Schwachstellen helfen Workarounds oder zusätzliche Werkzeuge.

Der renommierte Entwickler und Autor Robert C. Martin sagte, dass Code, der sich nicht bei Änderungen automatisiert testen lässt, automatisch zu Legacy Code wird. Robolectric hilft dabei, bisherigen Code unter Android auf unterster Funktionsebene wartbar zu gestalten.

Stefan Nägele
ist als Berater im Bereich Enterprise Mobile Application Development für die NovaTec Consulting GmbH tätig. Sein aktueller Schwerpunkt ist die Testautomatisierung von Apps.
(rme [2])


URL dieses Artikels:
https://www.heise.de/-3289233

Links in diesem Artikel:
[1] https://developer.android.com/training/improving-layouts/smooth-scrolling.html
[2] mailto:rme@ix.de