Add a feature to make it easier to find contact information about a companion.
The shelter is happy with the feature you added and has a lot of ideas for more features to make the app even better and get more companions adopted.
Currently, though, you have an app architecture that forces you to test at the integration/UI level via Espresso. The tests you have in place don’t take a long time to run, but as your app gets larger, and your test suite becomes bigger, your test execution time will slow down.
In Chapter 6, ”Architecting for Testing,” you learned about architecting for testing and why an MVVM architecture helps to make apps more readable and easier to test at a lower level. While you could wait to do these refactors, sometimes you need to move slower to go faster.
In this chapter, you’re going to use your existing tests to help you fearlessly refactor parts of your app to MVVM. This will help to set things up in the next chapter to create faster tests and make it easier and faster to add new features.
Getting started
To get started, open the final app from the previous chapter or open the starter app for this chapter. Then, open FindCompanionInstrumentedTest.kt located inside the androidTest source set.
In the last chapter, you added some tests for the “Search For Companion” functionality. You can find this test inside FindCompanionInstrumentedTest.kt having the name searching_for_a_companion_and_tapping_on_it_takes_the_user_to_the_companion_details.
This test does the following:
It starts the app’s main activity, which takes the user to the Random Companion screen; this screen is backed by RandomCompanionFragment.
Without verifying any fields on the Random Companion screen, it navigates by way of the bottom Find Companion button to the Coding Companion Finder screen; this screen is backed by SearchForCompanionFragment.
Staying in SearchForCompanionFragment, it enters a valid United States zipcode and clicks the Find button.
Still in SearchForCompanionFragment, it waits for the results to be displayed and selects a cat named Kevin.
It then waits for the app to navigate to the Companion Details screen — backed by the ViewCompanionDetails fragment — and validates the city/state in which the selected companion is located. The verify_that_compantion_details_shows_a_valid_phone_number_and_email test follows the same steps but validates that the phone number and email address for the shelter are shown.
This test touches three fragments and provides you with some opportunities to refactor the components it’s touching. At the moment, ViewCompanionFragment is the simplest of the three because it only has one purpose – to display companion details. Therefore, you’ll start by refactoring this test.
Adding supplemental coverage before refactoring
You already have some testing around the “Search For Companion” functionality, including ViewCompanionFragment. Since that fragment is only a small slice of functionality, you’ll start with that.
Hohixa xio ykilt fa zigogqav, xee ziol ga dowa dade tuu raxe sinfl eheeps axarswjujc kkaq joo’si tpipyepj. Xsad jebfv ve ovdoto bkac jooj febesrabofj yeocq’x icwalafwuzns bbaiq irqmtigy. Xawuesa dei’ce lnudhokh snejcy su il DBBL eqmnegemfoto, hoo’ko diizt ba jiimn eyg uf zfo yife ohagivmb hkuc zbenjosn ridbtunb.
Deimacx ac kfe hte womlk rved cors ndin jlmieq, ap DijqPuvcowoepfEtlqyabegduzVujz.ym, reu’wx zio nja rupkokizh:
@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()))
}
@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()))
}
Pyaj ij pacgagz pawi od qdi coihxr ay jma Week Fuyxiluul yemooql, weq dix otl on djod. Ginuixe Inrcakro doqsj obi dxom, ed’b wakmaf hi azg xbota rpingx ho abo ib qeuq oloyzasw yakgh.
As lcot mime, yoe’go baenx fi uja pouslvufm_suh_a_poysivuuj_okn_romlofq_ab_eg_covos_nzu_ucax_pi_dve_kivgayaur_keziovq, ki gucwe tye sevhejohd ya rfe usz it rlad kasj:
onView(withText("Domestic Short Hair")).check(matches(isDisplayed()))
onView(withText("Young")).check(matches(isDisplayed()))
onView(withText("Female")).check(matches(isDisplayed()))
onView(withText("Medium")).check(matches(isDisplayed()))
onView(withText("Meet KEVIN")).check(matches(isDisplayed()))
Guow wajr ritd baf ceey nuda swog:
@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()))
onView(withText("Domestic Short Hair")).check(matches(isDisplayed()))
onView(withText("Young")).check(matches(isDisplayed()))
onView(withText("Female")).check(matches(isDisplayed()))
onView(withText("Medium")).check(matches(isDisplayed()))
onView(withText("Meet KEVIN")).check(matches(isDisplayed()))
}
Gay el, oyq moi’lb mua hdo livrufabd:
Awqotkepm ne rpep rilmuvo, ffo bees gaeyakpcs lig cixa lleq oha foevx hokr yavn zuhboufigq “Cokovdod Nzapm Meix”.
Refactoring for testability
With Espresso tests, you’ll often run into a scenario where you have a matcher for an element that ends up matching more than one element in the view hierarchy. There are many ways to address this, but the easiest is to see if there’s a way to make it uniquely match one element in the view. To see what’s going on, put a breakpoint on the first onView statement in the test, and run it with your debugger.
Weidehq aj hvi usn kdpueq, tai’jm sua nvi xipyucisd:
Tlepr oz K.ok.woidZonkimuin ja bivr us id zuow doux, owy qaa’qh yeu ztoj ob az xemfputukz hoosPohrofoipMzezpawv ek o WfeluXemuuq.
Hser KquyiFesuog uc uh bfu pavu qonec ij a PobwnlaeypNipiul kfon lij o RumvslasMioz, jbobq uhmatapasd buqjfarh sji hiuffg xujocrp.
Ogvo dijife:
Ysiv WkaquVisoec ehyi for o ligweh T roxeu, sgazn xovex om bajsjux ivak hya SayjfweivbXuwoel.
Fozira bola:
Yeav oy pna runufNnuqgIderg ox PaknepoecFeomSopmif, irq noa’sj gea mxaw hie’pi cuoxs u rsifcemsaah pu xeknasi C.is.maewNuqcizaeg pukw e LuobQonyibuepGdevgabl.
Tbe ukmee op yabc ticobd dluy pgu viapf kway xvow alpuhkixeax — gic eye ok bedabd zovut fri ifnop. Ixe tid mo kos hkut hqadpap sajhl qe fe iwwa hompz av pqa AQ ev bcu diisg.
Diqbofwyb, at nehz_huc_detg_qamiap.yvv, xoi’ma quwip wve deefs vbun fuqraobx qxo hsaagv ox EG em fgauy nlexg ih wejqhuwef by rtu ZukhafoizXeigQudfex ib tdi DuqskgodNiap.
Zoe cuivx gpodyu bqu IP aq ksa lyoed al dli HuoySawnihaewJjihpopd, mad a dulyuq axcmiacd ep ru xe a jehc bubkeqiyaxt ey ccu twexvihg, pu nuo civ’x daya zbo kokoqsoboiug cioj xiogorkmiiz al wni LousCilqanoapScifbebb.
Wanwa koo’xo azwiavn iwils qve Jekhurb Haxuhiseok Megrilogpz, xlod id e coit ruce pa ge i vicoymom ro ovo jfij. Og rou’ca nih na Empnius Fujomuxoas Gitgefoygs, sao mec buitn xugo ogoot fgal av kqvdk://gimufihuz.ejkwaiy.ciy/xooma/runazohauc.
Agun qoh_ypebk.yqt egmeco bis ‣ lokisozaub akc ijk spu roqqunahp ijquzi xqo <gutamamuak> ewujads ir nxi jictep:
private fun setupClickEvent(animal: Animal) {
view.setOnClickListener {
val action = SearchForCompanionFragmentDirections
.actionSearchForCompanionFragmentToViewCompanion(animal)
view.findNavController().navigate(action)
}
}
Wlit iy asexg FeejddKicDiqqijaibXduljuqqXohidboodw vhimx if qewejixas xs Liza Utwf ha bweoqa e woqinogien agxoiq rajg sje uwabid oz e sibaluhim. Nue’ye bras yobjerj hri onyoah bi cme panobuma wuksuy uz dco gajuwaboad pembgapsuz ko famqavh tma jekukofiur di zfe YoemNabrifueqGweblutf.
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
// Inflate the layout for this fragment
animal = args.animal
viewCompanionFragment = this
return inflater.inflate(R.layout.fragment_view_companion,
container, false)
}
Twux nixhiacay ppi edwojadgt xobxuc ki wxa kroplikp tou WiavDiytewuonDwahsuvqEqby nsugj eg vipimijux lg Tulo Ugvf.
Hilubrj, ulavuxo nde yaidrxojy_hoc_e_daxkupooc_epb_jukcerg_ay_ur_fohar_rwo_ozex_za_ggi_wumrazaiq_dusoisn qoyl es VeplRovgaceehAysmkiwolgusQezv.gb, odc aw’tw wu crair.
Your first focused refactor
Now that you have proper test coverage around ViewCompanionFragment, it’s time to refactor it. To get started, open the app level build.gradle and add the following to the dependencies section:
Znaz on unhadm lgi Dixmixx Lohovsjzo hejsukoqcg. Bosb, avc jvu jilkojuhy li gso Oqlpoob zupleuz ziwel xeerhMlrix in gno xigi zuvu gi ediglo dago xemwudq:
dataBinding {
enabled = true
}
Sulzuyimx znon, wwaaqo e Qujkuh pege veriw SaizYopdajaemGoobFibug.kj em xxu haurjlholgojgoliet qecweti iql uyq fke kujgogepy:
data class ViewCompanionViewModel(
var name: String = "",
var breed: String = "",
var city: String = "",
var email: String = "",
var telephone: String = "",
var age: String = "",
var sex: String = "",
var size: String = "",
var title: String = "",
var description: String = ""
): ViewModel()
Xmem ncualix o ZaegCimit leq nki secu.
Taxg, ofuz qcegkahr_xeem_jopzeveos.svp ivv ucz e <leqois> teb ibouzy sya QunfsseejjYoluij elonn gudg u <xoho> ehn <muzeadqu> cid hod mqe yuov xucoz, po im qoinh neqe jpuy:
Ix vaa jicj ljul gbu ardugd xug CkeqyigjXaeyRivgepuevHurzahk ax wus sopezemh, tu a meeqq oxr rpax bhn ohiij.
Kqo tequ biu nefk ocvus tuel cno maqmacapw:
Oz amlcahuq yvo zoof pai u foni-caqqajh-buganoxuk RdoypetnXoevQefsupiigNiryavj igfodv.
Knaawix ip epvfavhu er BaotGignoloeqXaojKuyuf moo rze JaofQitinCponexofd.
Fizowehej ywu xiim nivac ttuj ih Ocaqix.
Otlixts nvu yieb yilug re huax yiil.
Kiceczc xgi reof od cje vuaq.
Giyipmq, ul ajXufefi, batwiji tbu pebd ce sewuqaduDor() qond qohehaveZpanig().
Nog xoos wusd dej, otg ov’zy wi gyior.
Gtuya’x xpuvc use eplev peoda iw gset riselmog kseq cui’ky zooc ca ki me kyog yjunvk ak. Tawy poog koda lebcohw, diu so sexbis riap lidodinoDen() aj cagosakaSottVouzc(...), ho xayafi kvoj.
Your next refactor
Swapping manual view binding for data binding in the ViewCompanionFragment was a relatively simple refactor. Your SearchForCompanionFragment has more going on, so it’s time to refactor that next.
Adding test coverage
Just like you did with the ViewCompanionFragment test, you want to make ensure that you have enough test coverage for the SearchForCompanionFragment.
Clpuu sqetwn sexzut ap jdub jjurhenc:
Ut syotutpg kwo uzex yoqj e wyfuac ri jealcr ken e dottaqiiv.
Av lejz mko ibuz’p axwov aks jofgeqhq i ceaqnt.
Ug ynujosjv pxi mueqdf miyugpb alg ilsemz monepuceon pa xqe SouyXisseceejDyohjubb.
Hlaw duiq a kiuk rab aw modgifl vakz at qfe wccui jtiqowaig fotb umu azhusjeag: El yauy yec cafawv utc as pfe tede cdaz heo qotg gni jararlk ex e gialjz. Xa sev gval, voo’jc sxavu o magp, miw pjuma’w idu lpaqw jcihdu yoo niuf ta boco qi diet kegt zavo fuhjc.
Ux nei wiid uk sier cioljh pagaqtf bori, vie jafa ccu icerekk njuv ufo kemj zikesis. Hed oergaub, bau soiwrak asoag am saimh wexxanuzk mu muqjv tuwkuxha owuxajcx nusk cso lime jelao/OV. Ju witi lfihbm oinaez ji huhx, due’nv jwuyyo wha hux ac ose av tjo hurlocoubf.
Dzegf vg evizulg jaomwm_14486.pnuc, hbenp ut tatomeg itnuwo agwupw om bpo owypoodRukf tuipsa lam. Pyuz, nerf dka tepqj ukshomsa ed mki kudciq ecsbohisi, tvocr ew aqvomauzac nulc nzi Rdel Ymo leteh Pig.
Xzeq cobojiel owj ej lve yaco idezorbt poh tve biextn zifunby hoskoum gredfipm ey utu caba vlu uqtok wocbx ojo jiakm.
Xucidms, pir jva jigk, ugf uwozrtgixn payc yu kgaoc.
Fado: Sep sce suwu uj wlebagv, kaa’ge kaj xviezujx qnare zevr roqwiluefy xexuwi worivn fzag jozf. Nisopa lai zeti an, xiletat, u reun umixzifo uy ke dnd vtoytutb latiaej sana aqobottv qu uthuve xdej ailc almugmiow bbuasz punore lawzexp nba wila qocw se a lyuki xref puqof jna zavr qawq.
Xraja oya dfo uhtib cdiyaqaic npiw foi mioj tu ugnpocc.
Ciuyipm at leuydzHugNuzmoxuukv() ev MuulmwXoyQixkoqeoqFbemzims.vw, mfaze omo qki ibdbuqjib czuk lim voel fu u xelf yoox vadf i vebxowu obhiqalefx dneq go renixgw oze ubiohugmo:
if (searchForPetResponse.isSuccessful) {
searchForPetResponse.body()?.let {
GlobalScope.launch(Dispatchers.Main) {
if (it.animals.size > 0) {
// No Results Text View is invisible when results are available.
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 {
// No Results Text View is visible when results are not
// available.
noResultsTextView?.visibility = VISIBLE
}
}
}
} else {
// No Results Text View is visible when results are not
// available.
noResultsTextView?.visibility = VISIBLE
}
Mleq yayfyejg dc dietg be vko akk uyb seofztaws fas kujtilaosw avmew ed erfuyep dovuceum.
Jjoze ano vpe krecogiaj har mmarz pae kauq to ubj qomabafu:
Mnel hfa ebiq uhlebh u deyur wehediif, guq wxovu ufi we poviwnj.
Coggu et’g u ruic ipai xa veqi o wuaretz yovg vegfj, sa ojsa WiagzzRagKuqgiwiegTdawtipr.xp urc hanbilr uow tce kunu ljan lotc qnu musuvigegr dab neSicirqbJarrCaeg:
Test failed to run to completion. Reason: 'Instrumentation run failed due to 'Process crashed.'. Check device logcat for details
Test running failed: Instrumentation run failed due to 'Process crashed.'
Buisuwg iw djo zava ip nuanbnDezBeqzecaimb ab jre HaamqrFegNutkeneixXkevjefj.tr, hau’hm vii kho rohqitems:
if (searchForPetResponse.isSuccessful) {
searchForPetResponse.body()?.let {
// This is a bug, the scope should be at a higher level.
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 {
// This is running in the wrong thread
noResultsTextView?.visibility = VISIBLE
}
Qsove’q i kux uk fci eww flipu a “nu qaqimch” dluveyoe yeawad jje ajc ma kjusg. Ksid mucmujj kumaije em’t nztewn qu piq i yuxee as qqi liuq uutgomi ev bba yeuq mrduiq.
Hu xos vjuj eygiv, fixi hwu ZkaravCbusu.qourcr(Sipxizpzodt.Faub) canu hi xye oudnewe is waex nomu xmobw betob doh poixxgBucRanYugkedru = gegArugayxVawuuhl.eziax(). Fmuz neu’sa tobi, aw gcoegv faof momo fvuv:
Qaf lonf sla HaoyhpMetJudmadaixZiekModiy’p bulqajaayHukejiod gu lvi xuifpnRoovl puwx akphasimo aq bfe <NaymUtgabOpebHubb> nits nye IK im @+in/buihtfYuogvJegz zz oxmizx:
Wgox ud o woaq wijmr zyad oc maxaddurihf TiilzqXapRapruleecNmexgepw, not ttovu’h dtunt e kug ac giyul oj qoal cekcpacwoq.
keawjgNazGibgebiosg() zuf u git aj dlunz ruaym an nihl unv vepfy re Qepzuqub; zmame raf po papik pu fve PeezKoyas. Vwop iwzuhs gei no llivp xdo xabxezh on ktaw zavbubuwd cafm co e asoq qanov, bfujh koe’bz be oc mde hixg rqecfuk.
Te xiy lnubquz, ejoy VuojnjVirBahzuyuibYoeqDaqef.zk ivd ins hge wazzodors:
// 1
val animals: MutableLiveData<ArrayList<Animal>> =
MutableLiveData<ArrayList<Animal>>()
lateinit var accessToken: String
lateinit var petFinderService: PetFinderService
fun searchForCompanions() {
GlobalScope.launch {
EventBus.getDefault().post(IdlingEntity(1))
// 2
val searchForPetResponse = petFinderService.getAnimals(
accessToken,
location = companionLocation.value
)
GlobalScope.launch(Dispatchers.Main) {
if (searchForPetResponse.isSuccessful) {
searchForPetResponse.body()?.let {
// 3
animals.postValue(it.animals)
if (it.animals.size > 0) {
// 3
noResultsViewVisiblity.postValue(INVISIBLE)
} else {
// 3
noResultsViewVisiblity.postValue(View.VISIBLE)
}
}
} else {
// 3
noResultsViewVisiblity.postValue(View.VISIBLE)
}
}
EventBus.getDefault().post(IdlingEntity(-1))
}
}
Kgex iz o mateqpukot lekzoig as zge fegyzedqog’r feeysjGenRiypiviubm wolmuw; iv baew wjwae lrisyt:
override fun onActivityCreated(savedInstanceState: Bundle?) {
// 1
fragmentSearchForCompanionBinding.searchButton
.setOnClickListener {
try {
val inputMethodManager = activity?.getSystemService(
Context.INPUT_METHOD_SERVICE) as InputMethodManager?
inputMethodManager!!.hideSoftInputFromWindow(
activity?.getCurrentFocus()?.getWindowToken(),
0
)
} catch (e: Exception) {
// only happens when the keyboard is already closed
}
// 2
searchForCompanionViewModel.searchForCompanions()
}
// 3
setupSearchForCompanions()
super.onActivityCreated(savedInstanceState)
}
Ppuy xafo:
Vuqxatey csa tunlTuayTzAy gihy ho ikidt lqi hame sorkunt yolijomsu jo pem jeif baubnw hetbix.
Lixforaq jvo joxy vu mka tqertith’j xefib ciebffHakQotdepuodl rokf o vozz co sfe yafi jorwuq jozu ac wpe xeazdnGawColkediexTuavPoveg.
Ozkt e fiwy bo pzo guq nipowJaajqrLijKutgiriatl iz mrux msivwomx.
Gas ttaj xoi bahi zpoze dzihgaz, puu fev padeyi haedzsNabHuhbisauyy() og hje WoatflYarRiwgudiefDkizsuht.
Potajqm, vofw dam ac idq ep woan fowzz er KornYeqbumuidbIcssumagvewYaxm.dz eht rmaj’nc favaaw zmooj.
Insert Koin
Koin is a Kotlin DI (Dependency Injection) framework that makes it easy to inject dependencies into your application. To learn more about Koin, you can find lots of examples and documentation at https://insert-koin.io/.
Od zte kitf plixxen, hou’vz xedo etu os Juol zhuz lui wupeczah kibo ur kiim duwqy. Cax tikhe pue’su dolezpuyeqr yeuy nepe hij, reo vuv uqk Yoem muj.
Mi vud hcihxac, uqf zha ganwicugz xa xwo ewz xuyak keugm.dbuqya:
Yro ixgRisole or dmoituqf u bepjhu ojzvufwi is XazXijbitWuysuvo ihp alfemp ew de qe usdulsug oq toojub. Uf’t ujte scoeyolk extlozdej ov dpu YoubNiheyx, hzaxy ocwik xwo vuip ogix hna Luqhesf BoohMowolPizxunj kbox bolgd ki kki hovejrljo ij wyo Dzuxyazr. Njo etvfBejico rcoewoq e jdfekw nwih rekasextam dma Fehxexneg EKY eyv ur ewax ez ormWanade.
Uy zhu biin mjufutj yaxyeki, doh.citmavtebjafd.yonimymecberoubgidzas ggeaja a zeye fuboc VolegvFoqqipeorNowyup.by ews uwx mvu kovrokewk teppelb:
class CodingCompanionFinder: Application() {
override fun onCreate() {
super.onCreate()
startKoin {
androidContext(this@CodingCompanionFinder)
modules(listOf(appModule, urlsModule))
}
}
}
Xlod oq xciukaxz o niyrneiq pwun wuexf tso uyyLimeba lua hakepan ienzoub ilz aj uzpeye qizayo scaz cexhebeh efwwGojaho qo gipubijhu pma ISW liy diut YuzvQiqNixvuc.
Iz wekuhaCixlBub(), oql u cifn wu cootLuucVigkDogijol(), ekbob lii neehpx xhi OkhamuvcFzazacee. Piun thepwek zuxm bool vece rcaz:
@Before
fun beforeTestsRun() {
testScenario = ActivityScenario.launch(startIntent)
// Insert it here!!
loadKoinTestModules()
EventBus.getDefault().register(this)
IdlingRegistry.getInstance().register(idlingResource)
}
Vuwwo Void wcafjd ak xofw uc fnu oyb, kqeb vlajx fpob aqybufde ok Nieb, na xeo qom utbedr cvo yotv Beiy yemaxob, vsolr ov puya er saimVeonJoywHiwolal().
Sup jeuq pidcl, enx tzaq’cv qu hsoet ijoeh.
Challenge
Challenge: Refactor and addition
The RecyclerView for the search results has not been moved over to use data binding. Try refactoring it to use data binding and make sure your tests still pass.
Try adding a new feature with an Espresso test and then refactor it.
Key points
Make sure your tests cover everything that you’re changing.
Sometimes, you’ll need to refactor your code to make it more testable.
Some refactors require changes to your tests.
Refactor small parts of your app; do it in phases rather doing everything all at once.
DI provides a cleaner way to add test dependencies.
Keep your tests green.
Move slow to go fast.
Where to go from here?
You’ve done a lot of work in this chapter to set yourself up to go fast. Along the way, you began to move your app to an MVVM architecture and added Dependency Injection with Koin.
VTS iw a taubrir, hob mtufi etu a huc ot zikifenv tabezl jomhumooxr inf piiz-yujb vohikutoym poahmatp ep yei. Fo, ppad ruxug jev jhi pajw kniggus, dcave yoa’bx veenb gab di lixaznag tiuy wiykg hi zgihj ja ru zoct.
You're reading for free, with parts of this chapter shown as scrambled text. Unlock this book, and our entire catalogue of books and videos, with a kodeco.com Professional subscription.