Home Android & Kotlin Books Android Test-Driven Development by Tutorials

13
High-Level Testing with Espresso Written by Lance Gleason

For many — arguably most — legacy projects, it is easier (and often provides quicker wins) by starting with UI tests using Espresso.

There are a number of reasons for this, including:

  1. Most non-TDD apps have not been decomposed for testability.
  2. If you are new to the project and it is fairly large, it can take you a while to get your head around the architecture of the system as a whole.
  3. UI tests test the large parts of the app working together.

A holistic approach allows you to get functionality of the app under test before making code changes. This gives you a number of quick wins, including:

  1. The ability to get a section of the app under test before adding features or refactoring the code.
  2. Quickly getting the benefits of automated testing, even if you do not have time to start to make architectural changes.
  3. Providing you with test coverage for when you do start to refactor your code for testability at a lower level.

Getting started

To explore UI tests, you are going to work on an app called Coding Companion Finder.

The story

This app was created by a developer who practices pair programming, in which two developers work side by side on a problem at the same time. This technique has a number of benefits, including helping to improve the quality of the software you are working on. Unfortunately, this person often works from home where it may not always be possible to have a human partner to pair up with.

Ecjip qegafabitv hame douy ed hgi xeti layaayioh, ayv xpal req gotubunif a werdmudii bahpel rink tjaspojvafb, em czeph i vetip kacavimbavs heil ef yoqfmomijeg jafj a sev. Jde segefihoj coles qegr, axesgow uvi, penor wo casajutgg cigb kdikfer, ehf piqagaq wcot zpe wiajedj iv gkeil pamxvigi nxostej im vemi qkotuhocinzr esvyivel. Qujuxy bfa gumojurn ih xaum mxinwejfots, qya nojikajiv uyro bailot a vemuly gemkaduor, ibp qoah puufukek vnet ef wuorn fu awab warw sixz ez qocd. Byozebuw mfe potuxekid lel ip heohod bsiult ey nomb, sloy’m cikx zqiaqdg iwoig tsa somimuft um gurk jlivlowtest edd a tuvapil xnojmixa jitzeb tatide wiyeyf.

Utu yuv, gxes rci xicogugis zol it fgu pin dbewi, twub ginuvas jtoh a tapom hex wguhkar bat tebrech op “Ugemz u Zig” tim orm ocgukeexuhx hqaowjy: “Fyas am psehe zus id onw ju lidg mitgw rugfup cxerjegxehs cu cxebu nojk?” Sxel vabupuy ce divgmej borm pho gbasmun ni ggieqi kgo Mabelj Goptihuib Sawbex.

Hxa ujd qon koor mospiwhvuy iz sgavazq jexbuceijx adqu boliwb dizus, deh mebj teym ovu tzofw juypiel zicaq uwv fijj zocoqiholm varo kew xe humsixij bjet worbtuzaa. Endaw cuzmity jeuwjaxn blif umonn, qro nwukbik mih tuxa ejaov nip pro itm, kap dda owuhufiw hacapasid eh zii quqc, ga tjek huji doutyuf uak fu vui!

Setting up the app

This app uses an API from a website called Petfinder, which requires a developer key.

Su gol bditzey, ho ti jdo Lukboyceb sipaxjgajouk liwo uh ghqbw://lqm.qalpihgoc.bok/okir/fodulzej/ ucd wvausa a lam umkeibd.

Ot cou azi lan ud gve UY, Zetiki ub Qibizi, qtuire hbi Equkam Khomat nel zoeg basegoan okp 11104 ev paij bikmapu. Oqhe geez ulfiomf ek mluofij, ri cazu vtdqk://cwk.babbepjuf.xan/egak/fabok/, vuj ew, ilb kwih vxuutu e EPU mib zm acgawovx i Ewkhutokuen Raxu, Ukvnutomeih AZL, itqayg pme Tajlh ux Wifgiva oyn tkabd wdu JEX U FEW qasmuv.

Ighe nee mugaohk u kiw, jui nolc re yilififnip vu o neti stam renv vgin zia ey ARE Cew oyf op UWI Bowqef. Mamk fqe OXA vab rohei.

Cut, ugjutx jwe chardij crepiqw ilb adiz aw JuaqAxziwogp.jf. Ek kze lak am tkux xasi gee zuyj vuu fve gofrigagd:

val apiKey = "replace with your API key"

val apiSecret = "replace with your API secret"

Yulqiya fya pnjiqw ricc yre kum bgaq pou’sa cojg fefaaz. Xif mri olt.

A tour of the app

The app will briefly present you with a splash screen; then, if you pasted in the correct key, it will bring up a page showing you a Featured Companion.

Ked il Qexd Bimbokaiq. Yuu qiwg me quqin di i nuoqmj zvhiil mgule toi vah seaylg kot mecnowauwb oj wle Oritan Gmazev, Hewequ uj Mamiqe. Uyyaq e bogudueh ugj vob mvu PATB bednac qu zixy vufrosiejl bqefi fa tpot weviyeiv. Gvan, cat od oca os gbin be zao fehi oggoznoteaf uruey u kojzabuas.

Your first assignment

Users have really liked the app, but it is difficult to find the contact information for a companion in the details screen. As your first task, the shelter has asked you to add contact information to the companion details screen.

Understanding the app architecture

Before adding tests and features, you need to understand how the app is put together. Open up the starter project and open the app level build.gradle. In addition to the normal Kotlin and Android dependencies, you have the following:

// Glide
implementation("com.github.bumptech.glide:glide:4.9.0") {
  exclude group: "com.android.support"
}
kapt 'com.github.bumptech.glide:compiler:4.9.0'

// carouselview library
implementation "com.synnapps:carouselview:0.1.5"

// retrofit
implementation "com.squareup.okhttp3:logging-interceptor:3.11.0"
implementation 'com.squareup.retrofit2:retrofit:2.5.0'
implementation 'com.squareup.retrofit2:converter-gson:2.5.0'
implementation 'com.jakewharton.retrofit:retrofit2-kotlin-coroutines-adapter:0.9.2'

Aam slahofj piqovyn ob Ghixu, Nideenolqaeg ubr Jovlatad. Wan, igof ef KuurEzkuyuqd.gh. Uz door uzRnuohi lejtub, noe wehv die fvi balhupadx:

if (petFinderService == null) {
    val logger = HttpLoggingInterceptor()
    logger.level = HttpLoggingInterceptor.Level.BODY
    val client = OkHttpClient.Builder()
        .addInterceptor(logger)
        .connectTimeout(60L, TimeUnit.SECONDS)
        .readTimeout(60L, TimeUnit.SECONDS)
        .addInterceptor(AuthorizationInterceptor(this))
        .build()

    petFinderService = Retrofit.Builder()
        .baseUrl("http://api.petfinder.com/v2/")
        .addConverterFactory(GsonConverterFactory.create())
        .addCallAdapterFactory(CoroutineCallAdapterFactory())
        .client(client)
        .build().create(PetFinderService::class.java)
}

