Gebruikersinterfaces voor Android testen met Espresso

In dit bericht leert u hoe u UI-tests kunt schrijven met het Espresso-testraamwerk en uw testworkflow kunt automatiseren in plaats van het saaie en zeer foutgevoelige handmatige proces te gebruiken. 

Espresso is een testraamwerk voor het schrijven van UI-tests in Android. Volgens de officiële documenten, kunt u:

Gebruik Espresso om beknopte, mooie en betrouwbare Android UI-tests te schrijven.

1. Waarom Espresso gebruiken?

Een van de problemen bij handmatig testen is dat het tijdrovend en vervelend kan zijn om te presteren. Als u bijvoorbeeld een inlogscherm (handmatig) in een Android-app wilt testen, moet u het volgende doen:

  1. Start de app. 
  2. Navigeer naar het inlogscherm. 
  3. Bevestig of het usernameEditText en passwordEditText zijn zichtbaar. 
  4. Typ de gebruikersnaam en het wachtwoord in de respectieve velden. 
  5. Bevestig of de aanmeldknop ook zichtbaar is en klik vervolgens op die aanmeldknop.
  6. Controleer of de juiste weergaven worden weergegeven wanneer die login succesvol was of mislukte. 

In plaats van al onze tijd te besteden aan het handmatig testen van onze app, zou het beter zijn om meer tijd te besteden aan het schrijven van code waardoor onze app zich onderscheidt van de rest! En hoewel handmatig testen vervelend en vrij langzaam is, is het nog steeds foutgevoelig en mist u enkele hoekgevallen. 

Enkele van de voordelen van geautomatiseerd testen omvatten het volgende:   

  • Geautomatiseerde tests voeren exact dezelfde testcases uit telkens wanneer ze worden uitgevoerd. 
  • Ontwikkelaars kunnen snel een probleem snel herkennen voordat het naar het QA-team wordt verzonden. 
  • Het kan veel tijd besparen, in tegenstelling tot handmatig testen. Door tijd te besparen, kunnen software-engineers en het QA-team in plaats daarvan meer tijd besteden aan uitdagende en belonende taken. 
  • Een hogere testdekking wordt bereikt, wat leidt tot een betere kwaliteit van de toepassing. 

In deze zelfstudie leren we meer over Espresso door het te integreren in een Android Studio-project. We zullen UI-tests schrijven voor een inlogscherm en een RecyclerView, en we zullen leren over het testen van intenties. 

Kwaliteit is geen handeling, het is een gewoonte. - Pablo Picasso

2. Vereisten

Als je deze zelfstudie wilt kunnen volgen, heb je het volgende nodig:

  • een basiskennis van de belangrijkste Android-API's en Kotlin
  • Android Studio 3.1.3 of hoger
  • Kotlin plug-in 1.2.51 of hoger

Een voorbeeldproject (in Kotlin) voor deze tutorial is te vinden op onze GitHub repo, zodat je gemakkelijk kunt volgen.

3. Maak een Android Studio-project

Start je Android Studio 3 op en maak een nieuw project met een lege activiteit genaamd Hoofdactiviteit. Zorg ervoor om te controleren Inclusief Kotlin-ondersteuning

4. Stel Espresso in en AndroidJUnitRunner

Nadat u een nieuw project hebt gemaakt, moet u ervoor zorgen dat u de volgende afhankelijkheden van de ondersteuningsbibliotheek Android Test in uw build.gradle (hoewel Android Studio deze al voor ons heeft opgenomen). In deze zelfstudie gebruiken we de nieuwste Espresso-bibliotheekversie 3.0.2 (vanaf dit schrijven). 

android // ... defaultConfig // ... testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" // ... afhankelijkheden // ... androidTestImplementation 'com.android.support.test.espresso: espresso-core: 3.0. 2 'androidTestImplementation' com.android.support.test: runner: 1.0.2 'androidTestImplementation' com.android.support.test: rules: 1.0.2 '

We hebben ook de instrumentatierunner opgenomen AndroidJUnitRunner:

Een Instrumentatie die JUnit3- en JUnit4-tests uitvoert tegen een Android-pakket (toepassing).

