1. Android Test Driven Development
Carlo Codega
carlo.codega@funambol.com
Milano, 21 Maggio 2010
2. Agenda
• Introduzione al Test Driven Development
• Unit testing vs Functional testing
• Unit testing:
– Componenti indipendenti da Android: JUnit
– Componenti dipendenti da Android, isolabili dal resto dell’applicazione
– Macro componenti: Application, Activity, Service e ContentProvider
– Generazione di test suite
– Esecuzione di test suite con adb (Android Debug Bridge)
• Functional testing:
– Componenti che hanno interfaccia utente: Activity
– Integration testing sull’intera applicazione: Instrumentation
• Stress testing: Monkey tool
3. Introduzione al TDD
Perché fare TDD in Android:
1. Scrivere codice che funziona :)
2. Scrivere i test prima dell’implementazione funzionale
3. Pensare al design dell’applicazione prima dell’implementazione
reale
4. Evitare i bachi di regression. Scrivere specifici test case per ogni
bug trovato
5. Facilitare il refactoring del codice
4. Unit testing vs Functional testing
• Unit tests:
– Servono a verificare che tutte le componenti di un programma
funzionano correttamente
– Vengono fatti solitamente dagli stessi sviluppatori che hanno
implementato il codice
– Vengono scritti in un determinato linguaggio di programmazione
• Funcional tests (o acceptance tests):
– Servono a verificare se un programma risponde ai requisiti dell’utente
– Vengono fatti da persone di QA (Quality Assurance)
– Vengono scritti in un linguaggio di alto livello
5. Unit testing vs Functional testing
Unit testing Functional testing
6. Android testing framework
• Android integra al suo interno un framework per il testing:
– Package android.test
• Basato su JUnit 3
• Supporta:
– Unit testing: TestCase, AndroidTestCase
– Functional testing: InstrumentationTestCase
• Include delle utility per facilitare la creazione di test suite
• Android Debug Bridge (adb) per eseguire i test su emulatore o
device reale
7. Unit testing
• Componenti indipendenti da Android: JUnit classico
TestCase
• Aiuta a separare la logica dal contesto
• Esempio: MorseCode
9. Unit testing
= dipendende da Android
Activity
= indipendende da Android
MorseCode MorseCodeConverter
10. Unit testing
class MorseCodeConverter {
static final long SPEED_BASE = 100;
static final long DOT = SPEED_BASE;
static final long DASH = SPEED_BASE * 3;
static final long GAP = SPEED_BASE;
/** The characters from 'A' to 'Z' */
private static final long[][] LETTERS = new long[][] {
/* A */ new long[] { DOT, GAP, DASH },
/* B */ new long[] { DASH, GAP, DOT, GAP, DOT, GAP, DOT },
/* C */ new long[] { DASH, GAP, DOT, GAP, DASH, GAP, DOT },
...
};
/** The characters from '0' to '9' */
private static final long[][] NUMBERS = new long[][] {
/* 0 */ new long[] { DASH, GAP, DASH, GAP, DASH, GAP, DASH },
/* 1 */ new long[] { DOT, GAP, DASH, GAP, DASH, GAP, DASH },
...
};
11. Unit testing
/** Return the pattern data for a given character */
static long[] pattern(char c) {
...
}
/** Return the pattern data for a given string */
static long[] pattern(String str) {
...
}
}
13. Unit testing
public class MorseCodeConverterTest extends TestCase {
public void testCharacterS() throws Exception {
long[] expectedBeeps = { MorseCodeConverter.DOT,
MorseCodeConverter.DOT,
MorseCodeConverter.DOT,
MorseCodeConverter.DOT,
MorseCodeConverter.DOT };
long[] beeps = MorseCodeConverter.pattern('s');
assertEquals(expectedBeeps, beeps);
}
}
14. Unit testing
• Componenti dipendenti da Android:
AndroidTestCase
• Viene utilizzato per le componenti che richiedono l’accesso al
Context:
– eseguire Intent, lanciare Activity e Service
– accedere al File System e ContentProvider
• Esempio: AndroidKeyValueStore
16. Unit testing
public class AndroidKeyValueStore {
private SQLiteDatabase dbStore;
private DatabaseHelper mDatabaseHelper;
public AndroidKeyValueStore(Context context) {
mDatabaseHelper = new DatabaseHelper(context,
"dbname”, "tablename");
open();
// Create table
dbStore.execSQL(...);
close();
}
public void open() {
if(dbStore == null) {
dbStore = mDatabaseHelper.getWritableDatabase();
}
}
17. Unit testing
public void close() {
dbStore.close();
dbStore = null;
}
public void put(String key, String value) {
dbStore.execSQL(...);
}
public String get(String key) {
dbStore.execSQL(...);
}
}
18. Unit testing
AndroidTestCase
AndroidKeyValueStoreTest
19. Unit testing
public class AndroidKeyValueStoreTest extends AndroidTestCase {
private AndroidKeyValueStore store;
protected void setUp() {
store = new AndroidKeyValueStore(getContext());
store.load();
}
protected void tearDown() {
store.close();
}
public void testPutGet() throws Exception {
store.put("aKey", "aValue");
String value = store.get(aKey);
assertEquals(value, "aValue");
}
}
20. Unit testing
• Test di macro componenti in ambiente controllato:
– Controllo globale sul ciclo di vita del componente
– Application:
• ApplicationTestCase <T extends Application>
– Activity (functional test):
• ActivityTestCase
• ActivityInstrumentationTestCase<T extends Activity> (deprecato)
• ActivityInstrumentationTestCase2<T extends Activity>
– Service:
• ServiceTestCase<T extends Service>
– ContentProvider:
• ProviderTestCase <T extends ContentProvider> (deprecato)
• ProviderTestCase2 <T extends ContentProvider>
21. Unit testing
AndroidTestCase
ApplicationTestCase ServiceTestCase ProviderTestCase
22. Unit testing
InstrumentationTestCase
ActivityInstrumentationTestCase
Instrumentation = Functional testing !!
23. Unit testing
Generazione di test suite per raggruppare e classificare i TestCase:
TestSuite
MyTestSuite TestSuiteBuilder
24. Unit testing
Includere tutti i test case appartenenti ad un package:
public class SomeTests extends TestSuite {
public static Test suite() {
return new TestSuiteBuilder(SomeTests.class)
.includePackages("com.myapp.package1", "com.myapp.package2")
.build();
}
}
public class AllTests extends TestSuite {
public static Test suite() {
return new TestSuiteBuilder(AllTests.class)
.includeAllPackagesUnderHere().build();
}
}
25. Unit testing
Struttura dei test:
> AndroidManifest.xml
src
res
tests > AndroidManifest.xml
src > com > myapp > AllTests.java
SomeTests.java
package1 > TestCase1.java
TestCase2.java
package2 > AndroidTestCase1.java
AndroidTestCase2.java
27. Unit testing
Eseguire test suite con adb (Android Debug Bridge):
• Eseguire tutti i test:
– adb shell am instrument -w com.myapp/android.test.InstrumentationTestRunner
• Eseguire una singola TestSuite o un singolo TestCase:
– adb shell am instrument -w -e class com.myapp.SomeTests
com.myapp/android.test.InstrumentationTestRunner
– adb shell am instrument -w -e class com.myapp.package1.TestCase1
com.myapp/android.test.InstrumentationTestRunner
• Eseguire solo unit tests:
– Test che non derivano da InstrumentationTestCase
– adb shell am instrument -w -e unit true
com.myapp/android.test.InstrumentationTestRunner
28. Unit testing
• Classificare per dimensione:
– adb shell am instrument -w -e size small
com.myapp/android.test.InstrumentationTestRunner
– Classificati in base alle annotazioni:
• @SmallTest > -e size small
• @MediumTest > -e size medium
• @LargeTest > -e size large
• Output da adb:
com.myapp.package1.TestCase1:........
com.myapp.package1.TestCase2:.....
com.myapp.package2.AndroidTestCase1:.........
com.myapp.package2.AndroidTestCase1:......
Test results for InstrumentationTestRunner=............................
Time: 124.526
OK (28 tests)
29. Unit testing
• Eseguire i test direttamente dall’emulatore:
30. Functional testing
• Componenti che hanno interfaccia utente: Activity
ActivityInstrumentationTestCase
• Integration testing sull’intera applicazione:
Instrumentation
31. Functional testing
• Componenti che hanno interfaccia utente: Activity
InstrumentationTestCase
ActivityInstrumentationTestCase
32. Functional testing
ActivityInstrumentationTestCase2<T extends Activity>
• Testing isolato per una singola Activity
• Tramite l’oggetto Instrumentation si può interagire con l’interfaccia
utente
• È possibile utilizzare le TouchUtils per simulare eventi touch
40. Functional testing
public void testPressSomeKeys() {
// Make sure that we clear the output
press(KeyEvent.KEYCODE_ENTER);
press(KeyEvent.KEYCODE_CLEAR);
// 3 + 4 * 5 => 23
press(KeyEvent.KEYCODE_3);
press(KeyEvent.KEYCODE_PLUS);
press(KeyEvent.KEYCODE_4);
press(KeyEvent.KEYCODE_9 | KeyEvent.META_SHIFT_ON);
press(KeyEvent.KEYCODE_5);
press(KeyEvent.KEYCODE_ENTER);
assertEquals(displayVal(), "23");
}
41. Functional testing
public void testTapSomeButtons() {
// Make sure that we clear the output
tap(R.id.equal);
tap(R.id.del);
// 567 / 3 => 189
tap(R.id.digit5);
tap(R.id.digit6);
tap(R.id.digit7);
tap(R.id.div);
tap(R.id.digit3);
tap(R.id.equal);
assertEquals(displayVal(), "189");
42. Functional testing
// make sure we can continue calculations also
// 189 - 789 => -600
tap(R.id.minus);
tap(R.id.digit7);
tap(R.id.digit8);
tap(R.id.digit9);
tap(R.id.equal);
assertEquals(displayVal(), ”-600");
}
}
• Le primitive sono semplici e astratte: press, tap, assert
43. Functional testing
• Integration testing sull’intera applicazione:
– Definire un linguaggio di alto livello per testare l’applicazione dal
punto di vista dell’utente
– Implementare un interprete del linguaggio per tradurre i comandi in
azioni effettuate tramite l’oggetto Instrumentation
• Implementare un custom Instrumentation ci permette di avere il
totale controllo sull’applicazione
• Il manifest di test deve contenere una copia del manifest
dell’applicazione originale e la definizione di un nuovo
Instrumentation per poter eseguire i test di integrazione
45. Functional testing
• Definire un linguaggio di alto livello per simulare le azioni
dell’utente (esempio: Funambol integration tests):
– StartMainApp()
– KeyPress(String command, int count)
– WriteString(String text)
– CreateEmptyContact()
– DeleteAllContacts()
– Include(String scriptUrl)
• Linguaggio utilizzato dal QA per scrivere i test partendo dagli
acceptance tests
47. Functional testing
• CustomInstrumentation:
– Carica lo script di test e lo esegue tramite il CommandRunner
• CommandRunner:
– Interpreta lo script
– Traduce ogni comando in azione eseguita tramite il Robot
• Robot:
– Esegue delle azioni sull’applicazione tramite un riferimento
all’oggetto Instrumentation
48. Functional testing
• Acceptance test case:
1. “On Device add a record in the Contacts section filling in all possible
fields”
2. “Fire synchronization and wait for sync complete”
3. “Check the new contact is added to the server”
49. Functional testing
# Create on Device side a new Contact (Andrea Bianchi)
CreateEmptyContact()
SetContactField(FirstName,"Andrea")
SetContactField(LastName, "Bianchi")
SetContactField(TelHome, "0382665765979")
SetContactField(TelCell, "3445674")
SetContactField(Birthday, "1987-09-13")
...
SaveContact()
# Fire the synchronization and wait that is complete
KeyPress(KeyFire)
WaitForSyncToComplete(2,20)
# Verify Exchanged Data
CheckExchangedData("Contacts",1,0,0,0,0,0)
RefreshServer()
# Verify if the contact is added on the Server
CheckNewContactOnServer("Andrea", "Bianchi", true)
50. Functional testing
• Eseguire i test di integrazione:
– adb shell am instrument [options] -w
com.myapp/com.myapp.CustomInstrumentation
– È possibile specificare dei parametri aggiuntivi tramite il flag -e (extra)
– I parametri vengono passati tramite una Bundle attraverso il metodo
onCreate() dell’oggetto Instrumentation:
• public void onCreate(Bundle arguments)
– Dall’emulatore:
• Dev Tools / Instrumentation / MyApp Integration Tests
51. Stress testing
• Monkey tool:
– Applicazione command line che può essere eseguita su emulatore o
device reale
– Genera eventi utente pseudo-casuali:
• Click
• Touch
• Gestures
– Configurabile:
• -p <allowed-package-name>: lista dei package sui quali è possibile
generare eventi
– Usage:
• adb shell monkey [options] <event-count>