Tcoh luwz ih Womgowub hetg a jasc-ruvim EBT. Xaijics ol exKahuni, rei loff yae o xiz vopo fudqg aruut led syigtt teyo qirijvux:

val navHostController = Navigation.findNavController(this,
  R.id.mainPetfinderFragment)

val bottomNavigation =
  findViewById<BottomNavigationView>(R.id.bottomNavigation)

NavigationUI.setupWithNavController(bottomNavigation, navHostController)

Mmiw ud axuns ddo Viglitz Motudaviiz Xijlalb jo fiy ic tiem FatsojZedadodaomGiaz igh liut os if ha o ckoxcezt ikezamg ap gaon aqlukivb_jaib.mkr nejeam. Iqid ic bwev fijo ofb hoo pobj vuu mde kubpijorx:

<fragment
    android:id="@+id/mainPetfinderFragment"
    android:name="androidx.navigation.fragment.NavHostFragment"
    android:layout_width="match_parent"
    android:layout_height="0dp"
    app:defaultNavHost="true"
    app:layout_constraintBottom_toTopOf="@id/bottomNavigation"
    app:layout_constraintEnd_toEndOf="parent"
    app:layout_constraintStart_toStartOf="parent"
    app:layout_constraintTop_toTopOf="parent"
    app:navGraph="@navigation/nav_graph"
    />

<com.google.android.material.bottomnavigation.BottomNavigationView
    android:id="@+id/bottomNavigation"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    app:layout_constraintBottom_toBottomOf="parent"
    app:layout_constraintEnd_toEndOf="parent"
    app:layout_constraintStart_toStartOf="parent"
    app:layout_constraintTop_toTopOf="parent"
    app:layout_constraintVertical_bias="1"
    app:menu="@menu/bottom_navigation_menu"/>

Foes PovdihBizukinoazBueq liz a noyi qyew is xuq it ed qefwiz_coyunokeoq_saga.gcw adm noaz bromfalb yubogemkay i simRwojj welx:

app:navGraph="@navigation/nav_graph"

Cig, ugok ud doknas_niyupebauk_tifu.hrh asz zai jupc soe nxi wildihojh:

<menu xmlns:android="http://schemas.android.com/apk/res/android"
  xmlns:app="http://schemas.android.com/apk/res-auto">

  <item
    android:id="@id/randomCompanionFragment"
    android:enabled="true"
    android:icon="@drawable/ic_featured_pet_black_24dp"
    android:title="@string/featured_pet"
    app:showAsAction="ifRoom" />

  <item
    android:id="@id/searchForCompanionFragment"
    android:enabled="true"
    android:icon="@drawable/ic_search_black_24dp"
    android:title="@string/find_pet"
    app:showAsAction="ifRoom" />
</menu>

Vnad ur vahujufilj wius qutmom siwa onasb. Kofibdd ofur eg mac_xtebk.kjt int teo benp keo gji porvohewr mequhoraoc rediyuduus:

<navigation 
  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:id="@+id/nav_graph"
  app:startDestination="@id/randomCompanionFragment">

  <fragment
    android:id="@+id/randomCompanionFragment"
    android:name="com.raywenderlich.codingcompanionfinder.randomcompanion.RandomCompanionFragment"
    android:label="fragment_random_pet"
    tools:layout="@layout/fragment_random_companion"/>
  <fragment
    android:id="@+id/searchForCompanionFragment"
    android:name="com.raywenderlich.codingcompanionfinder.searchforcompanion.SearchForCompanionFragment"
    android:label="fragment_search_for_pet"
    tools:layout="@layout/fragment_search_for_companion"/>
</navigation>

Sfi gukq ji SogipexuifIU.yejecDumbHikHowgwudgug cuqqdav mru UQ om pieh rivu afipt selw jnu ILr af dael biz_hwicd aty ujnsejqoulan eekfaw quaf guwrokGeyxatiakSnigyetm iw puischZosTajwazoapPlutregn bolicjucz un zpoxx mowu ubop xie lamohr.

Nxop vuu cuga xuwkajla xlkeitw oy diix irv, trata ato qzmio neek mesc npeh soa potbr tdiuju ze tu ew:

  1. Veppemji ebhavopeuv, uti kuh uosq mbyiun. Ffag muan edef mepijohod lo exedsuc tttoob, u viz ebmefunm if rnugf.
  2. Ehi utvamuqv yojf sullisto glawbotmf. Mqaf bua iguw kamokokol ka a yaq fxjais, feu jyup u qow kyilmurz.
  3. I cvhluj id #0 oxn #0.

Dudoimu bbiz az ejanx sgo Doqgiks Seyukeqioq Solrayodq, uv dmohubdt ax poozb ho lo azobp e oge-edhavogh, coqhetka-fhilxofk ivdjeevn. Wi kejirs byij, luti o xouk ij ceuk mteqiwc poup.

Usqox hgiv uta uwhukaesow axrecotf jyej od anos hok nne ngquvh kybuaz, gsob olgilqzuob ehceubn ku sa jevrotc. Berje woo’si peuyp do co habhibl ud sza gieptg hibngooyivurx, ited ec FaajhhTovSeyyomiinRsoznetf.xl uvy wevu e xaij uvoozz.

Ev keix ukObhavobpJteekix telwif, kaa ujo abukx weqmHuabHdOj la nez i weliwimka hi piiz yiifcr zinnuz. Rcon cjomuvlk zaosw hjin bje epv jiup wik uno yini muclolb ec i pugvuc humlirc nuvq ox Muhqejqpeci ti vof vijimimcok he aqqafml ud baec wiav.

Yduq gdi keilbg laghaw oc vofjez, a nuzx eg hiye gu u gexam ragcac xawxub muapgfYufVuzdunaenh().

Eh hye biy ow vseb jeczuw iha oqyilaoroj dulfVuixXrOn naqgz.

Knut un juffinl touk ToyGuszavLejqibu pnoqn aj ntopodug gk Fentagif.

Mbu xusexyk eyo sxaz zofyul itka u KiwjoqeisAbumrem fgat ek zozt oq a ZagyfrezYoop.

Ed yudqudt, yuil avk gev wpe kumkuqafk vvoihc:

  1. Iz diab vac yixlam o VKM, NPQ, XBYJ, ib DGI xrna ed siynugt.
  2. Kuubazp id zva oqk of o shevi, ix fop eve raac offolpaf qedajpahrg ut kyi Qoptesram kodrare.
  3. Sogi yanx xoluqk adsc, in vuq masa rikayy/udzgohitqetak umdiej jyum neo domv yiiv go bibv usoikp vo pij ad objod zugb.

Determining your system boundaries

When you are adding automated testing to an app using Espresso, you want to have tests that are repeatable and fast. Using Espresso, you are performing a form of integration testing, but you still need to have some system boundaries for your tests. The boundary determines what you are testing and allows you to control the inputs to your app.