Let daar op Instrumentatie is gewoon een basisklasse voor het implementeren van applicatie-instrumentatiecode. 

Animatie uitschakelen 

De synchronisatie van Espresso, die niet weet hoe te wachten tot een animatie is voltooid, kan ervoor zorgen dat sommige tests mislukken, als u animaties op uw testapparaat toestaat. Als u de animatie op uw testapparaat wilt uitschakelen, gaat u naar instellingen > Ontwikkelaarsopties en schakel alle volgende opties uit onder het gedeelte "Tekenen": 

  • Vensteranimatieschaal
  • Overgang animatieschaal
  • Animator duurschaal

5. Schrijf uw eerste test in Espresso

Eerst beginnen we met het testen van een aanmeldscherm. Zo begint de login-stroom: de gebruiker start de app en het eerste getoonde scherm bevat een single Log in knop. Wanneer dat Log in knop wordt geklikt, wordt de LoginActivity scherm. Dit scherm bevat slechts twee Tekst bewerkens (de velden gebruikersnaam en wachtwoord) en a voorleggen knop. 

Dit is wat onze Hoofdactiviteit lay-out ziet eruit als:

Dit is wat onze LoginActivity lay-out ziet eruit als:

Laten we nu een test schrijven voor onze Hoofdactiviteit klasse. Ga naar je Hoofdactiviteit klasse, verplaats de cursor naar de Hoofdactiviteit naam en druk op Shift-Control-T. kiezen Maak een nieuwe test ... in het pop-upmenu. 

druk de OK knop en er verschijnt een nieuw dialoogvenster. Kies de androidTest map en klik op de OK knop nog een keer. Merk op dat, omdat we een instrumentatietest schrijven (specifieke Android SDK-tests), de testcases zich in de androidTest / java map. 

Nu heeft Android Studio met succes een testklasse voor ons gemaakt. Boven de klassenaam neemt u deze annotatie op: @RunWith (AndroidJUnit4 :: klasse).

importeer android.support.test.runner.AndroidJUnit4 import org.junit.runner.RunWith @RunWith (AndroidJUnit4 :: class) class MainActivityTest 

Deze annotatie betekent dat alle tests in deze klasse Android-specifieke tests zijn.

Testactiviteiten

Omdat we een activiteit willen testen, moeten we een beetje instellen. We moeten Espresso informeren welke Activiteit moet worden geopend of gestart voordat deze wordt uitgevoerd en vernietigd na het uitvoeren van een testmethode. 

import android.support.test.rule.ActivityTestRule import android.support.test.runner.AndroidJUnit4 import org.junit.Rule import org.junit.runner.RunWith @RunWith (AndroidJUnit4 :: class) class MainActivityTest @Rule @JvmField var activityRule = ActivityTestRule(MainActivity :: class.java)

Merk op dat de @Regel annotatie betekent dat dit een JUnit4-testregel is. JUnit4-testregels worden voor en na elke testmethode uitgevoerd (geannoteerd met @Test). In ons eigen scenario willen we starten Hoofdactiviteit vóór elke testmethode en vernietig het daarna. 

We hebben ook de @JvmField Annotatie van Kotlin. Dit geeft de compiler eenvoudig de opdracht om geen getters en setters voor de eigenschap te genereren en deze in plaats daarvan bloot te stellen als een eenvoudig Java-veld.

Dit zijn de drie belangrijkste stappen bij het schrijven van een Espresso-test:

  • Zoek naar de widget (bijv. Tekstweergave of Knop) die je wilt testen.
  • Voer een of meer acties uit op die widget. 
  • Controleer of controleer of de widget zich in een bepaalde staat bevindt. 

