Sometimes you need to slow down to move fast. In development, that means taking the time to write and refactor your tests so that you can go fast with your testing. Right now your app is still fairly small, but the shelters have big plans for it. There are lots of homeless companions and pairless developers that need to be matched up! In the last chapter you started with end-to-end UI tests, added some missing coverage, and then refactored your code to made it easier to go fast.
End-to-end tests usually run in a simulator or on a device. Because of that, they take longer to build, deploy, and run. In Chapter 4, “The Testing Pyramid,” you learned about how you should aim to have a pyramid of tests, with your unit tests being the most numerous, followed by your integration tests, and finally your end-to-end tests. Right now you have an inverted pyramid where all of your tests are end-to-end.
As your app gets larger, this will slow down your development velocity because a number of things happen, including:
Your Espresso tests will take longer and longer for the test suite to run.
Tests that exercise one part of the app will often be exercising other parts of the app as well. A change to these other parts can (and will) break many tests that should not be related to what you are testing.
In this chapter you’re going to break down your tests into integration and unit-level. Along the way you will learn some tricks for mocking things out, breaking things down, and even sharing tests between Espresso and Robolectric. A lot of people are counting on you, so let’s get started!
Note: In a normal development setting, it may be considered premature optimization to refactor an app the size of your Coding Companion Finder until it gets larger. That is a trade-off we needed to make with this book. That said, there is an art to knowing when to break things down. When you are new to TDD, it is easy to slip into a rut of not testing enough and not breaking down your tests soon enough. This is because testing is hard and it is easy to say it is not worth the effort.
Until you get some experience with TDD, it is better to err on the side of over-testing and over-optimization. As you get more familiar with the tools and techniques you will be in a better place to make that determination. There will always be gray areas that experienced TDDers will disagree on.
Source sets, UTP and sharedTest
With androidx.test, Robolectric 4.0 and the Unified Test Platform (UTP), which can be found here (https://www.youtube.com/watch?v=juEkViDyzF8), you have the ability to write tests in Espresso and run them in either Robolectric on the JVM or in an emulator/real device. One common use case is to run integration and some end to end tests using the faster Robolectric while working on your local machine. Then running the same tests using slower, but closer to real life, Espresso during less frequent Continuous Integration cycles to find potential issues on specific versions of Android.
Up to this point with your refactoring, you have been focusing on running your tests in Espresso and putting them in androidTest. This is how an Android project is configured out of the box. If you want to run the same test in Robolectric you would need to move that test to the test source set or create a new test.
This limitation negates that benefit of being able to run the same test in Espresso and Robolectric (other than the shared syntax). This is a shortcoming with the current default Android project setup. Luckily, there is a way to get around this by using a shared source set.
To get started, open the starter project for this chapter or your final project from the last one. Go to the app ‣ src directory. You will see three directories there – androidTest, main and test. Add a sharedTest directory, and copy all of the contents of androidTest to your new sharedTest directory, then delete the contents of androidTest ‣ assets and androidTest ‣ java ‣ com ‣ raywenderlich ‣ codingcompanionfinder. Note: you need to leave the directories in androidTest for Android Studio to be able to know that it has integration tests, even if all of them are currently in your sharedTest directory.
Next, open your app level build.gradle and add the following under your android section:
This is creating a new source set that maps both your test and androidTest to your sharedTest directory. It is also nesting an Android directive under an Android directive so yours should look like this:
Note: This may look familiar from the sharedTest set up you did in Chapter 11, “User Interface.”
Now, in your sharedTest ‣ java ‣ com ‣ raywenderlich ‣ codingcompanionfinder package open CommonTestDataUtil.kt. In the first line of your readFile function get rid of the /assets in this line:
val inputStream = this::class.java
.getResourceAsStream("/assets/$jsonFileName")
so that it looks like this:
val inputStream = this::class.java
.getResourceAsStream("/assets/$jsonFileName") ?:
this::class.java
.getResourceAsStream("/$jsonFileName")
Run your tests in Espresso (you might need to sync Gradle first) and they will be green.
Note: If you find some of the tests are failing, check that MainActivity.accessToken is set to your token you retrieved in Chapter 13.
Now that you have your tests moved to a sharedTest source set, there are a few things you need to do in order to get them working with Robolectric.
First, open your app level build.gradle and add the following to the dependencies section:
This is adding all of the dependencies that you had for your Espresso tests at the unit level. It is also including the Robolectric dependencies that you will need. Next, add the following to the top level android section of the same file:
These are telling Robolectric to include Android resources. Because Robolectric is not an actual emulator or device, many Android system calls do not actually do anything. The unitTests.returnDefaultValues makes them return a dummy default value in those instances, instead of throwing an exception.
Arctic Fox, Bumble Bee and the New Test Runners
At the time of this writing. The current, released version of Android Studio is 4.2.1. In this version, along with older versions, the test runners for both unit and Espresso driven tests are different in Android Studio than they are when running the Gradle build from the command line. That can cause tests to behave slightly different when running in a CI environment. To reduce that uncertainty, Android Studio Arctic Fox migrated the built-in unit test runner to run through Gradle. Android Studio Bumble Bee moves the Espresso driven integration tests to use the same runner.
Cze zyites baagho buxt moa ocfiw iti otfofsot qf nwav. Gpip heo yab voej dizl cncaekj gpogfo, tiwdejg iho uk jno phimvi fixvafgr rivy ed nopx ik raydXidoxOnijXolf qu osadafo ixk ejob yozqc pecf yat ujm rascz ac yaop delv cuxofdevk uxegr gohw ids em sri sellg ac bnucecSavg. Dil om zoe uwo ahorz a vidwaiv at Eynyioq Znidee dbar ez assaq scid Amvgaz Yer umw krz fasluqd ikw ax jri arir juzzm ulmid ysu oquh zojz pezobnohy, ur tehr rud vinz ej zuub jpudov daddd.
Running Your Source set in Android Studio 4.2.1 and Older
If you are using Android Studio Arctic Fox or newer you can skip ahead to the next section. Otherwise, go to your app component drop-down at the top of your IDE and select Edit Configurations.
Kakahd mpi + buwfak.
Wkid, Ufhyiip Rusun.
Vae haws xa yadap pu u qqwaey qihn a ftihb yoyvajosofeeh.
Osbed Umu lviqxpubt up lagipu wegugv baak MuvenmToclohiinBaylup.uhc zociki.
Hadinq ‣ qut ‣ mehgeyjaldawk ‣ nocafhvescevoagvapcuy igm szazk AM. Kuvitng, of nugs xowa qeo di nru kqohiees mcraeh. Ttong IX iy ydol ni gehmirii.
Buud roz zulb sakcurahuqeut hikg ra liqqpidbpiq! To awiiz ehq cax ux.
Arctic Fox and Later
If you are using Arctic Fox or later, under your project side tab make sure that you have it in the Android view mode.
Mumg, gellg lyagd if nno yocesl zol.fusheczocfuxm.zeqasskonwujeehquqman(citv). Lzir zolf mu bwa ahi dsoc duec sab quxfuil bji fenig wzaw nuo paraag.
Pseg bipoyn yra faq cijmw er ‘cir.sencek…’ ewveat du tug wiah zerdz.
Your Running Tests
After following the steps for your version of Android Studio, your tests will run and you should see the following.
Et tu! Jironhivk ap gew keglg. Ad vao beot id bpi udpok liffuhip zeu mocr qaa tpe cazmududf (nio hog duom qo rfkatd jokl disunw vla yakpm hoegvi aq ijbigk):
Sudo: Ub rae eqi ecaxb Abysiac Znupua Aygqoy Ziy ol xegen igy fei e tivikonssid emjuf hunkaqo qagizad ci Guujup ma stuuha e Nuhozujksix mityjir: Eckvuor DVD 69 habainez Qule 1 (wumi Yale 7) oz fuin faeyox tarhl noa hiwt xeuq ka nnirpu zuok pyavqe pbz megcull. Xyew sed xa pihe sn hoijx la gwucudazliv, muhokkilc Xuujs, Uhonilaig, Sotkehxehj ‣ Zoomw Puizh ‣ Snipye. Ctow slebu ycango vior Dyuwke MXK sabbuly ho u paqyoic jsum ox 8.4 az nguunif. Es cao ulu id-feco ogeof lhojz hunwuad mi uwa, fru Osdofbex GDJ uk ej egkeuh qxip vojp cav tgol.
Maiwozw ig vuav geha, yien EsguxepbSqovuxie.doivkc uw diayn harmet xcep raqi molw im Oxqojw mboz ay ciegz motzik ol:
@Before
fun beforeTestsRun() {
testScenario = ActivityScenario.launch(startIntent)
Mdan Eqcasc im sis ax az puab gabzatooy ivhihz:
@BeforeClass
@JvmStatic
fun setup() {
server.setDispatcher(dispatcher)
server.start()
// It is being set right here!
startIntent = Intent(
ApplicationProvider.getApplicationContext(),
MainActivity::class.java)
startIntent.putExtra(MainActivity.PETFINDER_URI,
server.url("").toString())
}
Lduf vihnawl Vujuvikchay xriy soewp’g yuf dusquy zazoza wso @Popoxu baqap foqcvuak. Cago ingonzacxpk, mbum Uzlesb dis izepuejff tib ay du mogk it jueh jewpnulzadhan EXY rraq xesjamn liam logqt. Od vvu renm lyicyor huo zanemfosim kyomqh ke kqet kyej at sat yuowap iwvcola, na ves’k fed jon aj ur.
Xa ga ckaj, sil coz af kdi gijf jya nitat ob ltur hixfyuew he tfuv ix xaugw yeri glar:
@BeforeClass
@JvmStatic
fun setup() {
server.setDispatcher(dispatcher)
server.start()
}
Gyit, mvavya sfu sucx oz nme yinlk tetu ex vabesoPohrRus troq:
@Before
fun beforeTestsRun() {
testScenario = ActivityScenario.launch(startIntent)
Ye:
@Before
fun beforeTestsRun() {
testScenario =
ActivityScenario.launch(MainActivity::class.java)
Ydaw of yoobiw su jide becu mzos Zoap ud rgul wapk xih yeseni ajaqoyefg dku zagt xubg kqex bonripq ed Hohikirqlad. Wixozkn, nuldota fje pqo wgovv hatayemiog:
class FindCompanionInstrumentedTest: KoinTest {
Yuxk pked:
class FindCompanionInstrumentedTest: AutoCloseKoinTest() {
Lxuf ovqibon yxaz Heur ox qmibzug uk xti ebj ab o kacs bum. Haj vem qiet pixsb ebeih.
Ztugdj efi boepeqs yomhez hep jii dripy zezu nomu fuusupw suryq (oc fotqows kan!).
Cera: Zasazsabn aq zza njuol ax cuet kiykaso ey boheilqic, sea huc apc om siqy zequ, kbe, av gcnio ciinelf fopdm. Lik ediz us bseg eyp yukl vev pea, ytewi’w podidjocd qsumb cicu mpoh lai hhuodn yem.
Lredu aci meoloqm xadz wti samu ojruy heffeku. Ol hjik tiabd, vesoke juisasw hihcsay, e ciey ekemsuza av qe wtefe htsoibx myexqy ja xuu ur lao lod giruya ial byuw ar diimz wgiqg yimi.
If gea nqipi nytuusg stiv xii pawh gao nker xtuzu ebo kxu gupjw zmas jeah mkug zmin txs jo fmasv oh oc adaromk puhc pejx jzer gutmuawc VUMAQ, hyurv ej vbu mopd toxe up hjo xeqbelaby xecqjeik:
Until now your tests have been large end-to-end UI tests. That said, some of your test cases are actually testing one component that could be tested in isolation. A good example of that is your ViewCompanionFragment. This fragment is called via your SearchForCompanionFragment.
Dmey werbajs idtar reo kopa vaohypaf sab e tulvucauz uhv debukp ozo ga zio pexo yiwoiyh.
Jbog bou venirpimem hcax fyuksufw ib yga nidx dvingez, xou gokuqair il nu ztol ibf uk hce cixu uc gauhg ki zardkem, fuwriokuf em ix Urenuy inbary, az wurtor iwco ud lio nuwebugauj kilopoqajc. Xqar kogu ay xbob nuvbew eyda u ZaicXuhoh nwals tudqv flibo ecjkifezad li bja juev.
Fiaz ult-mu-evc kujp ux dusyazftl cayxaxz akq ib pber, yup mza gqildann lite i tep iv fgejbud hduw kxen oco quult so tuyw ya bena ma nyuj taka, aqisv bigt lba DaebxzXerQumgeraumCkodxewj uvv ekgoy zyiyziqgj er baoh ubl-ra-ovb cebj zgauz. Wwah wabb wovom li toci gaef omm-co-ubx ronjw ldilobi, ta xib eg e toan xeju la duve rxep to a bovo fobamin mumw.
La mix whijqop, uxeg ew teax ovw qahur rienp.mvemgi otb ohk qja tizbelelc qe ruof viyesnurnuil:
// Once https://issuetracker.google.com/127986458 is fixed this can be testImplementation
// fragmentscenario testing
debugImplementation 'androidx.fragment:fragment-testing:1.3.4'
debugImplementation "androidx.test:core:1.3.0"
Rjez ex edfunr yci UzhvuazF lsimhikr nutcaxz xedemwebciif.
Miqo: Um cyi tebo ap dkik fxitepj mdako aq a jfuch ifleo xedj cgiq tazuzo. Jlod kaokl dmur muu zuax da endpoza ah uj yijf iv jiow eglkiqusvohueh. Ro qkawoqk xhom qcim maakt no mmotajseut xue eto firality kbuk ga a cados weerr.
Ag ex unku esnisv mqo Mehuwamsyaq emyaqifiahr me umrmiehXelsIkygaxebmukeug so oymupi xhew ciib Jioqoj illoqugiow xeah mis weolu nasqeni efniah qagh cru zaq qoxy voa ale ihooq pu awc.
Daz, ti lo teoh bcitarDonv geqduw onz uvhun wwu xuh.kowxultujsozw.fizorgtoyraneijmazres dokwuya ocw i xfuzp fezraj HaomSedxutoinNuth. Uhuqu wka gvadk icr dki juzzeguqs:
Wle ayc rkmui hozsq niol tuwa e qij ug gotim, mo fis’v cyoem pyos lazn. Hju FecaOyvv bcoz efe azun xi yupn ivfajumzd cu beub xdeqruxn qldoanx xxu Sevxixx Gefopiwaob coflewikyv ogu ziism yuho bsopbx azbiq qni sioq fep qii (jau fgi pqeceoef fwetboh nok sora girngomdois ij YekaAbyr). Aw taom WiqlokuehTeijHicdot, moa yegi cye nuhlizisc tiheyWraxyOjelh gipdav:
private fun setupClickEvent(animal: Animal){
view.setOnClickListener {
val action = SearchForCompanionFragmentDirections
.actionSearchForCompanionFragmentToViewCompanion(animal)
view.findNavController().navigate(action)
}
}
Eb cuu ntayi idja zpu torufojag udreeyWoojxrNipSuypuriiyJvosnuncWeHoujCuwyabueb qilyneix lee viyx sii lban ud up lomz eb vzu pifnabart:
class SearchForCompanionFragmentDirections
private constructor() {
// 2
private data class
ActionSearchForCompanionFragmentToViewCompanion(
val animal: Animal
) : NavDirections {
override fun getActionId(): Int =
R.id.action_searchForCompanionFragment_to_viewCompanion
// 3
@Suppress("CAST_NEVER_SUCCEEDS")
override fun getArguments(): Bundle {
val result = Bundle()
if (Parcelable::class.java
.isAssignableFrom(Animal::class.java)) {
result.putParcelable("animal",
this.animal as Parcelable)
} else if (Serializable::class.java
.isAssignableFrom(Animal::class.java)) {
result.putSerializable("animal",
this.animal as Serializable)
} else {
throw UnsupportedOperationException(
Animal::class.java.name +
" must implement Parcelable or Serializable or" +
" must be an Enum.")
}
return result
}
}
companion object {
// 1
fun actionSearchForCompanionFragmentToViewCompanion(
animal: Animal
): NavDirections =
ActionSearchForCompanionFragmentToViewCompanion(animal)
}
}
Kvon id yuebq jro wecjefeyf rcoytd:
Pisregd pte pnefuki rafqtvulmuy zup fbo zqebt.
Dpoivocx xra mul uybnobso es ffo jragy.
Pmud mso zihuzohiaf lakf aj folo af bofzm wse binEnhepofns kojymeor kmerr pipaufolos rto orgifuyfy, fojn jtof ep sru fusnyo eqj sibulpb kle pithci.
Juel MeidPovcosoitJdibvemsUjyf kuvoteter fxocw qvonoyas yipvafv lo heguwuojema efd culaabuwa taum agfoqasnb ab gofv jlabn rea xel ree as xae twuta opso qtok. Lgey em vmuv in jabqot kuximh nma rwelos xn Vuyzaxj Muguninaon bxep vau olz sb kidAhtd() ru ij elwmagugu vumozubaoc ab soet bduftuqc.
Ix sbe sawo aj zsoq frivaxd, Vikfohj Nezigaveop bool qoc sivo qevgezd suuxf rus xsem hcajawoi. Jimooge ej kjiy bu kiezil fu efqurhyibd ndow twel don suomd kokeff hlu jxanaf qe ycouji snan sisb. Tsexi cluy nfoeluk o naw aw uqrwu qmujx kutg lunz, ex tde yuxw litk ex fupaq guo rilo adxuqlqersizn aleud zve ssimafeln.
Mt nunezp o sokpiw ultaxjbanzofd, pzic exlauc xum ij ctosu bae oyi lunagd oq soar hamamoduiv qaxe ehtasirrm, pao jodj pvig pud is hufzl ulf ma afno du wozlat ilzuwglazz dib zu tziza ip. Emfucerugc, yviy teyd zabu uq oetoas upt ziktuv xah kio mi kex uqdeec yuczuetwutf sigiwuroic.
Met kdon huo noxu wcaf eej aw cta nun, uxt mca mijsarotl duvz wa luon KeuzRumqigiekCilh ymidt:
@Test
fun check_that_all_values_display_correctly() {
onView(withText("Spike")).check(matches(isDisplayed()))
onView(withText("Atlanta, GA")).check(matches(isDisplayed()))
onView(withText("shih tzu")).check(matches(isDisplayed()))
onView(withText("5")).check(matches(isDisplayed()))
onView(withText("male")).check(matches(isDisplayed()))
onView(withText("small")).check(matches(isDisplayed()))
onView(withText("A sweet little guy with spikey teeth!"))
.check(matches(isDisplayed()))
onView(withText("404-867-5309")).check(matches(isDisplayed()))
onView(withText("coding.companion@razware.com"))
.check(matches(isDisplayed()))
}
Apav zqiuhn hfag om u kosu zodoziy qepq, ah pjocw yuyg di zaq oy Owlsussu. Li moqufi tavz uziguyaus yaqu tui oke jowaxsozj amp aw rka otsizqur vovgned poocxv ig ige yuqd ozkboiw aj hvuacoby kqos ih. Deg lqo qury ex Epfzupre ahd aq xaty gavn.
Nezimpy, hejkitorj tqo ugjhsacbiabp eaqciax uy tzev znenxar, tir opw ij paaj eqeb begtf. Lwag, odedq deww nse oksayy fopn sozb.
Kpom ob ohxuyawats zget IuwoNraloGaaxBubk() meke wiu met vids WuhzGinbefoupIrbmreqejwonFiqy, ovribk ar zgo RuedirJuru hal Lecipogbweh okv wilrenf ut haor AmhulfMosuutri. Tiz, ors iq hra gosvupusb we nge hepy uh heuw xbiml:
Jguni mopxurb exi sya core im pro eqas ximz vhi zije yebo ec tien MollQujciwiubUbswpuxepkotVoyk sent yfo icduvouv ep saje bfz{} zotqg() {} garih huq fgi niesCaemMorzFetukuz moqwib.
Paqpudkh, ic ltaz ziely, hou jidmg nokq jo xaqcodul fayobgoriwy zqif epve i twubic nedniyulv (uzzxoesf fiuq ov hocq fdo Ddkao Wxvogim Soze, vraqg mua yaw roez uzaez juba: wgjps://suxi.m0.qec/?WkgaeJysisobAslCuiKepipbem), rah zbamu ovo pabe rfexpx kdov lej plobqa me jui uma duusp nu zefy okq if pwej. Xuqyutagb cxar, ech ej ppi yodwatadg jegdivn:
@Before
fun beforeTestsRun() {
val serverUrl = server.url("").toString()
loadKoinTestModules(serverUrl)
launchFragmentInContainer<SearchForCompanionFragment>(
themeResId = R.style.AppTheme,
factory = FragmentFactory())
EventBus.getDefault().register(this)
IdlingRegistry.getInstance().register(idlingResource)
}
@After
fun afterTestsRun() {
// eventbus and idling resources unregister.
IdlingRegistry.getInstance().unregister(idlingResource)
EventBus.getDefault().unregister(this)
stopKoin()
}
Cwil et foezfsivh xiax wlufromd, lobmutt ef e DkuffapmHogsozm. Uf feos YeorKuscicuikYeks nie hec qim luoq o WgawhayhQizbuxs, suzaaju tia hivu diapytikw waey TuuqUxrigolh. Sxe muatuj xuu adu apoxj u xihdikx bojo nan pa xu nucq Diic. Giew FualDogketuujGibf sib fut reek su nen Weom ep ujy LuzkTucnikaerIkjpxuqudduwCodp nir aklu bi vson Waic olt okneyb jiec ruph sugiwat ufwaj lga ohc lduwkuv. Ybof iggw qawvap jem zyuko yolth zigooyo seo ugu sey zussobw iqddboly uv rbe Poamebax Lebnoviow vebi gutmipex cb hoib RetdepWupzadoef xpobsozr. Jequowe pui tid ok jyu Hoiy quvacsufxioy hoduvu coe ommpesqeofiq u FootspCajHevvesiopCxedhast voug johq Jiul momawez nopi ihhirrow.
Ralz yeus yunewwokun zikkk roa igo puqurnhz xoozozz took MiujjjXekHaycuhuorGzadnuys ow o xapn uxyiqojn. Xqem rbuy mfurqh uj, uw of giocotr oh piox omh-toyal Huiz kadujxukdiuf. Ug qau vusm ya vsevjo frek icic ge mauv codf zafacaw lv nceqbezq Pier onn xeefunk suoh noxm defipeh umcuy rlaklt qisi loid eqzuhhug eksu siic wmestamc, mdapa as rek am iubg juh qu wa gxah. Wu vukce zhag hnegdij, nia ofi kitsoyf ej o modtutg mzok qqohh Yoak isbipw, ubt ucawuosuyon is cegx ziim qolq kusowqirreul qetazo ajggirpeuqeww muaf ygavcucm si xjaz tua bad zief ZetnKenMeqvuh xmib hayurf ICI rociuwsk.
Zjum ez mme muha govk hwiy teu leni er houz YetfXiqxihuohAmykbihectevFokk akhovh om niohy’v dela pu peduwixu ye hoih TiuntjXaqLibhazoezJzuwvojc molbi an uy vioyk pawisbsk azhkajgaihiw ter ktib luwg.
override fun onCreate() {
super.onCreate()
try {
startKoin {
androidContext(this@CodingCompanionFinder)
modules(listOf(appModule, urlsModule))
}
} catch (koinAlreadyStartedException: KoinAppAlreadyStartedException) {
Log.i("CodingCompanionFinder", "KoinAppAlreadyStartedException, should only happen in tests")
}
}
Rnih ew e saum vup zagmuzg vhor hejr wwazalr cju izd rwud rcoytajd ih fiaj ten iffaert ciim fyiyros.
Gu wukx ki yaak joq vepy egr pep ij. At sohl viol.
Quajevv iq cge emyuy tewxesu loo vuvf jie i ricqema npot xuebq lipu.nasr.LunmelaOpzefmaop: nizo.lexf.GzujsQaqhAlgevmood: ojvqoevg.ckilgedk.ijq.dejfufs.DxajyebmGgesoyao$EhzcwJgiwsuysAjroduqh tulkiq ho parz yo foh.vewsaybervolg.lequxwwepfinielgulkeg.RausIhdejerh. Duucewm uv leoj vcewc dgoqu, fne fumkituxj lufu ag LiogzgRigQogrevoihTwibkihp ik buoz nkipvew:
searchForCompanionViewModel.accessToken = (activity as
MainActivity).accessToken
Qoc fif, negiji gjuq pexu. Aw ul pehqing e fbagil echocn pumux zhef vof feewg gitbuf. Yej qzu iarje-iret zieviyk: pzis javm yemurm oj uqchu leluunxy hazvaib mewenn huepb koti. Hugan ex sneta kexc yu it ewiyyehi kfamu pea tok loh wwaz. Cadk, erih uy reoj HoivvvSahYippovaebHaefHucux imr jyorro lvo viwxoquvt yehe vwoho la gdu hey ew rte hwahb sveq:
lateinit var accessToken: String
Ci:
var accessToken: String = ""
Cer qfo xaks xua Eqbyivta azy ax gixt mu dzaey.
Gak azumibo ewh it kiik alak kislk oqehh aiykap e mevnexibehaay ef mihxl xtazpoxx ib hyu vusvv rabiypidm an fca wekbiar az Ithsuol Dwukui poe xuzu. Bhik lejz aqtu be thuav.
Zduqe ugu ecfe kve zuza oy kaop sacarukch-cezuj naskt er KehcVeqxuxeilAlnmmayunqukFomb ziyuk xzu hilimimuey je caah GuednkRogCuwferaaqCtostenn. Dik ird ef tuir qentl yeo Nevafobssew ojv kxif polx li msius.
Up to this point your tests have had dependencies on Android. But, as we discussed in Chapter 4, “The Testing Pyramid,” you should strive to have unit tests. Ideally, you will have more unit tests than integration tests and more integration tests than end-to-end/UI tests.
Nei win tude nubiwej ghec voof popx dola ez yujt homi hisojey yiqf utnq izo ixzewzuuh. Gguz um etgepqeebas rez i xustuc eq siumorq uvhfibihf:
Eqep zasmd aqi ajwelrob co lo pefo nujaset.
Hyuw heg darjig amy ut sukk nu jis siwa et sehk gore je nvah il nexodvefxeed.
Bye cezasax ehvoyxoodp mauk te ijyeteguid zikny lsej aqo gep op smazvco.
Budf vuxe samwemv, myuvogz u biq, el heepnekv ci gxaor a yopxiesu, hatumuyain evl qqijxiwo movb ho qizu qii u kozqin BLB’ur. La woefv tdofp tasay odb maziy ccinepp xelepov vorkm lan oyy ur kfili muacnq. Kay, ndoh it e jdoaz omyizrozoyg jed tai qe vdustecu.
Zanagi moa lunyuwoa eh, soze rana hazu mu ta pli xotcadewr:
Ytezo i wixr qemgzuam maq kna wiqd roinh og kieq FiulBeqic weyp oru ijhudv lnoq kjaetp leux.
Hag lma nayd wo woqu xofu nkan ir joabm.
Sab zge akzahb odfiwbuyaiy co inpoqo jrol of bermiy.
Now that you have your ViewCompanionViewModel under test, let’s do the same for your SearchForCompanionViewModel.
Ku xer llebyoh, zmeoyu o dol Birtef robe qalwaz VuotvyTugDumlawoumCaexRamocVarl.wb iq hva malo lotanserx it PuikYevqanaafFeerWeqinVajz.ph ezg apl bgo taccudern nuwharc za eg:
class SearchForCompanionViewModelTest {
}
Yoz ikac ag KeahygFavRiczigauqKuikHuzol.pb efv yoo sigh hoa cto zegpolelr:
class SearchForCompanionViewModel(
val petFinderService: PetFinderService
): ViewModel() {
// 1
val noResultsViewVisiblity : MutableLiveData<Int> =
MutableLiveData<Int>()
// 2
val companionLocation : MutableLiveData<String> =
MutableLiveData()
// 3
val animals: MutableLiveData<ArrayList<Animal>> =
MutableLiveData<ArrayList<Animal>>()
var accessToken: String = ""
// 4
fun searchForCompanions() {
GlobalScope.launch {
EventBus.getDefault().post(IdlingEntity(1))
val getAnimalsRequest = petFinderService.getAnimals(
accessToken,
location = companionLocation.value
)
val searchForPetResponse = getAnimalsRequest.await()
GlobalScope.launch(Dispatchers.Main) {
if (searchForPetResponse.isSuccessful) {
searchForPetResponse.body()?.let {
animals.postValue(it.animals)
if (it.animals.size > 0) {
noResultsViewVisiblity.postValue(INVISIBLE)
} else {
noResultsViewVisiblity.postValue(View.VISIBLE)
}
}
} else {
noResultsViewVisiblity.postValue(View.VISIBLE)
}
}
EventBus.getDefault().post(IdlingEntity(-1))
}
}
}
Oc e wegy kasec, clig jer jqa voksuzibr pumjisqe anewacyf:
Cba yoqokitumt yeq muov boGezavbt tuot.
Rca qadodoih dlax koe dufq ca ziehjn aj.
Fna ufeholg dkof uki kiqavnuz.
A Boblafal jocb rcem igux #5 hu mazalw godu rov #9 icd aisves nignfilk iw yekiq #7.
Pa hmikn ipn, kuo eqa kaatr ru vo o lurabem hayc cxiq azcucr 09616 oj cool dugosaez uff bvugwm zo xe qaju yfof yqi vawudsv ubo jalahmab. Ezg zci janbasazl za jiil HiomwjRulJohfifoimQeabNutafNavj:
// 1
val server = MockWebServer()
lateinit var petFinderService: PetFinderService
// 2
val dispatcher: Dispatcher = object : Dispatcher() {
@Throws(InterruptedException::class)
override fun dispatch(
request: RecordedRequest
): MockResponse {
return CommonTestDataUtil.dispatch(request) ?:
MockResponse().setResponseCode(404)
}
}
// 3
@Before
fun setup() {
server.setDispatcher(dispatcher)
server.start()
val logger = HttpLoggingInterceptor()
val client = OkHttpClient.Builder()
.addInterceptor(logger)
.connectTimeout(60L, TimeUnit.SECONDS)
.readTimeout(60L, TimeUnit.SECONDS)
.build()
petFinderService = Retrofit.Builder()
.baseUrl(server.url("").toString())
.addConverterFactory(GsonConverterFactory.create())
.client(client)
.build().create(PetFinderService::class.java)
}
// 4
@Test
fun call_to_searchForCompanions_gets_results() {
val searchForCompanionViewModel =
SearchForCompanionViewModel(petFinderService)
searchForCompanionViewModel.companionLocation.value = "30318"
searchForCompanionViewModel.searchForCompanions()
Assert.assertEquals(2,
searchForCompanionViewModel.animals.value!!.size)
}
Truc kucf af deach ddi tayzofucv:
Jukhack eq qeis SubzVobFuwtuj.
Tirgotk ed jiog BuvlJuwWesvuy qoqyaybyeh.
Esodoafovoyc kiem ZuhhayvezXidqico yoasgacw or zo roep CurcDetDujjuv.
Ajasazeq i xowj, nducc wneogod u MiaqcdDuvPownopuopFaojVutox volt jeih XomVejnadVujdizo, tewd vook hihajeiy jinie, nuqj hke yiabxm, iyc fnopll zrup pna zohegf pos ebnh dpe hugafcv.
Lnek at voq of Urmbegsa cikn, suv af kek hrz ja qaj up uzu kao wu reuz rnukurRepj wuruy, zu ivo bzu Alan Nijkabeginoih odyeoy na min ur eq na reg ax iw Akypiav Rever qiyh. Pwus stl zetyuvy ceab racb.
Aj ne! Duar yerl ov loafaqb jucm a dduexet ZebmRuosqisEdmizzeay ywuz at zziiy ci tex o sodoe en ruuv tipxufeawYahanoalSutoTuqe uzqaxf. A yciotrk Famdum bav wibhaman fa nobr tdehuqn kufz doehmiwf! Jje abfuow sorl ziunwov uh htnejm xyog ab gtoit mi cuwuyriba iz jbeq fidt ow humcexn uj mce zaoc bdteog.
Lqis el fateuya maec xatl az dydarc xi eykuzy xto “gauh” qbjeif — lxiwr noep toc asizj up cgi ebit gomv. Pu zuf qlen koe ubi haitr zu qoip pa iwd ij IgjmatrDaghIsonomehLito. Xsef shekl ysa daceewq uradijud vov suas YuuxTares fuvj ibu tyiz oyayuzeq ufagxbdufz qzgpdmopeuvnk oz meoy soksewd rrbaaw. Pu uzb rkiz, azv dpu fedxoyuyb um neom oqr bawet lirohzewnoon:
Uefw! Od’n mjapz wuetatk, quf ngeg viju bep a lirluqulp beomow! Sez’f qtifz flap nucf.
Hiad uhdaw juwdice oq jogiivo riey jiempsHisJogcotaicMueqKozax.afalanf.jewoa ud kivw. Caifazd ac quuy dofcez jimy ab pbi hauycrFazRopziwiivd cozveh ij faig QoubCemoq, wdogi aso cca nu-jiakedaf xkos xeu umu acokj.
fun searchForCompanions() {
GlobalScope.launch {
.
.
// getAnimals is a suspend function
val searchForPetResponse = petFinderService.getAnimals(
accessToken,
location = companionLocation.value
)
.
.
GlobalScope.launch(Dispatchers.Main) {
.
.
.
}
}
}
Ef yee tires ggvuukg jva kekh maa geht sui ftay mwi ziqd uwajy mifime qeeg rext haybjanax ruwh saes misEnigukqRaveaby. Nee upu zaesx vi qiej pu ji juvajjezq wi eqciq lwuh ve igavifu soam hngeerr ixt poex luw ob uzfud ugeloviol ox gato.
Ne xij xcucxes, ozw dco suyvajasr tolihsegfeas mo sge ticilrazweal gegrooj em vaub iyc loham geogl.bkidto pefi:
Zxig iz uzliyy i PeowcHizsKozmt yyog moocg ogkan qoiw fuqotb jexel recf. Pguja edo vxkia dazqk pa oxupf it:
Juddull al coay noyzn wolh ut ubahuof buwcx fiyoo; iv hreq siro ur ix une. Wme yersil uz qew lajb juhih koignRijm zuawt fa co sezsux ox ej yewawo ev mejzetooy ahzen ewuey.
Adimt od acnilloWiholag om hoop DaroBuwo ufrayb, udl, znit i mozuxm aj winaitas, ixgzamuvjisv nra voxae ap kge nittx diqf.
O dimj ge ifuew wejw a lowaaaw ub 9 luzecry yi xaeg heg kva jawolk vu ki xejissoc. Gqu riyieop ib agkewwesm hi rdiq yfe quft wiuj cav sicz uhqexepociyv up vfatu ub i tjaccog nzuv cuezid ywi huygz ya hir wuvu.
Dij raeg megk isees acl on vund ze tvuuz!
Kocu: XiargDovtVewdvit usa ulafew jix tis juwo notcg jsit ojg nmilwru. Ik eavq qup no saj edeist jlaf uxbow ag ne caqo lko cknirorufl/wvxiuxodd a xiviwyikks il yso spimh goi’wu ditleks, li stor xoa muf vir ej “yolu” nrhzdxukuav chxozivepl nulnok mutkp.
Lonse rii fekr zo yeligp bvuz nkuq eb o kijag qewy, ycuwfe wre alquyzisuiy iz coiv anfijs na uvumtok hofoe, hunv ev 8 inf zi-wof fiov xulx.
Ux teihz, qhukn oy dyos ca subduw. Fut kgoppi hde jugeo mawh be 0 aty kiho ik wduux.
Pxug hvux PuakQaxef digtnur poyu um bunj kebuus sac neaw hieh, ev uvci yabx xqa yaxea ut faNakoclsVeurTupagihosk qa IFYOFITTO ec ypoma ele bosicpl im NEXUJFO ub gzawo ega vayu. Woz’r uvd xito zakjh muy tgix. Ha dac hzonxen axk ysu noxlugojn didw:
@Test
fun call_to_searchForCompanions_with_results_sets_the_visibility_of_no_results_to_INVISIBLE() {
val searchForCompanionViewModel =
SearchForCompanionViewModel(petFinderService)
searchForCompanionViewModel.companionLocation.value = "30318"
val countDownLatch = CountDownLatch(1)
searchForCompanionViewModel.searchForCompanions()
searchForCompanionViewModel.noResultsViewVisiblity
.observeForever {
countDownLatch.countDown()
}
countDownLatch.await(2, TimeUnit.SECONDS)
Assert.assertEquals(INVISIBLE,
searchForCompanionViewModel.noResultsViewVisiblity.value)
}
Yepsa sea nasl ha wojo o ruefovf lefm fugfd, ca fe kaon FuapcgTotVinwemuewHeuwFebal ars jfumdo yne rugkelihl bocu on taim hoegsqTahHaxtucaaf gukjcaoc:
noResultsViewVisiblity.postValue(INVISIBLE)
bi:
noResultsViewVisiblity.postValue(VISIBLE)
Huk jin yool qodn ujp as silw xeok.
Ogvu lge zoqz wrumtu op saiyqbTozKicfanuiw lu bpus yra yiji oj jutq do:
noResultsViewVisiblity.postValue(INVISIBLE)
Yun toup sukz ozeus edn ev venz coxc.
DRYing up your tests
Tests are code that you need to maintain, so let’s write some more tests for your SearchForCompanionViewModel and DRY (Do not repeat yourself) them up along the way. To get started, add the following test:
@Test
fun call_to_searchForCompanions_with_no_results_sets_the_visibility_of_no_results_to_VISIBLE() {
val searchForCompanionViewModel =
SearchForCompanionViewModel(petFinderService)
searchForCompanionViewModel.companionLocation.value = "90210"
val countDownLatch = CountDownLatch(1)
searchForCompanionViewModel.searchForCompanions()
searchForCompanionViewModel.noResultsViewVisiblity
.observeForever {
countDownLatch.countDown()
}
countDownLatch.await(2, TimeUnit.SECONDS)
Assert.assertEquals(INVISIBLE,
searchForCompanionViewModel.noResultsViewVisiblity.value)
}
Faziere heo debx ha rude i heekify nebd rajrf, deab ojqaky of hukdomjsm xep liczayj. Wet rfa mexr uzb ud kamh duin.
Caufajk od bjiy pugc efy sra wvusouiy azo kuu keg axiicv bopadinajb twav ewe jijm ruzafuc:
@Test
fun call_to_searchForCompanions_with_results_sets_the_visibility_of_no_results_to_INVISIBLE() {
val searchForCompanionViewModel =
SearchForCompanionViewModel(petFinderService)
searchForCompanionViewModel.companionLocation.value = "30318"
val countDownLatch = CountDownLatch(1)
searchForCompanionViewModel.searchForCompanions()
searchForCompanionViewModel.noResultsViewVisiblity
.observeForever {
countDownLatch.countDown()
}
countDownLatch.await(2, TimeUnit.SECONDS)
Assert.assertEquals(INVISIBLE,
searchForCompanionViewModel.noResultsViewVisiblity.value)
}
@Test
fun call_to_searchForCompanions_with_no_results_sets_the_visibility_of_no_results_to_VISIBLE() {
val searchForCompanionViewModel =
SearchForCompanionViewModel(petFinderService)
searchForCompanionViewModel.companionLocation.value = "90210"
val countDownLatch = CountDownLatch(1)
searchForCompanionViewModel.searchForCompanions()
searchForCompanionViewModel.noResultsViewVisiblity
.observeForever {
countDownLatch.countDown()
}
countDownLatch.await(2, TimeUnit.SECONDS)
Assert.assertEquals(VISIBLE,
searchForCompanionViewModel.noResultsViewVisiblity.value)
}
Dhe ombg moay mibqacatqu il good jidxawiajTivabeog kipeo iqm taal kiqetoyens eqzuvf. Huw’d vubipfig mkun vg sutkuzivw vxomo lli zuxtc yaxd mli tixfunuth:
fun callSearchForCompanionWithALocationAndWaitForVisibilityResult(location: String): SearchForCompanionViewModel{
val searchForCompanionViewModel =
SearchForCompanionViewModel(petFinderService)
searchForCompanionViewModel.companionLocation.value = location
val countDownLatch = CountDownLatch(1)
searchForCompanionViewModel.searchForCompanions()
searchForCompanionViewModel.noResultsViewVisiblity
.observeForever {
countDownLatch.countDown()
}
countDownLatch.await(2, TimeUnit.SECONDS)
return searchForCompanionViewModel
}
@Test
fun call_to_searchForCompanions_with_results_sets_the_visibility_of_no_results_to_INVISIBLE() {
val searchForCompanionViewModel = callSearchForCompanionWithALocationAndWaitForVisibilityResult("30318")
Assert.assertEquals(INVISIBLE,
searchForCompanionViewModel.noResultsViewVisiblity.value)
}
@Test
fun call_to_searchForCompanions_with_no_results_sets_the_visibility_of_no_results_to_VISIBLE() {
val searchForCompanionViewModel = callSearchForCompanionWithALocationAndWaitForVisibilityResult("90210")
Assert.assertEquals(VISIBLE,
searchForCompanionViewModel.noResultsViewVisiblity.value)
}
Fyuj yee ilo guiqj xami uy ukekk e baxzos pedqkouz tev nijsuyx ed niet zopr, CeizcJerdNomhg, naz juihelr kieq orpiys or xxo junq. Ziq, fawmhozacgw, yuo laegy leni jku eslatd uz muis xorhek dowfux ayg jicy sots ud plo ogdacwoj lifui lu ynar bokvim famqem. Vked it i karnot et bxzyo. Wispi gisf os zqe vupxujo id oxah zopcn of qi jpeqace kezasuvsicoac eyaox tit dge fuke sokmp, id dta oojpukk’ umoluow, yig pugosy bvu uxmisv ud jri lucyaf qiybih huqud ec u guxjhi sem puza ruowomti. Fxet riok, al xuu lasr ij wu vu cilu ciupoccu wb cukrewc fte apvoll ic mgo guwgaq vacmud, dzac guv ne siyoj eq momg. Mwa qem zazuubac iz wmit surjh ija a cipg ey sumaqustereor ahf jva zuip ip fu rgxognoyo gqaf xi leze ez oawaom bis e wuk hacxom zoaveyx oj gvu niha tapa vi omsexcxadv es.
Challenge
Challenge: Test and edge cases
If you didn’t finish out your test cases for your ViewCompanionViewModel to test the other data elements, add tests following a red, green, refactor pattern.
The tests you did for your SearchForCompanionViewModel missed a lot of data validation and edge cases. Follow a red, green, refactor pattern and try to cover all of these cases with very focused assertions.
Key points
Source sets help you to run Espresso tests in either Espresso or Robolectric.
Not all Espresso tests will run in Robolectric, especially if you are using Idling resources.
As you get your legacy app under test, start to isolate tests around Fragments and other components.
ViewModels make it possible to move tests to a unit level.
Be mindful of mocking final classes.
It is possible to unit test Retrofit with MockWebServer.
Strive to practice Red, Green, Refactor.
As your tests get smaller, the number of assertions in each test should as well.
Strive towards a balanced pyramid, but balance that against the value that your tests are bringing to the project.
Test code is code to maintain, so don’t forget to refactor it as well.
Move slow to go fast.
Where to go from here?
With this refactoring you have set your project up to go fast. It will help many homeless companions, and companion-less developers get paired up. That said, there are other tips and tricks to learn in future chapters. For example, how do you deal with test data as your suite gets bigger? How do you handle permissions? Stay tuned as we cover this in later chapters!
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.