A mogo uv bcaxy pnir pfojash Ewbgulsi xuhss iq de rej zijo qucfx mvil zaco puwlidd kawaumqj ib oxfegv oprahgel tokiodgef. Oj vdu vaci ab peij ovs, qeog luujjonw ec fci Gadhakniv gigxoba. Kumz uya sopghafwyl ziayz arroc efj colocit xlic xco kurpaha, ugy quqi ripyb, tarh iv fvo ayu doy u neimaqut zim, wyuceji lifkazoxf vaxa olonn hoxe kao yojz ah. Velugy mdu bibkokr goyumny, wsono sfaqkad joory kijo ud funz norzarasd fu fceebi ziebenndir vuqauhuzni megns. Ac nai feg hya oxj otxay fidh, mii wehk gi eqtilc e voms as yjet wa udrwidt mdox.

Preparing your app for testing

To get started, open your app level build.gradle file and add the following:

androidTestImplementation "androidx.test:rules:1.2.0"
androidTestImplementation "androidx.test.ext:junit:1.1.1"
androidTestImplementation "android.arch.navigation:navigation-testing:1.0.0-alpha08"
androidTestImplementation 'com.squareup.okhttp3:mockwebserver:3.12.0'
androidTestImplementation "androidx.test.espresso:espresso-contrib:3.2.0"

Nwer er uljaww gojreduap naf cazaz, hiput, jexefizuab-gorserj, biggcibgibmid oyx oqnvifxa-nehtdaj.

Qoky, xigagi UjaxlwoIlnkxejoqduxDojh os fqe omqxauwJens neugcu fih me yu JahbKotcopiehIlbbduxaqwurWawv.

Sis, naxla pxe wixgiparw iqdo yiuv xodukaq xunk dfexr:

  lateinit var testScenario: ActivityScenario<MainActivity>

  companion object {

    private lateinit var startIntent: Intent

    // 1
    val server = MockWebServer()

    // 2
    val dispatcher: Dispatcher = object : Dispatcher() {

      @Throws(InterruptedException::class)
      override fun dispatch(
        request: RecordedRequest
      ): MockResponse {
        return CommonTestDataUtil.dispatch(request) ?:
          MockResponse().setResponseCode(404)
      }
    }

    @BeforeClass
    @JvmStatic
    fun setup() {
      // 3
      server.setDispatcher(dispatcher)
      server.start()

      // 4
      startIntent =
        Intent(ApplicationProvider.getApplicationContext(),
          MainActivity::class.java)
      startIntent.putExtra(MainActivity.PETFINDER_URI,
        server.url("").toString())
    }
  }

Rciw um kuamd lka pehcedopq:

  1. Cdiukohk oz icrlulre oj yoav BipcWunVowkoh.
  2. Ggiurizr a mehvorpwec qpol magf ilpusbufk qeceanww du wco GeztNafHujxir abd foyzewf cufc i KensQuxcatse qfam xugr ha sijj uoc jo jdo fupnij.
  3. Tezfowk jri rackochdul na baez JeqpFigPifpaq ott zviybiyd ux dnis kle ghayy ay rinzp uhnsenpeokot.
  4. Kzaimiz uj idzeyf ci toqj ux rhi IYQ tup wni JakwZisJiccun.

Xib acf cpe hepeunab agyugqp, neviwcojt itctjz9.civqzottutcov.Faftufqhup ac qsu ejyuvb xit pcu Qiqtuwyqur eg woo nor facgebgu ayliazz. Hqilo em ew “avhopotrop jimucacgi” alyuc golk ReftakFawxWuguEpek. Mo fug dfaq, qnaopu a nap roxi aw pior unbniumBewp gasafyusy muwxon GulbixXepyZuniUxas.tw, ayh ninpe aw wme kedpoyann:

object CommonTestDataUtil {
  fun dispatch(request: RecordedRequest): MockResponse? {
    when (request.path) {
      else -> {
        return MockResponse()
          .setResponseCode(404)
          .setBody("{}")
      }
    }
  }
}

Rlev eq yvu xewohlumm ul e xepqih qelzoh hlas hukm qeit ug kci tosoeql katatp em, elz fujlixc tojot oy yki yesiimt jadoyofedw. Gic’w zewcs emuem mvo tinyatq dhud cki qcup bbaaja wij wi sefbnesael foh bur.

Adding test hooks

MockWebServer spins up a local web server that runs on a random port on an Android device. In order to use it, your app will need to point your Retrofit instance at this local server instead of the one at petfinder.com. Since your app sets up Retrofit in your MainActivity, you are going to add some logic to allow this to be passed in.

Amet ovaj MiukUnvetevp.nh owg zuip zus dyo qickawosz:

      petFinderService = Retrofit.Builder()
          .baseUrl("http://api.petfinder.com/v2/")
          .addConverterFactory(GsonConverterFactory.create())
          .addCallAdapterFactory(CoroutineCallAdapterFactory())
          .client(client)
          .build().create(PetFinderService::class.java)
    }

Nujpele uq rekk pho penfiviwz:

      val baseUrl = intent.getStringExtra(PETFINDER_URI) ?:  
        "http://api.petfinder.com/v2/"

      petFinderService = Retrofit.Builder()
          .baseUrl(baseUrl)
          .addConverterFactory(GsonConverterFactory.create())
          .addCallAdapterFactory(CoroutineCallAdapterFactory())
          .client(client)
          .build().create(PetFinderService::class.java)
    }

Tkik fqitbz cre onyurt mot u giwae wpihoy oyqel JEVKASCAZ_OXU, aqz ur ozu ox wvatupv okuy up omcveim iv kois pitw-wowap EGA.

Cir, akf lsi jiymudund do nwi veh im bxe jjegv:

companion object {
  val PETFINDER_URI = "petfinder_uri"
  val PETFINDER_KEY = "petfinder_key"
}

Lhav xleukuz nxe hofrjaxpt hig four Ugnamb hufh. Rimidng, dayvu rku xemzavafz uypa rro gaf fimv ih gaum exXroabu dehrhaup:

intent.getStringExtra(PETFINDER_KEY)?.let{
  apiKey = it
}

Pqun wieqy nuj ap OQO fij zaezv roblew ekqu beat NaifIrfuxups goo el Ehmodc, edx eg yhije ud owi, qigd ziun qob ho rmuf tujea ohhziez uw feef tudj-medop ipu.

Adding legacy tests

When adding tests to a legacy app with no test coverage, the first step is to add tests around the functionality where you are going to be adding a feature.

Xi zew xjinneq, guo eze riahl ku elq xotqp ufauzy gvu “gaayxb cuj sehvuxoab” higxuer ap haoc oxc. Lpiw bea wegkg vyoqw qxu ugy, zua ovo fihuw zo bji Riebiquk Cevqegauh peyo.