De volgende typen annotaties kunnen worden toegepast op de methoden die in de testklasse worden gebruikt.

  • @Voor klas: dit geeft aan dat de statische methode waarop deze annotatie wordt toegepast, één keer moet worden uitgevoerd en vóór alle tests in de klas. Dit kan bijvoorbeeld worden gebruikt om een ​​verbinding met een database tot stand te brengen. 
  • @Voor: geeft aan dat de methode waaraan deze annotatie is gekoppeld moet worden uitgevoerd vóór elke testmethode in de klas.
  • @Test: geeft aan dat de methode waaraan deze annotatie is gekoppeld, als een testcase moet worden uitgevoerd.
  • @Na: geeft aan dat de methode waaraan deze annotatie is gekoppeld, na elke testmethode moet worden uitgevoerd. 
  • @Na de les: geeft aan dat de methode waaraan deze annotatie is gekoppeld, moet worden uitgevoerd nadat alle testmethoden in de klasse zijn uitgevoerd. Hier sluiten we doorgaans bronnen af ​​die werden geopend @Voor klas

Vind een Uitzicht Gebruik makend van OnView ()

In onze Hoofdactiviteit lay-outbestand, we hebben slechts één widget-de Log in knop. Laten we een scenario testen waarbij een gebruiker die knop vindt en erop klikt.

import android.support.test.espresso.Espresso.onView import android.support.test.espresso.matcher.ViewMatchers.withId // ... @RunWith (AndroidJUnit4 :: class) class MainActivityTest // ... @Test @Throws (Uitzondering: : class) fun clickLoginButton_opensLoginUi () onView (withId (R.id.btn_login))

Om widgets in Espresso te vinden, maken we gebruik van de OnView () statische methode (in plaats van findViewById ()). Het parametertype dat we leveren OnView () is een Matcher. Merk op dat de Matcher API komt niet van de Android SDK maar van het Hamcrest-project. De matcher-bibliotheek van Hamcrest bevindt zich in de Espresso-bibliotheek die we via Gradle hebben getrokken. 

De OnView (withId (R.id.btn_login)) zal terugkeren a ViewInteraction dat is voor een Uitzicht wiens ID is R.id.btn_login. In het bovenstaande voorbeeld hebben we gebruikt withId () om een ​​widget met een bepaald ID te zoeken. Andere view matchers die we kunnen gebruiken zijn: 

  • withText (): retourneert een matcher die overeenkomt Tekstweergave op basis van de waarde van de teksteigenschap.
  • withHint (): retourneert een matcher die overeenkomt Tekstweergave op basis van de eigenschapwaarde van de hint.
  • withTagKey (): retourneert een matcher die overeenkomt Uitzicht op basis van tag-sleutels.
  • withTagValue (): retourneert een matcher die overeenkomt Uitzichts gebaseerd op tageigenschapswaarden.

Laten we eerst testen of de knop daadwerkelijk op het scherm wordt weergegeven. 

OnView (withId (R.id.btn_login)). te controleren (wedstrijden (isDisplayed ()))

Hier bevestigen we alleen maar of de knop met de gegeven id (R.id.btn_login) is zichtbaar voor de gebruiker, dus gebruiken we de controleren() methode om te bevestigen als het onderliggende Uitzicht heeft een bepaalde staat - in ons geval, als het zichtbaar is.

De wedstrijden() statische methode levert een generiek op ViewAssertion die beweert dat er een weergave bestaat in de weergavehiërarchie en dat deze wordt gematcht door de gegeven weergavezoekmachine. Die bepaalde view matcher wordt geretourneerd door te bellen is weergegeven(). Zoals gesuggereerd door de methode naam, is weergegeven() is een matcher die overeenkomt Uitzichts die momenteel op het scherm worden weergegeven voor de gebruiker. Als we bijvoorbeeld willen controleren of een knop is ingeschakeld, gaan we gewoon door is ingeschakeld() naar wedstrijden()

Andere populaire view matchers die we kunnen doorgeven aan de wedstrijden() methode zijn:

  • hasFocus (): retourneert een matcher die overeenkomt Uitzichts die momenteel focus hebben.
  • is nagekeken(): retourneert een matcher die alleen accepteert als de weergave een is CompoundButton (of een subtype van) en bevindt zich in de gecontroleerde staat. Het tegenovergestelde van deze methode is isNotChecked ()
  • is geselecteerd(): retourneert een matcher die overeenkomt Uitzichts die zijn geselecteerd.

