Introduction à Espresso

Je vais dans ce tuto vous présenter la librairie Espresso, utile afin de réaliser des tests automatisés sur nos écrans. Elle permet de simuler différentes actions sur l’écran, comme des click ou des swipe, puis de vérifier le contenu affiché. Dans des processus d’intégration continue, ce type de librairie est très souvent utilisé afin de vérifier les non régressions.

Espresso va embarquer des tests dans l’APK, qui s’exécuterons sur votre smartphone au lancement de l’application depuis Android Studio. Le résultat sera affiché depuis les consoles de l’IDE.

Importer Espresso

Afin d’importer Espresso, il suffit d’ajouter les lignes suivantes à votre app/build.gradle :

dependencies {
    ...
    compile 'com.android.support:support-annotations:22.2.0'

    androidTestCompile 'com.android.support.test.espresso:espresso-core:2.2'
    androidTestCompile 'com.android.support.test.espresso:espresso-contrib:2.2'
    androidTestCompile 'com.android.support.test:runner:0.3'
}

Puis de définit le testInstrumentationRunner en “android.support.test.runner.AndroidJUnitRunner” dans la partie android{}

android {
    ...
    defaultConfig {
        ...
        testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
    }
    packagingOptions {
        exclude 'LICENSE.txt'
    }
}

Ce qui donne un fichier build.gradle de la forme suivante :

app/build.gradle

apply plugin: 'com.android.application'

android {
    compileSdkVersion 22
    buildToolsVersion "22.0.0"

    defaultConfig {
        applicationId "com.tutosandroidfrance.espressosample"
        minSdkVersion 14
        targetSdkVersion 22
        versionCode 1
        versionName "1.0"

        //on indique à android d'effectuer ses tests fonctionnels avec AndroidJUnitRunner
        testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
    }

    packagingOptions {
        exclude 'LICENSE.txt'
    }
}

dependencies {
    compile fileTree(dir: 'libs', include: ['*.jar'])
    //vos dépendances usuelles
    compile 'com.android.support:appcompat-v7:22.2.0'
    compile 'com.android.support:recyclerview-v7:22.2.0'
    compile 'com.android.support:cardview-v7:22.2.0'
    compile 'com.jakewharton:butterknife:7.0.1'

    //les dépendances requises par Espresso
    compile 'com.android.support:support-annotations:22.2.0'

    androidTestCompile 'com.android.support.test.espresso:espresso-core:2.2'
    androidTestCompile 'com.android.support.test.espresso:espresso-contrib:2.2'
    androidTestCompile 'com.android.support.test:runner:0.3'
}

Exemple d’application

Commençons par définir l’application que nous allons tester. Nous partirons du layout suivant, contenant un EditText, où l’utilisateurs sera invité à écrire son prénom.
Une fois qu’il cliquera sur le bouton “LOGIN”, ces 2 éléments disparaitrons afin de laisser apparaitre le message suivant “Hello PRENOM”.

layout/activity_main.xml

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:gravity="center_horizontal"
    android:orientation="vertical">

    <FrameLayout
        android:layout_width="match_parent"
        android:layout_height="200dp"
        android:background="@color/primary"
        android:elevation="4dp">

        <android.support.v7.widget.AppCompatTextView
            style="@style/Base.TextAppearance.AppCompat.Display1"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginTop="60dp"
            android:layout_marginLeft="50dp"
            android:text="Espresso Sample"
            android:textColor="@android:color/white" />

        <android.support.v7.widget.AppCompatTextView
            style="@style/Base.TextAppearance.AppCompat.Headline"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginTop="105dp"
            android:layout_marginLeft="50dp"
            android:text="So easy !"
            android:textColor="@android:color/white" />

    </FrameLayout>

    <LinearLayout
        android:id="@+id/loginContainer"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:orientation="vertical">

        <LinearLayout
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:gravity="center_vertical"
            android:orientation="horizontal"
            android:padding="10dp">

            <ImageView
                android:layout_width="25dp"
                android:layout_height="25dp"
                android:src="@drawable/ic_perm_contact_cal_grey600_18dp" />

            <EditText
                android:id="@+id/editText"
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:layout_marginLeft="10dp"
                android:hint="@string/yourName" />

        </LinearLayout>

        <Button
            android:id="@+id/login"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:text="@string/login" />

    </LinearLayout>

    <android.support.v7.widget.CardView
        android:id="@+id/textContainer"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_margin="15dp"
        android:visibility="gone"
        app:cardElevation="4dp"
        tools:visibility="visible">

        <LinearLayout
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:orientation="vertical"
            android:padding="20dp">

            <TextView
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_centerInParent="true"
                android:layout_marginBottom="10dp"
                android:fontFamily="sans-serif"
                android:text="Description"
                android:textStyle="bold" />

            <TextView
                android:id="@+id/text"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_centerInParent="true"
                android:fontFamily="sans-serif-light"
                tools:text="hello florent" />
        </LinearLayout>
    </android.support.v7.widget.CardView>

</LinearLayout>

Preview :

Capture d’écran 2015-07-09 à 16.18.28

J’ai ici utilisé Butterknife pour la récupération des vue ainsi que la gestion du click sur le bouton LOGIN.

public class MainActivity extends AppCompatActivity {