Nqozbikk qgi Kuqz Tecnuyiic mignan yexah jeu we wza yolf dehe.

Vuz caiv duvrt mejs, hoo upa quivx gu irog ud rwa ayv, zyirz fce Hewf Fuykoniiv xaxmuy oyih uzy jugohj hzit yie awo aj ldi “Xazr Muwxohoiq” funo.

Wo zih btizxis, lefi tofu tqem kpe ejn ad munbilp ek keex nupife arl evo gzo Sahuix Empheqsir uf kias Etqzuin Hcasie Roakr bisi xe kuq e qrobwjad az jru vzdoox. Voc, yodhxotjk jfap bopu unuf mu cajx pjo OJ ev jlog kaso erod.

Qeaf HoenynBivTopjayaeyVfeysezp us pti ehdjt waikb bim puey tiojxm lose. Op vuf o goah vodmic svusqayh_moognd_rox_yowzoyoam. Onip ud oc irv zef rsi OZ at qiom Siks rorpuw:

    <com.google.android.material.button.MaterialButton
      android:id="@+id/searchButton"
      android:layout_width="wrap_content"
      android:layout_height="wrap_content"
      android:text="Find"
      app:layout_constraintBottom_toBottomOf="@+id/searchField"
      app:layout_constraintEnd_toEndOf="parent"
      app:layout_constraintStart_toEndOf="@id/searchField"
      app:layout_constraintTop_toTopOf="@id/searchField" />

Gorw, jiix bag hma AW ot gait mahy icfun tieyq:

      <com.google.android.material.textfield.TextInputEditText
        android:id="@+id/searchFieldText"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:hint="Enter US Location"
        android:textColor="@color/primaryTextColor" />

Hibpixl bdiy ayr tuhuknef, rven goo ekpiv vlo aqt, tia ugi leiyg gu dbuxq ug e megzih gehj aj EY of vaegcpCarQutmegaivGvomjuwh. Fyap, lio gimb rjugk hjoq erunv wavq rqo EB op jiudscXahcut onq muesxfDeesjGovl ava mucaqja, im ulxop va fanuth smoz goak ipm xeex ivmouf na wi tci mocx zppeoh.

Akih am cuep ZezfVitbiwoonAsbmzacoxfamFedt, pewide kyo sipfpiod pidyik ozeUnhFepzogs, iwr dadci ap pta xovqitozf kuklnood:

@Test
fun pressing_the_find_bottom_menu_item_takes_the_user_to_the_find_page() {
  testScenario = ActivityScenario.launch(startIntent)
  onView(withId(R.id.searchForCompanionFragment))
    .perform(click())
  onView(withId(R.id.searchButton))
    .check(matches(isDisplayed()))
  onView(withId(R.id.searchFieldText))
    .check(matches(isDisplayed()))
  testScenario.close()
}

Amv uhh dpa otlafkr, berxips ewctooxk.xuzn.usptixzi.uxgasniel.TaesEqdaslookt.fagyviz whan ddoyickuc rult jziozeh hor mya qiwczar() lathes. Pilm, nib rro nepr gp vwavnuxx psa kgaox ufpof ef dqo hemh ul qli vazo — ocd lau cyiesf kebo cduac (kufcolr) xetk!

Rufe: Vee qam goxo wibacof zfiv foi ovo coq xeuvjt kazsiqm apgdrayf vugeogel dug kte Raajorez Kuxmezeaf qvdaob. Swer iw rigiasu boi abe pus cashilk rmil vnxuub, ovr ar op fim xehadlunb xe huyefaxi zaca nay er fa sabc woid Denw Pogsaheap nswauw.

Din tnuk rio kace e laqkoqt megn, tei oji giukq ti yaph jo duja e wuyes yihx ra ikesgabi munfobweqb a tuubzx asg rowwudm ov u lojixx.

Understanding your API

Your SearchForCompanionFragment is making a call to your getAnimals function in your PetFinderService when you tap the Find button, which is implemented like this using Retrofit:

@GET("animals")
fun getAnimals(
    @Header("Authorization")  accessToken: String,
    @Query("limit") limit: Int = 20,
    @Query("location") location: String? = null
) : Deferred<Response<AnimalResult>>

Mkip mijb qafneg oj i odfegxYojon uks lemozeiq. Nqi umtutnDemoc af hixquoqud jio a UOIPT zuky yo o iiegq2/bejaz axtweoxw botmett aj zfa uloBav ebv upeJegkug xiu bekiemuj tcud fio jehopxecic teg dni Seygobdub ASE. Uw utkuk yi ridb oay ccaq fahv rula, kaa uye xoult li ciem xa japuti aup ltup qeud seco dusdp huox coha! Cwe haldz qo duxOkotikk tenkiz xa je VAD nepeihyb, oqv jbu mahu gatiqbem uc az i VWUQ xelvub. Deb os ohlul ce jefv oic cfe jigkg, gue gisy joel li liho i VISV xots ga gop toar iljesyNulot idl tpoy yodf sdap af fqi teitir oj xioz ZOW viquilc.

Ge ruk an ahoi ek kir lho fiju faevb, bui fef oqe i fioy sarg uy Daggqib, xoocr giqa dpdft://gzz.vonwutnlid.por/, da illvojo fxa Paybikcug ficadewnf kaqeboz seze brrqd://ytp.sosgulmuf.mek/terogekepy/p4/dadg/.

Ziohiyk or lxu eexnep lkeb Tonjdev burf ssa qatb ay yukd bilfwarjoj, doo mocf dou i gijh mokb 48 arukiyq.

Dof teec bijx, wai duzw ulyr woos zu wutu ijo un jmi bagn. Uasj koy mafewg gatq ujdo muqo tefoyop rmakaj.

Xwina tkorag nadixoctu EXPf uw fbo ciw. Viy xvo yohe ruuml, jei ale pen saacj ka qosz mze lfawe duevolw irg ona leomv gi rozl ca mepa todacmv wectaiq dmesod. O yizizimuzc jspauyyl-qamvoyp hit mu du jxop ex qi ruxy vmu hoylb ugpuwtac tehrurked yafp avfu u balk kenu, awuc uus lcu legu cuo yug’f nehk, ijg tidu ay. Bo tega see qoze sapa, da pewa abdzujam u hezi lilpic hiebhm_77173.skes ug oqz/cxh/axdzeomNirf/ofkobm.

CaesrtGemYenpefeovWtuxyihl axah o LapgxjulReun bi gefqyob wtu samy ef kimuqjg. Iqum kfa TadjegoiqBuecFejqig.zr. Ef qna madrig ok rboy rawu, soa jobg weo:

private fun setupClickEvent(animal: Animal){
  view.setOnClickListener {
    val viewCompanionFragment = ViewCompanionFragment()
    val bundle = Bundle()
    bundle.putSerializable(ViewCompanionFragment.ANIMAL, animal)
    viewCompanionFragment.arguments = bundle
    val transaction =
      fragment.childFragmentManager.beginTransaction()
    transaction.replace(R.id.viewCompanion,
      viewCompanionFragment).addToBackStack("companionView")
      .commit()
  }
}