Als u de test wilt uitvoeren, klikt u op de groene driehoek naast de methode of de klassenaam. Als u op de groene driehoek naast de klassenaam klikt, worden alle testmethoden in die klasse uitgevoerd, terwijl degene naast een methode de test alleen voor die methode uitvoert. 

Hoera! Onze test is geslaagd!


Voer acties uit op een weergave

Op een ViewInteraction object dat wordt geretourneerd door te bellen OnView (), we kunnen acties simuleren die een gebruiker op een widget kan uitvoeren. We kunnen bijvoorbeeld een klikactie simuleren door simpelweg de Klik() statische methode binnen de ViewActions klasse. Dit zal a terugkeren ViewAction object voor ons. 

De documentatie zegt dat ViewAction is:

Verantwoordelijk voor het uitvoeren van een interactie op het gegeven View-element.
@Test fun clickLoginButton_opensLoginUi () // ... onView (withId (R.id.btn_login)). Uitvoeren (klik ())

We voeren een klikgebeurtenis uit door eerst te callen uitvoeren(). Deze methode voert de opgegeven actie (s) uit op de weergave geselecteerd door de huidige view matcher. Merk op dat we het een enkele actie of een lijst met acties kunnen doorgeven (in volgorde uitgevoerd). Hier hebben we het gegeven Klik(). Andere mogelijke acties zijn:

  • typ Text() om typetekst te imiteren in een Tekst bewerken.
  • duidelijke tekst() om tekst te wissen in een Tekst bewerken.
  • Dubbelklik() om te simuleren dubbelklikken a Uitzicht.
  • longClick () imiteren van lang klikken op een Uitzicht.
  • scrollTo () om scrollen te simuleren ScrollView naar een bepaald Uitzicht dat is zichtbaar. 
  • swipeLeft () om te simuleren van rechts naar links over het verticale midden van een Uitzicht.

Veel meer simulaties zijn te vinden binnen de ViewActions klasse. 

Valideer met View Assertions

Laten we onze test afronden, om te valideren dat de LoginActivity scherm wordt weergegeven wanneer de Log in knop is geklikt. Hoewel we al hebben gezien hoe te gebruiken controleren() op een ViewInteraction, laten we het opnieuw gebruiken, het doorgeven aan een ander ViewAssertion

@Test fun clickLoginButton_opensLoginUi () // ... onView (withId (R.id.tv_login)). Check (matches (isDisplayed ()))

Binnen in de LoginActivity lay-outbestand, afgezien van Tekst bewerkens en a Knop, we hebben ook een Tekstweergave met ID R.id.tv_login. Dus doen we gewoon een controle om te bevestigen dat de Tekstweergave is zichtbaar voor de gebruiker. 

Nu kunt u de test opnieuw uitvoeren!

Uw tests moeten slagen als u alle stappen correct hebt uitgevoerd. 

Dit is wat er gebeurde tijdens het uitvoeren van onze tests: 

  1. Lanceerde de Hoofdactiviteit de ... gebruiken activityRule veld-.
  2. Geverifieerd als de Log in knop (R.id.btn_login) was zichtbaar (is weergegeven()) voor de gebruiker.
  3. Gesimuleerde klikactie (Klik()) op die knop.
  4. Geverifieerd als de LoginActivity werd getoond aan de gebruiker door te controleren of a Tekstweergave met id R.id.tv_login in de LoginActivity is zichtbaar.

Je kunt altijd naar het Espresso-spiekbriefje gaan om de verschillende view-matchers te bekijken, acties te bekijken en beweringen te bekijken die beschikbaar zijn. 

6. Test de LoginActivity Scherm

Dit is onze LoginActivity.kt:

import android.os.Bundle import android.support.v7.app.AppCompatActivity import android.widget.Button import android.widget.EditText import android.widget.TextView class LoginActivity: AppCompatActivity () private lateinit var gebruikersnaam Bewerkingstekst: EditText private lateinit var loginTitleTextView: TextView private lateinit var password EditText: EditText private lateinit var submitButton: Button override fun onCreate (savedInstanceState: Bundle?) super.onCreate (savedInstanceState) setContentView (R.layout.activity_login) gebruikersnaamEditText = findViewById (R.id.et_username) wachtwoordEditText = findViewById (R.id.et_password) submitButton = findViewById (R.id.btn_submit) loginTitleTextView = findViewById (R.id.tv_login) submitButton.setOnClickListener if (usernameEditText.text.toString () == "chike" && wachtwoordEditText. text.toString () == "password") loginTitleTextView.text = "Success" else loginTitleTextView.text = "Failure"

