Tests Unitaires

Vous voulez réaliser des tests unitaires sous Android mais vous ne savez pas comment procéder ? asseyez vous et lisez ce tutoriel 🙂

Classe à tester

Imaginons que nous souhaitons tester la classe suivante : Storage
Cette classe va stocker une liste d’entiers en session.
Regardons ensemble comment la tester sous Android Studio.

public class Storage {

    SharedPreferences sharedPreferences;
    private static final String PREFS = "PREFS";
    private static final String PREFS_INT_LIST = "PREFS_INT_LIST";

    public Storage(Context context){
        this.sharedPreferences = context.getSharedPreferences(PREFS,Context.MODE_PRIVATE);
    }

    protected String transformToString(List<Integer> list){
        StringBuilder stringBuilder = new StringBuilder();
        int size = list.size();
        for(int i=0;i<size;++i){
            stringBuilder.append(list.get(i));
            if(i!=size-1)
                stringBuilder.append(",");
        }
        return stringBuilder.toString();
    }

    protected List<Integer> transformFromString(String string){
        List<Integer> list = new ArrayList<>();

        String[] splitted = string.split(",");

        int size = splitted.length;
        for(int i=0;i<size;++i)
            list.add(Integer.valueOf(splitted[i]));

        return list;
    }

    public List<Integer> load(){
        String content = sharedPreferences.getString(PREFS_INT_LIST, null);
        if(content != null)
            return transformFromString(content);
        else
            return new ArrayList<>();

    }

    public void save(List<Integer> list){
        sharedPreferences.edit().putString(PREFS_INT_LIST,transformToString(list)).apply();
    }

}

Tests sous Android Studio

Il est maintenant possible d’effectuer ses tests unitaires directement depuis Android Studio, sans avoir besoin de les exécuter sur un appareil Android connecté à votre ordinateur. Pour se faire, il vous suffit de réaliser quelques petites étapes au préalable :

Ajoutez les dépendances à JUnit4 et Mockito à votre build.gradle

app/build.gradle

dependencies {
    compile fileTree(dir: 'libs', include: ['*.jar'])
    compile 'com.android.support:appcompat-v7:22.2.0'

    testCompile 'junit:junit:4.12'
    testCompile 'org.mockito:mockito-core:2.0.5-beta'
}

Sélectionnez Unit Tests, en remplace menat à Android Instrumentation Test (qui demande d’être exécuté sur un device Android) en tant que test Artifact, dans la section Build Variantes

Capture d’écran 2015-07-03 à 23.14.10

Créez un répertoire test/java/ dans app/src/Capture d’écran 2015-07-03 à 23.15.28


Capture d’écran 2015-07-03 à 23.15.35

Vous pouvez maintenant générer un Test depuis la classe Storage (Alt+N sur Storage) (cmd+shift+t sous mac)

Capture d’écran 2015-07-03 à 23.02.16

Sélectionnez JUinit4, Setup/ @Before puis cochez toutes les fonctions à tester

Capture d’écran 2015-07-03 à 23.08.32

Ce qui génèrera la classe StorageTest :

app/src/test/java/…/StorageTest.java

public class StorageTest {

    @Before
    public void setUp() throws Exception {
         //sera appellé avant chaque Test, créer ici votre Storage
    }

    @Test
    public void testTransformToString() throws Exception {

    }

    @Test
    public void testTransformFromString() throws Exception {

    }

    @Test
    public void testLoad() throws Exception {

    }

    @Test
    public void testSave() throws Exception {

    }

}

Préparer nos Tests

La fonction @Before setUp() vous permet d’initialiser l’objet à tester. Elle est appellés avant chaque test, afin de créer un objet “vide de toute modification”. Nous pouvons donc créer notre Storage de cette façon

public class StorageTest {

    //objet à tester
    Storage storage;

    @Before
    public void setUp() throws Exception {
         //sera appellé avant chaque Test, créer ici votre Storage

         storage = new Storage(null);
    }

    ...

}

Tester une fonction

Tester une fonction unitairement consiste à gérer les paramètres fournis, afin d’attendre une certaine valeur en retour.

La syntaxe sous JUnit4 est la suivante :

@Test
public void testMaFonction() throws Exception {
    //Preparer les arguments
    argument = XXXXX;

    Object retour = storage.maFonction(arguments);

    //Verifier le retour
    assertYYYYY(retour);
}

Ce qui donne, sur la fonction transformToString(List integer)

@Test
public void testTransformToString() throws Exception {
     List<Integer> integers = Arrays.asList(1, 2, 3);

     String string = storage.transformToString(integers);

     assertEquals("1,2,3",string);
}

On vérifie ici que la liste [1,2,3] se transforme bien en “1,2,3”

Les Mocks

Les plus aguérits auront remarqué que j’ai fournit null en tant que Context dans le setUp. En effet, Context n’est pas instantiable avec un new Context(). Il faut donc utiliser un “Faux Objet”, nommé Mock (bouchon), qui va reproduire le comportement d’un Context, en étant totalement gérable depuis nos tests.

Un Mock est un objet dont nous pouvons redéfinir la valeur de retour des méthodes. Nous pouvons par exemple créer un faux Context, dont l’appel à la fonction getString(int resId) retournera toujours “Hello World”, de la façon suivante