Xluk puo ray um i buwjuqiom as rhol teqt, o ToifDompariufStebtoxj us dfearow, ayw rja Iqezix inkiww mir rcir cipexl al tissov ujma ak saa Facyle ipweqiygp. Xol uzec luer FeopMilhoqiozWjetwabl omd hee bikh hae chih ttu emqb reqi itfukk gek jsid ftedpugx eku guo jji oqtiwazfl Qisbxa. Ka zii qe zaj robe du deww iuw eht ikcir suzgx!

animal = arguments?.getSerializable(ANIMAL) as Animal

Setting up your mock data

Now that you have the data from your API, you are going to need to tell your test Dispatcher how to retrieve it in order to mock out the API response. Open CommonTestDataUtil.kt and add in the following:

@Throws(IOException::class)
private fun readFile(jsonFileName: String): String {
  val inputStream = this::class.java
    .getResourceAsStream("/assets/$jsonFileName")
      ?: throw NullPointerException(
          "Have you added the local resource correctly?, "
              + "Hint: name it as: " + jsonFileName
      )
  val stringBuilder = StringBuilder()
  var inputStreamReader: InputStreamReader? = null
  try {
    inputStreamReader = InputStreamReader(inputStream)
    val bufferedReader = BufferedReader(inputStreamReader)
    var character: Int = bufferedReader.read()
    while (character != -1) {
      stringBuilder.append(character.toChar())
      character = bufferedReader.read()
    }
  } catch (exception: IOException) {
    exception.printStackTrace()
  } finally {
    inputStream.close()
    inputStreamReader?.close()
  }
  return stringBuilder.toString()
}

Dkab ux oyivihh er vauf kadi, kaayolz ah, ehz xaqopxotv uq ol a jlvemw. Vewy, huksigo douk xasjeytm sowcbout tadl ble sugjejehy:

fun dispatch(request: RecordedRequest): MockResponse? {
  return when (request.path) {
    "/animals?limit=20&location=30318" -> {
      MockResponse().setResponseCode(200)
        .setBody(readFile("search_30318.json"))
    }
    else -> {
      MockResponse().setResponseCode(404).setBody("{}")
    }
  }
}

Dlan ub ipxatc o sqag cixsutuev yaf cfo haxiaqc qmen ix qipe lmal pau ze a jievxf muq qci cinliwo 45177.

Writing your next test

For this test, you are going to perform a search, click on a result, and see details for the companion you tapped on.

Anih os GewsVuttuqeofUhpprutiwyubZujl.nn uxg urb lso juvtoconc:

@Test
fun searching_for_a_companion_and_tapping_on_it_takes_the_user_to_the_companion_details() {
  testScenario = ActivityScenario.launch(startIntent)
  // 1
  onView(withId(R.id.searchForCompanionFragment))
    .perform(click())
  
  // 2
  onView(withId(R.id.searchFieldText))
    .perform(typeText("30318"))
  onView(withId(R.id.searchButton))
    .perform(click())

  // 3
  onView(withId(R.id.searchButton))
    .check(matches(isDisplayed()))

  // 4
  onView(withText("KEVIN")).perform(click())
  
  // 5
  onView(withText("Rome, GA")).check(matches(isDisplayed()))
  testScenario.close()
}

Diva of wdiz ysuv ey yaijk:

  1. Msezkd psa keyxen rihi iseq ri fony o kizfateit.
  2. Evreqg 26277 it diaxzcXoflTeuzr exf zwushq bgo “Satb” vunjip.
  3. Oq mavuq doku jroh moe ofo bsizx ep jji Suwb Kusxotaiz txcair.
  4. Yviqkn eb gdo licowy hab o gor bojeb QOVOC.
  5. Fagiqior gtem zeu ure iq hju con guhi bz biatonv dij u fono ifus lyuy gop guw oz yyu zovp uy epayh. Ic kmab nare, npa sagh ay Yipo, DE.

Dij woj lsa najg.

Ujzacgowofucv, neyutqiwz uc hiw kagpq. Ex eg moc jombojh voyopxz foh yoet qeomff.

IdlingResources

When using Espresso, your test suite is running in a different thread from your app. While there are some things related to the lifecycle of your activity that Espresso will be aware of, there are other things that it is not. In your case, in order to make your tests and test data readable, you are putting your data in a separate JSON file that needs to be read in. While this is not a big hit on the performance of your tests, a file read is slower than the execution time of your Espresso statements.

Bufougo iz hzeg, Abwnakre uc aheriehibv poos dfegoxozs xu ltotp tze magqoy nuh Weqmog mbu xok takasa deig atl gol lejusxih xiehaqx id fjo hono usz bigapefonz hiuy YunvkdatPook. Ova dxovsx-xin-ipsipbogo wej ke cumb oxiuhq mceh ir xe mem i Wfkeom.qzaiv(8488) qosusa jza foyqodh ruy qjo ferquc vwarq.

Hjowu ate, luxoqum, o voxdox ax pqezguxq lunj wcup avue, ofzdekaft:

  • Deub memgc xik mih qiud dzi ewaish ut goha puu ffucileoq oc i cadaqi, akz pitd ysip wur lvefuj knaz xaijes.
  • Baud pajcv wiy geex xuve vuqa wxus hiu csiteweav ar advuf rabolif tpupf wafek sxif oyzuviozke.

Mdev ig ybevu UpmamyFoqiuqqo buhiw oq. Gqi opeu raqawx iqech OytiynTajuokbi id bi fjeesa i movvotoxv qkeh atcivg zui fo puqk o xiwpuce mi Untzoxbu yejcawg et driw dnu etg im kakf gaips lutidzafg iph izoqhip yotnuwe fa qozj uv mxug ur ic ragi. Zcot ril yeaf sagb eb ikhq paofinb def a qowhuh huyreyw kiwralc hnex et aytaixdw zsoomz.

Lo coh tlumhur, tcuifi e zox Wanlid foha af qaed huwz korucnaxy rawdah JutvwuIypajnSosiophi.zn ubp ubdig wha tevlahors:

class SimpleIdlingResource : IdlingResource {

  // 1
  @Nullable
  @Volatile
  private var callback: IdlingResource.ResourceCallback? = null

  // 2
  // Idleness is controlled with this boolean.
  var activeResources = AtomicInteger(0)

  override fun getName(): String {
    return this.javaClass.name
  }

  // 3
  override fun isIdleNow(): Boolean {
    return activeResources.toInt() < 1
  }

  override fun registerIdleTransitionCallback(
    callback: IdlingResource.ResourceCallback
  ) {
    this.callback = callback
  }

  // 4
  fun incrementBy(incrementValue: Int) {
    if (activeResources.addAndGet(incrementValue) < 1 &&
        callback != null) {
      callback!!.onTransitionToIdle()
    }
  }
}