In de bovenstaande code, als de ingevoerde gebruikersnaam "chike" is en het wachtwoord "wachtwoord" is, dan is inloggen geslaagd. Voor elke andere invoer is het een fout. Laten we nu een espressotoets hiervoor schrijven!

Ga naar LoginActivity.kt, verplaats de cursor naar de LoginActivity naam en druk op Shift-Control-T. kiezen Maak een nieuwe test ...  in het pop-upmenu. Volg hetzelfde proces als voor ons MainActivity.kt, en klik op de OK knop. 

import android.support.test.espresso.Espresso import android.support.test.espresso.Espresso.onView import android.support.test.espresso.action.ViewActions import android.support.test.espresso.assertion.ViewAssertions.matches import android .support.test.espresso.matcher.ViewMatchers.withId import android.support.test.espresso.matcher.ViewMatchers.withText import android.support.test.rule.ActivityTestRule import android.support.test.runner.AndroidJUnit4 import org.junit .Rule import org.junit.Test import org.junit.runner.RunWith @RunWith (AndroidJUnit4 :: class) class LoginActivityTest @Rule @JvmField var activityRule = ActivityTestRule(LoginActivity :: class.java) private val gebruikersnaam = "chike" private val wachtwoord = "wachtwoord" @Test fun clickLoginButton_opensLoginUi () onView (withId (R.id.et_username)). Perform (ViewActions.typeText (gebruikersnaam)) onView (withId (R.id.et_password)). uitvoeren (ViewActions.typeText (wachtwoord)) onView (withId (R.id.btn_submit)). uitvoeren (ViewActions.scrollTo (), ViewActions.click ()) Espresso.onView (withId (R.id.tv_login)) .check (matches (withText ("Success")))

Deze testklasse lijkt erg op onze eerste. Als we de test uitvoeren, onze LoginActivity scherm wordt geopend. De gebruikersnaam en het wachtwoord zijn ingetypt in de R.id.et_username en R.id.et_password velden respectievelijk. Vervolgens klikt Espresso op de voorleggen knop (R.id.btn_submit). Het zal wachten tot a Uitzicht met id R.id.tv_login is te vinden met tekst lezen Succes

7. Test a RecyclerView

RecyclerViewActions is de klasse die een reeks API's blootstelt om op a te werken RecyclerView. RecyclerViewActions maakt deel uit van een afzonderlijk artefact binnen de espresso-contrib artefact, waaraan ook moet worden toegevoegd build.gradle:

androidTestImplementation 'com.android.support.test.espresso: espresso-contrib: 3.0.2' 

Merk op dat dit artefact ook de API bevat voor gebruikersinterface die de navigatielade doortest DrawerActions en DrawerMatchers

@RunWith (AndroidJUnit4 :: class) class MyListActivityTest // ... @Test fun clickItem () onView (withId (R.id.rv)) .perform (RecyclerViewActions .actionOnItemAtPosition(0, ViewActions.click ()))

Klik op een item op een willekeurige positie in een RecyclerView, we roepen aan actionOnItemAtPosition (). We moeten het een soort item geven. In ons geval is het item het ViewHolder klas binnen onze RandomAdapter. Deze methode omvat ook twee parameters; de eerste is de positie, en de tweede is de actie (ViewActions.click ()). 

anders RecyclerViewActions die kunnen worden uitgevoerd zijn:

  • actionOnHolderItem (): voert een ViewAction op een weergave gekoppeld door viewHolderMatcher. Dit stelt ons in staat om het te matchen met wat er in de ViewHolder in plaats van de positie. 
  • scrollToPosition (): geeft a terug ViewAction welke schuift RecyclerView naar een positie.