Context context = mock(Context.class);
doReturn("Hello World").when(context).getString(anyInt());
doReturn(NOUVELLE_VALEUR_RETOUR).when(MOCK).FONCTION_A_REDEFINIR(matchers_parametres)

Les matchers_parametres permettent d’indiquer quel type de paramètre nous voulons intercepter :

  • any(String.class) : un argument de type String (peu importe la valeur)
  • anyString(), anyBoolean(), …: raccourcit de any pour des types primitifs
  • isNull(String.class) : un argument null de type String
  • eq(“MA_VALEUR”) : une valeur précise, un string dont le texte est “MA_VALEUR”

Définir nos Mocks

La création d’un mock se fait en 2 étapes

  • Ajouter des variables @Mock à notre StorageTest
  • Utiliser MockitoAnnotations.initMocks(this) dans la fonction setUp
public class StorageTest {

    //objet à tester
    Storage storage;

    @Mock Context context;
    @Mock SharedPreferences sharedPreferences;
    @Mock SharedPreferences.Editor editor;

    @Before
    public void setUp() throws Exception {
        MockitoAnnotations.initMocks(this); //créé tous les @Mock

        //on remplace le context.getSharedPreferences(String,int) afin qu'il retourne notre mock sharedPreferences
        doReturn(sharedPreferences).when(context).getSharedPreferences(anyString(), anyInt());

        //on remplace le sharedPreferences.edit() afin qu'il retourne notre mock edit
        doReturn(editor).when(sharedPreferences).edit();

        //on remplace la fonction edit.putString(String, String) afin qu'elle retourne edit,
        //afin d'éviter le crash au edit.putString(S,S).apply()
        doReturn(editor).when(editor).putString(anyString(), anyString());

        //puis on constrit notre storage avec notre mock context
        storage = new Storage(context);
    }

    ...
}

Nous pouvons maintenant tester les méthode ayant besoin d’un Context / SharedPreference.

Verifier qu’une fonction est appelée sur nos Mocks

Afin de vérifier qu’une méthode est appelée, nous pouvons utiliser la syntaxe suivante

verify(mock,fréquence).FONCTION_DU_MOCK(marchers_arguments)

Nous pouvons donc vérifier que la fonction edit.putString(*,”1,2,3″).apply() est appelée :

@Test
public void testSave() throws Exception {
    List<Integer> integers = Arrays.asList(1, 2, 3);

    storage.save(integers);

    verify(editor, atLeastOnce()).putString(anyString(),eq("1,2,3"));
    verify(editor, atLeastOnce()).apply();
}

atLeastOnce() indique que nous souhaitons vérifier que la méthode est au moins utilisée une fois.

Résultat final

public class StorageTest {

    @Mock Context context;
    @Mock SharedPreferences sharedPreferences;
    @Mock SharedPreferences.Editor editor;

    //l'objet à tester
    Storage storage;

    @Before
    public void setUp() throws Exception {
        MockitoAnnotations.initMocks(this); //créé tous les @Mock

        //on remplace le context.getSharedPreferences(String,int) afin qu'il retourne notre mock sharedPreferences
        doReturn(sharedPreferences).when(context).getSharedPreferences(anyString(), anyInt());

        //on remplace le sharedPreferences.edit() afin qu'il retourne notre mock edit
        doReturn(editor).when(sharedPreferences).edit();

        //on remplace la fonction edit.putString(String, String) afin qu'elle retourne edit,
        //afin d'éviter le crash au edit.putString(S,S).apply
        doReturn(editor).when(editor).putString(anyString(), anyString());

        //puis on constrit notre storage avec notre mock context
        storage = new Storage(context);
    }

    @Test
    public void testTransformToString() throws Exception {
        List<Integer> integers = Arrays.asList(1, 2, 3);

        String string = storage.transformToString(integers);

        assertEquals("1,2,3",string);
    }

    @Test
    public void testTransformFromString() throws Exception {
        String string = "1,2,3";

        List<Integer> integers = storage.transformFromString(string);

        assertEquals(Arrays.asList(1, 2, 3),integers);
    }

    @Test
    public void testLoad() throws Exception {
        doReturn("1,2,3").when(sharedPreferences).getString(anyString(),isNull(String.class));

        List<Integer> integers = storage.load();

        verify(sharedPreferences, atLeastOnce()).getString(anyString(),isNull(String.class));
        assertEquals(Arrays.asList(1, 2, 3), integers);
    }

    @Test
    public void testLoadNull() throws Exception {
        doReturn(null).when(sharedPreferences).getString(anyString(),isNull(String.class));

        List<Integer> integers = storage.load();

        verify(sharedPreferences, atLeastOnce()).getString(anyString(),isNull(String.class));
        assertTrue(integers.isEmpty());
    }

    @Test
    public void testSave() throws Exception {
        List<Integer> integers = Arrays.asList(1, 2, 3);

        storage.save(integers);

        verify(editor, atLeastOnce()).putString(anyString(),eq("1,2,3"));
        verify(editor, atLeastOnce()).apply();
    }

}

Exécuter

Pour exécuter le test, vous pouvez utiliser la commande suivante

gradlew test

Ou directement depuis Android Studio

Capture d’écran 2015-07-03 à 23.22.06

Les sources de ce tuto sont disponibles sur github

Laisser un commentaire

Votre adresse de messagerie ne sera pas publiée. Les champs obligatoires sont indiqués avec *