    @Bind(R.id.login) Button login;
    @Bind(R.id.editText) EditText editText;
    @Bind(R.id.loginContainer) View loginContainer;
    @Bind(R.id.textContainer) View textContainer;
    @Bind(R.id.text) TextView text;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        ButterKnife.bind(this);
    }

    @OnClick(R.id.login)
    public void onLoginClicked(){
        String name = editText.getText().toString();
        if(!name.isEmpty()) {
            closeKeyboard(editText);

            //hide views
            loginContainer.setVisibility(View.GONE);
            textContainer.setVisibility(View.VISIBLE);

            //display
            text.setText("Hello " + name);
        }
    }

    private void closeKeyboard(View view){
        //close keyboard
        InputMethodManager inputMethodManager =(InputMethodManager)getSystemService(Activity.INPUT_METHOD_SERVICE);
        inputMethodManager.hideSoftInputFromWindow(view.getWindowToken(), 0);
    }

}

Au click du bouton LOGIN, je ferme le clavier, puis affiche le texte “Hello “+name,
puis met les 2 anciennes vues en GONE, ce qui entraine leur disparition.

device-2015-07-10-153431device-2015-07-10-153511  device-2015-07-10-153529

Passons aux tests !

Présentation

Cibler une vue

Première étape, dire à Espresso quelle vue inspécter, pour se faire il faut utiliser la méthode onView, de la façon suivante :

onView(MATCHER).ACTIONS

J’utilise onView avec un import statique de Espresso.onView afin de ne pas avoir à écrire Espresso à chaque appel. Ceci n’est possible que puisque la fonction onView est définie statiquement.

Espresso fonctionne avec des MATCHER et non directement avec des variables de type vue. Pour faire une comparaison assez simple, les matcher peuvent s’apparenter aux expressions régulières, sur un texte nous pouvons demander de récupérer les mots de la forme “*ar*”, ce qui peux, sur la phrase “Parler à papa puis marcher dans la rue” va matcher avec “Parler” et “marcher”.

Ici le comportement est plutôt ressemblant, nous pouvons matcher une vue de la façon suivante

onView( withId(R.id.text) ) //matches toutes les vues dont l'id est R.id.text
onView( withText("Your name") ) //matches toutes les vues dont le texte est "Your name"
onView( isAssignableFor(EditText.class) ) //matches toutes les vues EditText

Verifier une vue

//regarde si la vue text est affichée à l'écran
onView(withId(R.id.text)).check(matches(isDisplayed()));

//regarde si la vue text contient le texte "YOUR TEXT"
onView(withId(R.id.text)).check(matches(withText("YOUR TEXT")));

Il est possible d’utiliser des opérateurs sur les matches afin de vérifier plusieurs comportements différents sur une vue, ou de jouer avec des négations :

//regarde si la vue texte n'est pas affichée à l'écran
onView(withId(R.id.text)).check(matches(not(isDisplayed())));

//regarde si la vue texte est affichée ET contient le texte "YOUR TEXT"
onView(withId(R.id.text)).check(matches(allOf(isDisplayed(),withText("YOUR TEXT"))));

//regarde si la vue texte est affichée OU contient le texte "YOUR TEXT"
onView(withId(R.id.text)).check(matches(anyOf(isDisplayed(),withText("YOUR TEXT"))));

Résultat final

@LargeTest
public class MainActivityTest extends ActivityInstrumentationTestCase2<MainActivity> {

    //on précise que l'on veux tester un MainActivity
    public MainActivityTest() {
        super(MainActivity.class);
    }

    @Before
    public void setUp() throws Exception {
        super.setUp();

        //doit être appelé dans le setup
        getActivity();
    }

    @Test
    public void testContainsIntialViews() {
        //je vais tester ici que l'EditText et le bouton LOGIN sont bien affichés
        //mais que le TextView "Hello XXXX" n'est pas présent

        onView(withId(R.id.editText)).check(matches(isDisplayed()));
        onView(withText("LOGIN")).check(matches(isDisplayed()));
        onView(withId(R.id.text)).check(matches(not(isDisplayed())));
    }

    //teste le comportement de l'écran si on appuie sur LOGIN sans avoir écrit de nom
    @Test
    public void testClickLogin_emptyText() {
        //je vais cliquer sur LOGIN, mais sans avoir écrit de texte dans l'EditText
        onView(withText("LOGIN")).perform(click());

        //Ce qui ne devrait pas cacher le bouton LOGIN, ni afficher le "Hello XXXX"
        onView(withText("LOGIN")).check(matches(isDisplayed()));
        onView(withId(R.id.text)).check(matches(not(isDisplayed())));
    }

    @Test
    public void testClickLogin_withText() {
        //je vais écrire "florent" dans l'EditText
        onView(withId(R.id.editText)).perform(typeText("florent"));

        //puis clicker sur le bouton LOGIN
        onView(withText("LOGIN")).perform(click());

        //ce qui devrait faire disparaitre LOGIN et l'EditText
        onView(withText("LOGIN")).check(matches(not(isDisplayed())));
        onView(withId(R.id.editText)).check(matches(not(isDisplayed())));

        //puis afficher "Hello florent"
        onView(withId(R.id.text)).check(matches(allOf(isDisplayed(), withText("Hello florent"))));
    }

}

Afin d’exécuter les tests, il suffit de lancer “Run MainActivityTest” :

Capture d’écran 2015-07-10 à 15.41.08

Les tests s’afficherons de la façon suivante :

Capture d’écran 2015-07-09 à 16.38.23 2

Les sources de ce tuto sont disponibles sur github

Si cette librairie vous intéresse, je vous invite à vous rendre sur la documentation officielle d’Espresso

Laisser un commentaire

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