Vervolgens (zodra het scherm "Notitie toevoegen" geopend is), zullen we onze notitekst invoeren en de notitie opslaan. We hoeven niet te wachten tot het nieuwe scherm wordt geopend - Espresso zal dit automatisch voor ons doen. Het wacht tot een weergave met het ID R.id.add_note_title kan gevonden worden.

8. Testintenties

Espresso maakt gebruik van een ander genoemd artefact espresso-intenties voor het testen van intenties. Dit artefact is gewoon een uitbreiding op Espresso die zich richt op de validatie en spot van intenties. Laten we een voorbeeld bekijken.

Eerst moeten we de espresso-intenties bibliotheek in ons project. 

androidTestImplementation 'com.android.support.test.espresso: espresso-intents: 3.0.2'
import android.support.test.espresso.intent.rule.IntentsTestRule import android.support.test.runner.AndroidJUnit4 import org.junit.Rule import org.junit.runner.RunWith @RunWith (AndroidJUnit4 :: class) class PickContactActivityTest @ Regel @JvmField var intentRule = IntentsTestRule(PickContactActivity :: class.java)

IntentsTestRule strekt ActivityTestRule, dus ze hebben allebei hetzelfde gedrag. Dit is wat de doc zegt:

Deze klasse is een extensie van ActivityTestRule, die Espresso-Intents initialiseert vóór elke geannoteerde test Test en geeft Espresso-Intents vrij na elke testrun. De activiteit wordt na elke test beëindigd en deze regel kan op dezelfde manier worden gebruikt als ActivityTestRule.

Het belangrijkste onderscheidende kenmerk is dat het extra functionaliteiten heeft om te testen startActivity () en startActivityForResult () met moppen en stompjes. 

We gaan nu een scenario testen waarbij een gebruiker op een knop klikt (R.id.btn_select_contact) op het scherm om een ​​contact te kiezen uit de contactenlijst van de telefoon. 

// ... @Test fun stubPick () var result = Instrumentation.ActivityResult (Activity.RESULT_OK, Intent (null, ContactsContract.Contacts.CONTENT_URI)) intending (hasAction (Intent.ACTION_PICK)) .responsWith (result) onView (withId ( R.id.btn_select_contact)). Uitvoeren (klik ()) bedoeld (allOf (toPackage ("com.google.android.contacts"), hasAction (Intent.ACTION_PICK), hasData (ContactsContract.Contacts.CONTENT_URI))) // ...

Hier gebruiken we plan () van de espresso-intenties bibliotheek om een ​​stub met een schijnreactie op te zetten voor onze ACTION_PICK verzoek. Dit is wat er gebeurt  PickContactActivity.kt wanneer de gebruiker op de knop met id klikt R.id.btn_select_contact om een ​​contact te kiezen.

fun pickContact (v: View) val i = Intent (Intent.ACTION_PICK, ContactsContract.Contacts.CONTENT_URI) startActivityForResult (i, PICK_REQUEST)

plan () neemt in een Matcher die overeenkomt met intenties waarvoor een reactie moet worden gegeven. Met andere woorden, de Matcher geeft aan in welk verzoek je geïnteresseerd bent in stubbing. In ons eigen geval maken we gebruik van hasAction () (een hulpmethode in IntentMatchers) om onze te vinden ACTION_PICK verzoek. We roepen vervolgens aan antwoorden met(), waarvoor het resultaat wordt ingesteld onActivityResult (). In ons geval heeft het resultaat Activity.RESULT_OK, het simuleren van de gebruiker die een contact uit de lijst selecteert. 

Vervolgens simuleren we door te klikken op de knop Selecteer contact, die oproept startActivityForResult (). Merk op dat onze stub de schijnreactie heeft verzonden naar onActivityResult ()

Ten slotte gebruiken we de beoogde () helper methode om eenvoudigweg te valideren dat de oproepen naar startActivity () en startActivityForResult () werden gemaakt met de juiste informatie. 

Conclusie

In deze zelfstudie hebt u geleerd hoe u eenvoudig het Espresso-testraamwerk in uw Android Studio-project kunt gebruiken om uw testworkflow te automatiseren. 

Ik raad ten zeerste aan om de officiële documentatie te bekijken voor meer informatie over het schrijven van UI-tests met Espresso.