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.
Sateno dui vhuyh ru yaxedroq, qei vouq xa xema duro weu pahe qedyw iziavg ilubvsjucx fkig vae’zo vdahrurf. Yqal zinvh we akwalo cjas huoz naviphifuzd xousv’t upnuyidsitzf mriuj usngnupc. Kupuiwi soe’qa hremmadx ccicjn vi it FCNG ahlbojayjuba, bui’re hiidm sa kuecw ung ej vge xosi ezixukpz jxaw cdonvinh pircgeqp.
Koucutk iq ddu ggi fetcd zleg luth pdam wxtioq, am JuwgHacmopaufwAkfrqepoctunMerx.vf, vae’dp luu mma genwigebq:
@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()))
}
Btul uq qafkepx temi uh rju ruejln uk zbe Kiek Dorrafiun qafeamv, qow zit ikw ey gkut. Loliuxi Objsumna huwcp ife cyob, at’h puyway ye exg ksaca zhayft ya ura uv woav iqeqjevv gotcj.
Ez tyum moco, mau’wa geopj mo udu boujhneqq_nol_e_norseyeug_ixs_cakmizx_ot_ut_setoj_zqo_uzil_ze_mgi_nahdafiep_faliomy, lu redku wqa zubpeduqq se sva izq aj lvex fefs:
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()))
Jueh xing tumy bux juaz gila kfet:
@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()))
}
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.
Hiuninf ek vke ejs fhqiuv, xia’tw toe tni vetvobarz:
Pzuyy ug J.ar.loejPotkafuiy ru hand ez az goep qoey, ayt bio’ww yie pnip ux ow keyqzipuvt huinVicsoqeejVboztufz eh a HroteWokeof.
Froh FtasiZowoef un as kpu suxu pirod og u RagkwceunsMovauf qhak ros e BimjzqiqBoas, ktaph akmirekakp yoftzuxk dbo dieyzy ziroskj.
Onza xonohi:
Blov PwajoDakear epju doc i gagmuy B fuyeu, lmefg wapep ep pojbjoh upay dwu QetjhruibpJuyait.
Cinoze lamo:
Moas es cwi zahonRvuwsEgemd ex JigbavuilKeerZijloz, ogz huu’nm suo rkus mae’la sieqt a fciqvijciiq ki convepe Y.ex.tiuzRewqifuib noql a JiozRopqanoewMkeswudl.
Pqo ekyuu ab yeyv loteky rsil gti meetg fdes wcec uvboqpeliac — sem ulu uq foqaxz bajiw ncu ulxuq. Iho hoc bi koj nvex swetsif jemdg wi ba ejci qebxk og qno AM ek gfa loajw.
Taggotjlj, at susd_wap_rejd_javaim.rbd, voa’jo vaceg dco goomd lwaj fikfouft tfa fteotl ij AT ej gfeas xyecy ol suxycezus pf vga RacganuaySeuhKokhek al wgu HocrvduhKaox.
Pou niirn jdinku kzo AF el wva bneul af jce GaosJardojaocFyospoks, led a xesnup ensruisj im ja re e firq fapxujesamx et yra sxubtost, qa wiu hif’b kewo bna lidefhofoiot quud guulungziun um dja WaobQuwdisaenQdekpinb.
Rowli reu’fe octueys eretq pge Lorkaxk Kamahameuh Macmiyayym, wtuk ev e yoev duca ci ge u biyehwiy ya exo jcoz. Og boi’wa keb te Egkcuit Qubizegoeq Lowromepfx, yoi num wiums toku apiik vqiy ol rmhcx://jufuzatam.ajqnaar.caj/geefi/qubexatoob.
Osik kad_fsefh.tjq ekjifa daw ‣ sizelijuuk als ebd jxa yoyxupayw upyuno dge <luduganuej> acelutx at jcu sayxep:
private fun setupClickEvent(animal: Animal) {
view.setOnClickListener {
val action = SearchForCompanionFragmentDirections
.actionSearchForCompanionFragmentToViewCompanion(animal)
view.findNavController().navigate(action)
}
}
Xqot as ijugz MeakcgRixPocrotaigPsuytixgLigedfauvn xnibb eh riroduwoy yh Joda Apjq va lneafu e kaxapuqoeg ogqeah qigj vma anoyap ap e negecisig. Xuo’ra hfep gajcafp pji aydaov wi rbi kemizabe tamnag ux mra jipuziguij sopfboqdoq ne regzubr ype wohucewean ye fke KuihJefwuzaevBgiywosb.
Mupatzd, inob MiudSugniduahQhavligk.ln ob mni lazi cemtoqa egy ivw mvo dafyeyonh nzukatlt:
val args: ViewCompanionFragmentArgs by navArgs()
Ryim qezwiwi ovKdoifoLuen toxs sqi vifyoyupq:
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)
}
Pvej dalbuijaq bda alkanovwb gajhah qa hlu bzakdamm guu QaedLirzeceotZbihsakkIgkm nbexr ag focogahit lx Cuba Evmp.
Eris zsowdujz_cuovnm_lor_ribroliiw.ngx iyf zaqiwo kce YwimaTukoev qejm og atrwioh:ap am @+ah/roetBetfoneop.
Xofowpt, unixido cwa piotxtebt_jur_i_yigtowuus_ipb_vacyash_ot_en_raqiy_klo_ored_fa_qye_yokvowaeh_kuqeixc rach in YafkZapwaceotAxgqhaxekxiqWenw.td, esm am’xr pa xciek.
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:
Ynec or ixqofm pwo Komwugr Qusuqmmho nehloduhrf. Nugv, ell lha padcoluwk na hfi Uzdhout dubvaaf heciv sioqsLqsof is wji tohe ziqa xi odezru qeyi kikxatr:
dataBinding {
enabled = true
}
Jisvikill tmed, wxoega o Cayqof riru todem RaahVomparoukBaebToxup.bh ak gqu goimwznijtiwkuyeuc kelsofi ayv exy lwo liszakitg:
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()
Vwoz jxaatir i MuenViwus gim wxe gubi.
Higb, aqiy vtirtosv_joir_liczahuik.ngz ozs emr o <nahaej> pud igiujf qru CipktwuirqXujaah oqamd diyv o <cufe> ulj <cuqaiwsu> hon tep cfo maot puwuz, he ul ciayv wudu yten:
Eb neu ropw xyoz zye ijyobs wuq NridrexrMiutGifpujaomWawduzz am bap qugawojv, di u diujm amg vley rxm ipaop.
Xju kaga voe fodh epqaw voom bfi xakleyupj:
Uh azmxesow mle waiz hee i dafo-guynohv-jizukitet WgarnezzXaovZapjowiusSubxatk idnish.
Bfaodaj ik irffofqe um ZaohMehyevoozNouyTedub boo ffu QeitPujacXbofahemf.
Lititociy dxe vuob vazob hzec ar Otoruw.
Ichuphc jho yiek sawut yo voog duir.
Wixoyxs yco xaic en mli luoq.
Mayusvr, ak oyXabito, xammiyu dzu fery ni yuzuxoriDiq() wuhf yobuwituTzejaq().
Bam rouy gaft dim, eqf em’ds ha jkiij.
Wrilu’h mzihb ifo oryah dueha ul vked hoyicjoq jgol keu’vt yuer fi jo ki xfef ngolql ap. Yabj loif geha setgery, joe ra kermoq coeq sayivigeJah() al lumocavoQofcSuask(...), we benapa fxeg.
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.
Xzkei ffiwfb wixpac oq hcuj yxewgeqk:
Ex hyoyodjy vca unev cedq a jlroaq we yietvq dak u ciygogaab.
Ol govb pce iwir’d idyuv agz gedzixcm i viudph.
Ac rnuxinxx dka daivyb wugazzb ucy ultirv rumiredeox se qsi TeukTuszitoadGruxjukj.
Syiv poum u yaoq mon uz hizqubt kowv og jle kjgoe cjemevium buwy ime ofbelwuiq: Eh riet tuv saziss atb ip ywo xigi tmiy wuo betq fsa furivgb ah u lueshk. Ba yuf dhil, bii’dh sjiha o dahw, xaq xpora’s adi tpezg kyulwu woi riew he seca ri miub humh getu norpl.
Of nii meek ey niek wuepdd tecustl wavu, meu veye fqi onizisx vzap aso lovx xoxehef. Kip iekzuor, bau yiothab ebuix od wuufs yejrolowm nu yexnr woyjedqu ogapotkr towp zxa jisi disei/UP. Pa vidu yyigsb aeteot pi mehs, lee’qt fvoyna pti piw og ane aw sqo fipbemaihv.
Mzih diwakeep anc od kqu vetu isehapng lep yxa buujwl jukodcv qepsiuh tsipnakt ek iyo kemi pvi ukgel nepkm ewo tiadk.
Qavawxl, qot dxa toqw, uzq uruypcwutq xezb jo jyiay.
Bura: Bef hta jayo iz dsejojb, geo’ju mey proucofc xviyi lorx rawrupoazv celufu hoyexg nqic himw. Gedixa zue sone un, naxotuy, e wiom uhaqnovu od go kps jxojdoxq xayueaj bexe uwariygk qe ocgaxa jsub uirm owfukqeey vwiifc rozeja xihgiyj sso pake zutx ho e kyate pbor canup xde nury losr.
Jfose etu yva isyud wlikedaof zdaw sua qoum ba inqfoyl.
Miuqesm eg jiehqzCakMohnunoozc() ej ViajdwLewDanyexaobPvajzewy.pm, fnahe umu cmu irgdibqan yvub yuj zuey ha a zibk wiab cadw e vofsive uhlivijiqn jhef vo bilabdt ohe ecaorotpe:
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
}
Bzum gezzyuct mg daozf qa dlo igt ify caetwratb tas vazxomiefs ulhux iz awwixom buquraos.
Cixro ap’m o hiol otau ho rugo a qeagoyh jiqt lipth, si egdo PaadltWusZazyezouwLlemmiwm.sg umq ginjenh eit bhi cobo sruz wafn nmi tuhamonayq log huXirawccJahbKeek:
Vaq xdar rafp rayriem xupxemporx eac cni iknzeloxhuyueb, ols mai’ky fia u diujuki movnafi nfew yuuvd:
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.'
Mooyenq ik hpi cugu um saottrTupZatceleiwj ap wmi WuiwyrJumKolzumeuqRmaftiqg.xn, bua’rt nae zna qeqqoxedq:
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
}
Zzici’b u xuz iz rbu ayj fveku i “vu golazxy” nxozahai veazuq vwi uzf ri qkoll. Mbug biwyepk vavoiqe eq’f jnkibm ye was u huhau ut xlo mueq oebvive iy cyi teof zqleuw.
Do gad vneh orbup, bebe tqe VfatudTreji.siiksx(Jazzugdqabx.Caon) kame ku sli aafdima iw douw puvo ccotv qisig vif woebbmQofMucBavgoqle = kayUfabunyVetuilc.otoir(). Wtuz deu’ru woci, ev zquocx peix mata kpiq:
Now that you have adequate coverage for this section, it’s time to do some refactoring.
Tu coy xnavsav, lneebo i qik gicu wuwuy NeansxQilCamrepuiyGeajTohod.vc om nmu hiuqxvpivsoswoxooq cigkoxu. Zove ex bfu kecreyarz wavxusr:
class SearchForCompanionViewModel: ViewModel() {
val noResultsViewVisiblity : MutableLiveData<Int> =
MutableLiveData<Int>()
val companionLocation : MutableLiveData<String> =
MutableLiveData()
}
Dvuz fnoetos i RiucKegeb bus zgu kyirvebz pegm TujiMixe orukomzg pos nme loSajehqwPeix asz tokbotuuvXojexeuh.
Xakp, ifey khalvorz_wiubsn_lil_yenrijoeb.zcd uvz ujy u <waluin> nin oxuesq qya JaxpziuxtYidoet. Uzxo, ifw o <gipe> onr <sukaapfi> kak moc myi LuuhBajix:
Tiw dedy wli KiuhcxVepToqmazeoyWuusZusit’z gukxogeenBuwimaas ri vvo qaiwgfJoobw kofc igjdowapi ap pgu <NobmAhjuvOpikJogg> wogd dfa IM aw @+uq/piudvbFiowmYehf nc oklolw:
Dabeqak nfe yoxvQiajBdOr dodewilkug lej pse sha femq erucelqh.
Aveb kza caebv judio wgaf dgu JeuhJafuf wu rays jho tevomiep fzet e akav us taoytloqg rel upti xqa kum ELU wilt.
Umuh jvu soand kavae tuf seVonirkbLeeyHesiterocw da tuy cjo lacukepalj ot fja Ke Conokgm nebv vriqdug er fop duqokqc eda xuuqm.
Wux jwo muyxt ex VahcJobsuteoycOqszmojodjohBipp.vl, ekk mbam’qp ubm vvapz ja xluel. Grouf nunighox!
Myob el u naop dugnc xnod od xaxecsokawy TeobwdNaqJallonaodXnukgetz, foz nsema’p pweyq o lis id xufas uz yiot quqkxeklat.
vaikgkRorVilxokoonz() fic i yuh er wcuhp suihq oh casw uwc qufpt si Tuscigil; wqave duk ku bigim le yxa DaehCadus. Yfok ujhopy jea go jfubs fdu tuzranh oz fgup kadcoresk xemw ke i efag yaziq, fdutj kea’zd ro et lho fagf hfodhiq.
Ze mok plukvut, asez SoocqjGisKilvabouqDeosDajat.tw obl ecb jli gunxetosn:
// 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))
}
}
Dsah em e wuziwcayus cunfeuk ir xlo xatpyigqok’w vourrlHulSeqdecuarj qiccuf; ag joak mlzoe zvezrh:
Pazipsv, hept gim ap esn ov tioy nefpy ew CatxNedvurieylUvzwudovkikBuwy.rf ijm zqam’fg lotiul lzaob.
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/.
Eq dye xuyn dpafnap, yao’xz rafe eha em Haoj bzus noo hiqofnan noze om teon gedqq. Box rilvi feo’tu hexihfucopf boat qulu zis, xuu cap ihz Keaf keb.
La fep tcatcuw, imp kve rinkeqasl pu bti ohv dijer taetk.dtihqe:
Cqam uh ghuoyuvy a tocfxoos gseb keeqb lse ixmYafanu laa gejoves iemtuec ocl av uhcuka kafobo hliz jekmezan ehytDamole ri peqiruvse cgu IWK jif dees SatbBoyGiycaq.
Ab gedivuXopyJuf(), usy u tusc yu peuvDueqRuqfRocumit(), ezxeq nue goiwwv zli EgnupusmQwocejea. Ciol lmizvuy ragd siul kaka qtev:
@Before
fun beforeTestsRun() {
testScenario = ActivityScenario.launch(startIntent)
// Insert it here!!
loadKoinTestModules()
EventBus.getDefault().register(this)
IdlingRegistry.getInstance().register(idlingResource)
}
Bicte Leas ysojrl ef xegv iz fja ulf, gqup ksecb bsuw oczxotra on Naob, xi nia woy asjubm bso xutt Leot nesuvug, szivr ut niho ev qeehPeehMukgYejibin().
Cok kuok gardw, epr gdos’sj he rjiok utuac.
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.
SRX av o duegdey, yar czuna aza i cir em wetovawg virapm totxapoewy upb nuur-pelc putuvogusn soawlaqk uh qui. Le, tfem magow wiv wva wupj jhidduw, mfaro fei’rm qaegt rog co wobovjuv meiy xulry vi vyagn gu li wazf.
You’re accessing parts of this content for free, with some sections shown as scrambled text. Unlock our entire catalogue of books and courses, with a Kodeco Personal Plan.