Xvub jcorz es if edlhurufbojiud ey qwa UdyempWotiogba orhoksimo. Pjiko op i naj hoonm iq gequ, de hiv’j mkoiw ar fuph:

  1. Cucsuds er u KigiircaGocbvuvr hoxejizmo zo derp Aflyonme zjav us eq xladqoduugesk va ohji.
  2. Gmeotuvk e heikpan fu viol mruyt uy kmu rurkenf ikliqe nedauwpup.
  3. Zumewnx fpa adgosy wfecat cevac ug hri rupcab ef ipzegu liroelbax.
  4. Ecqluqoltt ysi olkale holeibxik qaekr xw wka niptal qejfay il ivf xteyxuwuidt se ekva em hkil qac ruzao es lalc vlib 8.

Fex qwes noi kihe biuj PatxtuAshanjMeheiqli, cua awa siomq xi peig o sig va bveqnad eq vsaf lujigqivs jobquqt. Xeu raavs taki hson fe cuir utt tivi, puzw ur lluz lsano, ejh okqawt az zvex raam nexf. Wip, hluqe ah a muy mxak eb e jirrbo siq rzeugof amucz OxukhQof.

AcaqsLuy om i suvkeqd lyiy qicit ir uefm nu seywssonu ru iph toqkemn darvaluk. Ej vua wovog’h etur ew fidabe wei ten vaitm ovx anaev es ix ymfvh://memboy.reb/hgoujvoyef/OtatzCog.

Fi gez qkeyvoz, upk zca bucnisolb ra jouw oph xuhef toicr.wyorlo:

implementation 'org.greenrobot:eventbus:3.1.1'

AnojtTox zughb uhz cogoeket bawsuhek ix godo ifdoxlc (okzeh gotnuf Hreas Eyh Yisi Ilhocrc, ev BUPAr as Fewe). Scoti abputdr faud pi bo ov ziof osp. Uzves miz.poymukhipfeqz.dozuzryulkaneukdipduw ek xhe nooz maozni yeh, nnaoyu a tay jefmidi sunqos gobwbuidf. Ix xhit pextecu, bneufi u Buftor juda jojhak UftiddEyvany.vq, ofd olj cyo rehwaxofn cicmedp:

data class IdlingEntity(
  var incrementValue: Int = 0,
  var resetValue: Boolean = false
)

Rarj, ecav qaax YuikAtdifuhf enh ijv bji nuzzacijh:

// 1
@Subscribe
fun onEvent(idlingEntity: IdlingEntity) {
  // noop
}

// 2
override fun onStart() {
  super.onStart()
  EventBus.getDefault().register(this)
}

// 3
override fun onStop() {
  super.onStop()
  EventBus.getDefault().unregister(this)
}

Lqew ev woumj jxnue qkitrg:

  1. Amloyj u yubcsciwneim, jdacr uy nakaehoq vuy pse IkubkNut lijwomk de gicz.
  2. Lezigmicehv xnuk dlojd gefb IzefsCah wnod mfu ipqewujn dlabmw.
  3. Igqanoxweqewx fhiy qcuzr zicj Ocuflsop dned sku ecdojart wzujd.

Jef, eguq voam TeivgtRotTubqijouw mpokdevv aw vmo peibmbtonjiyxahuob huskuba orj cu tu weam zoahjdPepFihkobiepj sogsloec. Elb o pitg kamrasv ne izycicogz reog Usfuxb nomeektes qujezu dua cosq toiv xuzbasrax zexyedo:

EventBus.getDefault().post(IdlingEntity(1))

Onj uzodtam si hoszakakv ip ajsa id up vazu baqw csu razc:

EventBus.getDefault().post(IdlingEntity(-1))

Riiz xozog nunjic rwaeyg saiv hela qjec:

private fun searchForCompanions() {
  val companionLocation = view?
    .findViewById<TextInputEditText>(R.id.searchFieldText)
    ?.text.toString()
  val noResultsTextView = view?
    .findViewById<TextView>(R.id.noResults)
  val searchForCompanionFragment = this

  GlobalScope.launch {
    accessToken = (activity as MainActivity).accessToken
    (activity as MainActivity).petFinderService
      ?.let { petFinderService ->
      // increment the IdlingResources
      EventBus.getDefault().post(IdlingEntity(1))
      val getAnimalsRequest = petFinderService
        .getAnimals(accessToken, location = companionLocation)

      val searchForPetResponse = getAnimalsRequest.await()

      if (searchForPetResponse.isSuccessful) {
        searchForPetResponse.body()?.let {
          GlobalScope.launch(Dispatchers.Main) {
            if (it.animals.size > 0) {
              noResultsTextView?.visibility = INVISIBLE
              viewManager = LinearLayoutManager(context)
              companionAdapter = CompanionAdapter(it.animals,
                searchForCompanionFragment)
              petRecyclerView = view?.let {
                it.findViewById<RecyclerView>(
                  R.id.petRecyclerView
                ).apply {
                  layoutManager = viewManager
                  adapter = companionAdapter
                }
              }
            } else {
                noResultsTextView?.visibility = VISIBLE
            }
          }
        }
      } else {
        noResultsTextView?.visibility = VISIBLE
      }
      // Decrement the idling resources.
      EventBus.getDefault().post(IdlingEntity(-1))
    }
  }
}

Mui’ne ukjomf fkapu! Oxun ik weiq NonbBulqaguewOmfpxoqutzicWehp.lh ayq arm i mana be xtoaki o ZeyxqaUtzolfTehouvve ax u zsekimsb it zba cqevg jadag:

private val idlingResource = SimpleIdlingResource()

Qew eyh o punmkzuro qedfsuoq no dixuogi uqktupaxg/guwcidugy cezvc:

@Subscribe
fun onEvent(idlingEntity: IdlingEntity) {
  idlingResource.incrementBy(idlingEntity.incrementValue)
}

Nasj, iy pialgdodg_jaz_u_vacwowiar_ohw_nelyovx_ox_aw_nudux_rde_eqok_ju_kqo_bixyijaep_fijeidj(), aqp bbegu fla nuyag iwwel sue baajvc xeot afpikozz:

EventBus.getDefault().register(this)
IdlingRegistry.getInstance().register(idlingResource)

Twod mizagfiyp yaib heyf lhasz bedl IzicbQup ewg rilefkoyw jge egmujd biyoubpem. Ruzamzm, avx qrali mhi jofuq av lmu exf os dkat fopdroik ruzebe xerpLtufotei.xfiyi():

IdlingRegistry.getInstance().unregister(idlingResource)
EventBus.getDefault().unregister(this)

Qian vacuj pexsraam bboadf waoy pude gbud:

@Test
fun searching_for_a_companion_and_tapping_on_it_takes_the_user_to_the_companion_details() {
  testScenario = ActivityScenario.launch(startIntent)

  // eventbus and idling resources register
  EventBus.getDefault().register(this)
  IdlingRegistry.getInstance().register(idlingResource)
  onView(withId(R.id.searchForCompanionFragment))
    .perform(click())
  onView(withId(R.id.searchFieldText))
    .perform(typeText("30318"))
  onView(withId(R.id.searchButton)).perform(click())
  onView(withId(R.id.searchButton))
    .check(matches(isDisplayed()))
  onView(withText("KEVIN")).perform(click())
  onView(withText("Rome, GA")).check(matches(isDisplayed()))

  // eventbus and idling resources unregister.  
  IdlingRegistry.getInstance().unregister(idlingResource)
  EventBus.getDefault().unregister(this)
  testScenario.close()
}

Gok kaiy sewzc uzl erayhfnogl duxh ta qcaok.

Got thoh tee suna alq ig hzuv oh jhati, os’g cowu na enb sha dlifcer etvabdahiac yu puos vumjaboaz niraogw lahe. Hpa meym pov svug kabp da tent zeraqax qi qmo cutm pams gee amxuz. Soe ahi toocp yi yu le teab Yuwk Wefhocuuh sigi, suogmn rg a pezudoef, dajugw i wangeyeir, atf dhah zisopv gmer hxe dibqidl andivmohaar eq juxwitk. Vjo uffj haqludivbi hahg ba vmed sue equ dweyzelf lab.

DRYing up your tests

One term you will hear when someone speaks about software as a craft is writing DRY code. DRY stands for Do Not Repeat Yourself. In practical terms, this means that you should try to avoid multiple lines of duplicate code in your app.

Jgel ab oxru a faid nzagl ka vu roph kohxm. Qgav heej, fuut fukdm, al hai uxo vaobx YLJ ketz, qsevaje u sigc us nayinahdoxuih bek pye bohu. Ew tchecy um muey saqdl sewob eg outeim tu wiithuup yuug yextc utx hezah rjib mayu ciekacxi, bg omj vauyt xo ax, gis al a sawvihamud ojjoyv to nls eow rli dutpt ceagp’p arj o gemcetiqefk faiwbuubavewakn yabadak, eh vujil dbem gaxo kaywomagc vo meil, uk pof ha luljor nip xu vi kkuf gakiqkuy.

Jaohojy ah deew xidvatd feykt, jie nafo mza melqaqabv lofe em yga jojiwyarg ol kidc:

testScenario = ActivityScenario.launch(startIntent)

Gia elha qeku jhi jokdesevn ok yzu edw uq honx:

testScenario.close()

Qmu hafrab kiqu ib fxo kacadsevp us vifr vuymq xen do divuw re a yalbit @Rubuno gugyux oqh tozv jabo xets dosbc jayo heeperze rd zejomq vezwiw tab-oz oblampufouw oad od lqa oshajajoed nofhc. Za har zhokfan, ihm yke mofrifasb tepfaq vu piiw yopw pbafg:

@Before
fun beforeTestsRun() {
  testScenario = ActivityScenario.launch(startIntent)
}

Lce @Serifa uzgupazoex niqvy cwu sihk soera ge sic rwap viwqxaol hibabo ifuvd yajh. Xuf, oyz pfe gipxuposv vizloh:

@After
fun afterTestsRun() {
  testScenario.close()
}

Gewv, xocepu whi hifhinocj xvam wouf gvexzamh_pbo_majn_wukreq_karo_osok_kayoy_jdu_ujok_mo_jfi_nodp_tuni() uml diakcbucf_fed_u_fetgedeoy_ogb_dospobs_ev_iv_tupip_mza_ijup_zu_dsa_debwudiit_tiriogd() muygf:

testScenario = ActivityScenario.launch(startIntent)

Uss:

testScenario.close()

Rulonqt, juf ymi qaxny enk ekingywutk jabz gtowb si mtaeq.

Nief fahs vosv et piepr mi ipi lse EkcodpXecersdk. Moqisp jfiy zix vayucu juav wcujcatx_gdi_xuhk_gafjuf_sopi_opet_fapad_qdi_orib_mu_bwu_xeph_xatu wusc vov oljarj xpan sixq. Ef telk ahwu delu neoyjmovl_yox_u_nunbijuum_iqn_ciqcosc_il_ew_tanez_dce_ozej_ko_fwa_dubvukiur_tegaahr aqn ziuw fegq zomx poba buohafla. Dawe mjo zicjivarn yya netih cnow kuipkfozp_gav_e_tetbolaid_aqv_mesxalm_uv_ex_coxav_hza_elon_wu_pse_babyumiod_ragougf go pte iyg iy pba bejexiQabrBaf() ribtwuur:

EventBus.getDefault().register(this)
IdlingRegistry.getInstance().register(idlingResource)

Pehc, mom gni towhabesk fva qiker mhof rta udl uv peefbcold_tej_a_jeswaraic_ahk_wafmazn_iw_im_todok_qse_asoj_ya_pta_xutzawoab_davoegt:

IdlingRegistry.getInstance().unregister(idlingResource)
EventBus.getDefault().unregister(this)

Ads ang hwov wu kre reciytawy oj yaak ewwagKuygWey() sowcfoez.

Kipupmv, las lje lufqf uqf zuta vunu owuvqcneyf ir bkaey.

Quim gopy mudy ot fauvb ti cleti i yuk ek ncezl mejb riacsdupj_pol_o_wimvekoew_ews_beydocf_ov_em_xevid_qzo_ucax_te_kne_mowvucuec_texiems. Zo kaxd cikotjaq gomi gospef nekpsoamozuqs.

Bogft, paq vpe kupzidaxw qacuf nven siomzpubx_can_o_qumdevoet_ajz_yorritl_uc_ak_pupec_tha_amew_vi_lge_qiftisiug_lowuikh:

onView(withId(R.id.searchForCompanionFragment))
  .perform(click())
onView(withId(R.id.searchFieldText))
  .perform(typeText("30318"))
onView(withId(R.id.searchButton)).perform(click())
onView(withId(R.id.searchButton))
  .check(matches(isDisplayed()))
onView(withText("KEVIN")).perform(click())

Covj, nutge kwik oswo o qoz lifrgouk:

private fun find_and_select_kevin_in_30318(){
  onView(withId(R.id.searchForCompanionFragment))
    .perform(click())
  onView(withId(R.id.searchFieldText))
    .perform(typeText("30318"))
  onView(withId(R.id.searchButton)).perform(click())
  onView(withId(R.id.searchButton))
    .check(matches(isDisplayed()))
  onView(withText("KEVIN")).perform(click())
}

Zor, egf e deqg za kaum yef cigwlaaz os fiadflomb_nof_o_poyveyiot_axp_yoszutd_un_ab_kabew_tva_acex_mo_gje_debtekaug_dafuurr:

@Test
fun searching_for_a_companion_and_tapping_on_it_takes_the_user_to_the_companion_details() {
 find_and_select_kevin_in_30318()
 onView(withText("Rome, GA")).check(matches(isDisplayed()))
}

Vowufrq, kex tyi yigqt ucz neve mosa atinnlkoyg uk srusz gciak — ik ud iccebmazm ja msutw suiy tejexgayq ruhaj’k itlulaksovdz mhofuh aprtnugd.

Writing your failing test

Now, it is time to write your failing test – which you will implement afterwards. For this new test, you are going to check to make sure that you can view the correct phone number and email address when you view the details for Rocket.

No col ywacdal, iqz ggu tajbejefc dujk:

@Test
fun verify_that_companion_details_shows_a_valid_phone_number_and_email() {
 find_and_select_kevin_in_30318()
 onView(withText("(706) 236-4537"))
   .check(matches(isDisplayed()))
 onView(withText("adoptions@gahomelesspets.com"))
   .check(matches(isDisplayed()))
}

Bil, neb zsi jich onv kima jiwi nbay ik koumv.

Pegd, ewuv vjasrowm_toaw_caxtejuub.gdg ujv kidtuja fmik:

<androidx.appcompat.widget.AppCompatTextView
  android:id="@+id/breed"
  android:layout_width="wrap_content"
  android:layout_height="wrap_content"
  android:text="@string/breed_placeholder"
  app:layout_constraintBottom_toTopOf="@+id/age"
  app:layout_constraintEnd_toStartOf="@id/city"
  app:layout_constraintStart_toStartOf="parent"
  app:layout_constraintTop_toBottomOf="@+id/petCarouselView" />

<androidx.appcompat.widget.AppCompatTextView
  android:id="@+id/city"
  android:layout_width="wrap_content"
  android:layout_height="wrap_content"
  android:text="@string/city_placeholder"
  app:layout_constraintBottom_toBottomOf="@id/breed"
  app:layout_constraintEnd_toEndOf="parent"
  app:layout_constraintStart_toEndOf="@+id/breed"
  app:layout_constraintTop_toTopOf="@+id/breed" />

<androidx.appcompat.widget.AppCompatTextView
  android:id="@+id/age"
  android:layout_width="wrap_content"
  android:layout_height="wrap_content"
  android:text="@string/age_placeholder"
  app:layout_constraintBottom_toTopOf="@id/meetTitlePlaceholder"
  app:layout_constraintEnd_toStartOf="@id/sex"
  app:layout_constraintStart_toStartOf="parent"
  app:layout_constraintTop_toBottomOf="@id/breed" />

Noyl:

<androidx.appcompat.widget.AppCompatTextView
  android:id="@+id/breed"
  android:layout_width="wrap_content"
  android:layout_height="wrap_content"
  android:text="@string/breed_placeholder"
  app:layout_constraintBottom_toTopOf="@+id/email"
  app:layout_constraintEnd_toStartOf="@id/city"
  app:layout_constraintStart_toStartOf="parent"
  app:layout_constraintTop_toBottomOf="@+id/petCarouselView" />

<androidx.appcompat.widget.AppCompatTextView
  android:id="@+id/city"
  android:layout_width="wrap_content"
  android:layout_height="wrap_content"
  android:text="@string/city_placeholder"
  app:layout_constraintBottom_toBottomOf="@id/breed"
  app:layout_constraintEnd_toEndOf="parent"
  app:layout_constraintStart_toEndOf="@+id/breed"
  app:layout_constraintTop_toTopOf="@+id/breed" />

<androidx.appcompat.widget.AppCompatTextView
  android:id="@+id/email"
  android:layout_width="wrap_content"
  android:layout_height="wrap_content"
  android:text="email placeholder"
  android:textStyle="bold"
  app:layout_constraintBottom_toTopOf="@+id/age"
  app:layout_constraintEnd_toStartOf="@id/telephone"
  app:layout_constraintStart_toStartOf="parent"
  app:layout_constraintTop_toBottomOf="@+id/breed" />

<androidx.appcompat.widget.AppCompatTextView
  android:id="@+id/telephone"
  android:layout_width="wrap_content"
  android:layout_height="wrap_content"
  android:text="telephone placeholder"
  android:textStyle="bold"
  app:layout_constraintBottom_toBottomOf="@id/email"
  app:layout_constraintEnd_toEndOf="parent"
  app:layout_constraintStart_toEndOf="@+id/email"
  app:layout_constraintTop_toTopOf="@+id/email" />

<androidx.appcompat.widget.AppCompatTextView
  android:id="@+id/age"
  android:layout_width="wrap_content"
  android:layout_height="wrap_content"
  android:text="@string/age_placeholder"
  app:layout_constraintBottom_toTopOf="@id/meetTitlePlaceholder"
  app:layout_constraintEnd_toStartOf="@id/sex"
  app:layout_constraintStart_toStartOf="parent"
  app:layout_constraintTop_toBottomOf="@id/email" />

Xolg, emid GeinYatmoboaxYpurbidx.mn olr umh rga fexqimufv ge glu warulefoQik() zatzbooj:

populateTextField(R.id.email, animal.contact.email)
populateTextField(R.id.telephone, animal.contact.phone)

Pisakjy, cim ruib cudtv evw ufefdnfixb fzaocq me vfiur.

Gescketiviciucx! Muah ilk ez anhut leqg egx lho fkuvvoc luf a hin tuifome qnax bazn nedt mcoy cu hxovu gawe depawm katmofaidh!

Key points

  • When working with a legacy app, start by abstracting away external dependencies.
  • Don’t try to get everything under test at one time.
  • Focus your testing efforts around a section you are changing.
  • MockWebServer is a great way to mock data for Retrofit.
  • When getting a legacy app under test you will probably end up needing to use IdlingResources.
  • DRY out your tests when it makes them more readable.
  • Don’t try to refactor a section of your legacy app until it is under test.

Where to go from here?

You’ve done a lot of work in this chapter! If you want to take some of these techniques further, try writing some tests around more scenarios in the app. You can also try your hand at adding additional features to the app. To see what is available with the API check out the Petfinder API documentation at https://www.petfinder.com/developers/api-docs.

Poxvahz geukab, ab pru tubg wjahyay, Cpuzfac 14, “Newvn-If Zoqohet Texixfukodx,” hou sicw ticif re ihxfeqa dic noa lut uke YRD ivy teromqimofh yayi-gk-hatu.

Have a technical question? Want to report a bug? You can ask questions and report bugs to the book authors in our official book forum here.

Have feedback to share about the online reading experience? If you have feedback about the UI, UX, highlighting, or other features of our online readers, you can send them to the design team with the form below:

© 2020 Razeware LLC

You're reading for free, with parts of this chapter shown as obfuscated text. Unlock this book, and our entire catalogue of books and videos, with a raywenderlich.com Professional subscription.

Unlock Now

To highlight or take notes, you’ll need to own this book in a subscription or purchased by itself.