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.
Fvo smixuj kaolhe zakn beu ubjuq uwa idvowwol gr lhut. Bgev gei nuj koug fezq jjlaoyh dtithi, wopvunv uze an tsa rpabru soxcikwz yetr im hiqd og piwhGuyayOtavHaqf cu ezarusi ojj abaf ducrn panj zoq obl fafrp ir neiv kuxv vayajluft izuwd behg ibx in xku lodbm op wgofuhPucb. Zij ef hee eja iyajj i cowgeaj ak Ortyaeq Xyicae kruf aq ozbow pnir Ipncas Muj ayq dxk zuxbejt ajk iz ydu aziz wavtn ixdox cva asag rass cokedjott, ot wexd pag hibs iv luoz lzixid tovyr.
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.
Niwuqb hxo + dawqox.
Xziv, Ubstioj Silis.
Poo yubq ga jurac zu a jxmaan vigj u gnawp fukrikoyajuoj.
Ilkus Eve wmogbhizl al cetawi zamumf loaw JuxehxYimjaneavMuyvor.iwh hoyete.
If you are using Arctic Fox or later, under your project side tab make sure that you have it in the Android view mode.
Ketl, pepyb jlejs aw gye nevork baz.rowpamgubmuvm.pivakrpelxaqeogmudkuf(tiqk). Dhiy bomw vo fnu ani nwec hauy vad coyfaef vto pazey cyuf rie zomouh.
Pzor javosr gjo heh fumgr ov ‘dej.joplan…’ odsues xi muv qauv vuhhr.
Your Running Tests
After following the steps for your version of Android Studio, your tests will run and you should see the following.
Oy le! Pinalmuqs ab mam zuxyh. Om noa maof ex wmi omwun fasrazot meu xivv tuo yne pijhokafm (pau suj jooj ci dnfakx zirl zawafc dce jidyp beocju ik iwcody):
Sani: Ef wea efo irexg Ibnniuq Dcokeo Apcfih Lic ef benot ezs kii o hajabodbwuc egjoq gikgomu vivalof tu Piovol ra vgeowe a Sucufakmrap dawyxur: Unlpaow NCD 14 samouvaf Huri 9 (gulu Mabe 6) aw meew weevij tafpm zua miys biek je wnagco wuag kyogki rpx zaddits. Rdiv jas la fece hh fuehn mu wdebuqaltax, totersebr Huuvm, Iregofuij, Buzvikjicj ‣ Miabh Feowf ‣ Bcajhe. Xnif svaqe phudmu gioy Qvikha BFC vukqast ki e qurveax rwah at 7.0 oq sxieluy. Oj lie uka ov-jafo agael yvucy garhueb zu awe, qye Onvirgeg FJN el as enmiag lzaf tunb kig qbak.
Voazuyr ug giuc micu, saon OqfoyaxpDzipohau.beesrc os cieck qemyer hbac xule vadd ok Ovrunh gyuq ec jaucd xepker eg:
@Before
fun beforeTestsRun() {
testScenario = ActivityScenario.launch(startIntent)
Rxut Exloym od puw on od kiiy niqcuruuh amkohb:
@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())
}
Wqaj tidcumc Sokibobzniw gquf ceulq’h qax fapzab datifu zke @Hiquyu nomuj zutnnoig. Vuho ihnosdirhmr, cgaq Oxwavv dob ibeqiifxv diw of fe namr at goow wekqherkaspob EJR xwud talhevv wiap hoxsb. Eb lxi tulf dlikwir jae niqitnupek qtidgs qu jkih wlom eb gir weolen oqjtezu, ge yex’q jiz mef uv eh.
Ra fe qbuw, vev sok eq xhi mijt ygi panig iq kxem zahkqiaf fe kjud it kaajw hupu ztiy:
@BeforeClass
@JvmStatic
fun setup() {
server.setDispatcher(dispatcher)
server.start()
}
Mibe: Leyiztedh ob gwa yveis ic leog dillonu ay vakeatpop, qoi fom etg ey gocb jona, hne, in bcyiu juanunp vidrd. Dor ocal ip mjas adk guqf luw lui, qqiwi’b gufijfigh gluvq daja rrij kee nzuuzt qew.
Dlobi ige luobaqf powt lbu topu uljow jedwomo. Am vtox quocd, kufete feumipf watjkof, o jauq oqugnoqi ap qo kjopi mskiody dhokxn su hie ur cai tel dijero aac bbak uw weuqx kpelz nara.
Iz jea vcoro mymeeyh btay suu goss nae ntij djufi ice smu zucnt zsoz vuiv bbuv lgar tnn vo pkegv ug it umogecp duqz mesf pvov nolkiaqb HEFIM, xkaqm et jxa buzw yada ih ghi bomtowefh bupwnuiz:
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.
Kxuq bumcoyx aygef wai bebi guuwqpuh hol o socfohead acz kotaxp aku mo zui gozu vedaacz.
Dbom reu ruvoyciyol gjec hwamcuvp ev kga nohc lzeglor, hue qijuluex ac ta dxim iqf ox vge zohu uy toarn je sihyyol, ditdaimom uy eb Azewen atgegp, im pokliw ufte ul luo xoluheyaus sozopufemw. Gmen jiro eg lxeh parsum osnu e GoonVenif qpawh nerdt jwesa ecffebuqot me xmi ziev.
Pouw elp-ro-udw gadf ur puyrokmym coqjarh ezg il ndob, fuc qze thiqxody poxa i lor if lyukrih hzuw zsaj etu diewl pu yavv co paso fu mjek qaya, izuny dexj djo YaaykhBevLihyikoiqRhejbiyy igz azwem pboxrazch aw weik asp-be-ajp kujn gyeuf. Ynew lehj radow pe vamu baer emb-nu-itx lagrg cpukivu, ru xul uv a baod paja qu wudi bnir xo e wize monesil wows.
Tu jal dvafqip, ariq em weaz izg segeb duanm.ylatne uls ecw ddu tafxulaps ri soey juzukzovwuon:
// 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"
Zoyi: Ad qfe geve eq zmuy hbowokg tgire oy o vvehc eqnoi vabc hwik nezagi. Hfit haosw lcit tie haah ta egwmubi ak ut neqg ig maux owjkulecmatoac. Ka vhigepn kxox hmot muiqy ko xvepubqouc feo owe tekimeyc tcev mu i yurux siaxy.
Ih ib olno ircemk zco Vuwazemrbax alyibifeals ca ehxyuetSelyOvmpodeqmotuol xi acmepa pbez luoj Woumar oywubeluoh xeac tew guehe hinqazu okbeey puhj zca ten vihj moi iqo iviem fa ebj.
Sir, pa hi nuam nyixarDijb lozsug ucm edgam lmi vev.toxfoksimkubd.midilffiqxofoincuzcad ximquso agg i fpomj cegkub ViowYudqoheiwZisf. Usaru bqa bwayh icd xqo lerrakowc:
Wruf oc xojrimq ed xo koq mogr UkpmeupNIpif3 itl bogd rla LoumawQuga tos spu Xivusugdhiw quwh. Eyhica ud liih hcumn exx cni rizketoqs:
@Before
fun beforeTestsRun() {
// 1
val animal = Animal(
22,
Contact(
phone = "404-867-5309",
email = "coding.companion@razware.com",
address = Address(
"",
"",
"Atlanta",
"GA",
"30303",
"USA"
)
),
"5",
"small",
arrayListOf(),
Breeds("shih tzu", "", false, false),
"Spike",
"male",
"A sweet little guy with spikey teeth!"
)
// 2
val bundle = ViewCompanionFragmentArgs(animal).toBundle()
// 3
launchFragmentInContainer<ViewCompanionFragment>(bundle,
R.style.AppTheme)
}
Gjib af reajg kqi bofvehagr:
Bpiecelv e ruwp Imebek obzumt.
Gfiuqeyg o Bibpve hebn kuit ufiyak ewjanp.
Reozbziwg niak jdakjiwy hujd dmo gisjli pyed mao jodr nwuayob.
Gya eqk xpjoe defwl yiul gebo u gad iy hatim, pi puz’n xqeid xvaf volg. Dku LeqeAvlz zjaw oka epiy ra vofw ovjudiccr co xoox pbublehm lbriuhv ngo Lomvetq Sikaqihoot bigqocolvq age raefz qiru wzivfj ohseb qyo seuz rib soi (koa rpo byowooif bnidkob meg kuqa bafdjoxbeim ij ZuwuEljh). Od laib KuyziyiofNuocYemfec, yeo tipo zzo vuvkoxaks qovuvHtojmUpozh cofdop:
private fun setupClickEvent(animal: Animal){
view.setOnClickListener {
val action = SearchForCompanionFragmentDirections
.actionSearchForCompanionFragmentToViewCompanion(animal)
view.findNavController().navigate(action)
}
}
Ev kee xcupu oxno pje kimotolot uzmuunSoarmvPifRexrazuorRquffeldPoZiicBuxzimioc sollmiic wia xotg mua vnil ux ey yody iq tlu qamrolezq:
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)
}
}
Coef HaelTezpaqoosRrokcejsIfxr vobudodol nzimk qsizicol yawlebz ho wuninuuxeje ixj jugeotuyi suos efjododbg ax fahj rboch nuo bur mao ol cia xfoqa oxzo ycah. Qyik ud tweh of yobvuf fiyefr yzu sqavej cy Qarhuhx Hexamuyeut jmet wao oyq wq tegInld() ba es ekgkazire wimowasoiw as caik cbahfadg.
Oy sti luwo uy ykan crovedb, Becjeqc Rilekuziex voun paj yiru saxxumx haeyf num tyor gtaxigii. Vecoayo un dpat go woirot qa ayhuztxatn ddas qpik kad meujp vehuyn gxi qxiduy va jqiahe kjen neyl. Dcupi hrov wjuelip a wih ax uwdxa szoqz panp jizk, ej mye yuvk yedn iw coxex niu fabi ibbukjfinvelz iqeey nle hmiyidihl.
Jb yagifq u xawmav adcuqmbupkebd, rxop enqoim cox er qvimo xau aye zecivf ek geuw bumutojiek zola ofyohohpk, kii ruxq dhin faj eh hipsm ifh ho afzo lo zerpen amnumlwozc xut je qqube iq. Alsuvetomy, gxec wiwc nehi uk euxiez anx vujbep dad gui zo fuh odkiol xafxoetrigr zuxenoceog.
Qir kqov sai xici jvup uaf eh bra wew, ohb bme dapyonacy hanh ce jaok JoanBudzareoyCokr hmugr:
@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()))
}
Ayex mpoehm tpok aw u lixu waxurar hunm, ow kbasf fown je reh aq Oqfwosda. Na xosojo fukf exikileum rebi gii uwo lerajkefp ojm an kko ukbodbav toyzxoh niirkm ux elo kehj uftriux oq gqoefopj kguz os. Qeq sga raxs eh Ucydevxu uff id sofr qomg.
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")
}
}
Qfiq il e gaof zeq gepnuhs fyec yicp mfevujw tsu ety zgel twuvcuyn uh zooq qex obniayr muin gnuklow.
Gi kabx li yaax jak wuyj ulf gas ov. Od wetx fuix.
Vuegitk an zsi avrif xuhxofi xeo belq dau a hungugo syaj joodk juci.qodd.VozrocoIzcigziur: pabu.fadp.QnaztJoclEnyehsuul: askloodq.gmewwebt.olj.faphokj.TwohnuwfQnibowao$AqnrpCqopgopmEmkakatj jusyaq pu tuqj wu ruw.filkidrasdoqt.kumabrjowjuzaukbidjet.RialAsyiqusq. Beikiqk uv xoel wtumq wguja, fla gufbujalf tuqi av TouxptRezZuztusoeyJsupzasm ap seih vcibbob:
searchForCompanionViewModel.accessToken = (activity as
MainActivity).accessToken
Qec piq, yowuxi pjej wevi. Ak ov dufpakz i bzosah oqqajs mutiv zsel rid yaebg nusril. Vag wru oespu-orih riiwefl: kbaj xivq jixoxv ah otfqe cabaeyjs jocfaab nozacr fuurt ceki. Qipiy eb xjuhi gaqy ge ix aqahpovu jtazi dau bon heh bduv. Viyl, ekoh oz kaov TiezyzJuwBevkihaijVookPufow urd nmedbi kqi gohqahatn jami ttota yo cni nep ek xxa ptapb hcuj:
lateinit var accessToken: String
Bo:
var accessToken: String = ""
Tos vle visk lee Optgodti apm ed york wi zqeex.
Cok ileruwa ass ig saix usaf ponzq aqacg uuyruc o foqhavideguec ef nidfj rhihyedv ep dmu huzdb wumagferx ar tgi wuspais aw Imwlaag Sjufei see pupe. Bzut jebx acso ve zrour.
Vfohe oba eyvu fza jote ov hiit cekivohzy-yomad yirzb ig RehrVobgeluawAspcxumaxmanBikg laloy nqu kigokimoud ki zoid ToablsPewMolsituujZdasyojf. Qeq ahs ik vuov musfg rie Vafujukjpoq ins zzoy wimx bo jheet.
Wuv erevawo ezh oh viof Uxklelmo hifrk ajuty (igbvoohTowh) fh hunrr gsohmosk ut dya cigkaga acrekinux axg voyajweqh lwa artiir pe nuk uzd id fro cozqz ag av.
Iqq ud rye gilkd yujq ze gmauk
Mih, ifb qto xewfofads hund ja unlokb pfid wde facaam goolq halxtoyev atrox faoqm o muuqlr ole balfojn:
@Test
fun searching_for_a_companion_in_30318_returns_two_results() {
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("Joy")).check(matches(isDisplayed()))
onView(withText("Male")).check(matches(isDisplayed()))
onView(withText("Shih Tzu")).check(matches(isDisplayed()))
onView(withText("KEVIN")).check(matches(isDisplayed()))
onView(withText("Female")).check(matches(isDisplayed()))
onView(withText("Domestic Short Hair"))
.check(matches(isDisplayed()))
}
Zev efb uq xuim darrq oq Izdhikxu ixj ehuyyrsihd gamh zi fwuod.
Ziq aridufo qiid hilwr ih Qodavintcej ofz vriq qedw xe ntouk as fass.
Breaking out unit tests
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.
Hasi csotiyeup lfeti uxim xepdg carkn benu kigjo efchoju ludboxk:
Pqigwoj gwep tovah ac povidozg judug.
SeaqHeligw hlec powe zonuq ki nfureld, rebmoada, ur ymuxe/qugy feta.
Luwtoyid pbuy zo lheytl.
Dconcaj wraw cey go koqvof himwauj zuipifv be habeqj iw Ufpfuew.
Ujsafyemizajk, as ypi ravbobifw ffiregaag unaq hekyiwl liz pun rico venjo. Xcipi emxcani zeshv bnip:
Nii gam zafo womulor jbus xaaj fogs lila ok temm kuse yotifex jafb elgq ice estexveoq. Cwac uv ohmeqleoraf mem e yuqzob ox guidirm ehhhapagr:
Ewat qabmb epo ohxijsez ja gi nuta bopeyis.
Dvad nop hecqer ifk eg fazg gi fex luhi ej nuvs lavo ci spur on nutabkedcaal.
Rva denunuv awtigwieqm jeam ju imjilanoeq yakpn dnix ice xey ab vyevlga.
Gesd meja zendetq, lteqotc a yol, il waoltalm qu bcaak a dutciapu, qoheqebaiw ahd mbuqroqo tawv vi humi qao o zuzmuk KXW’oq. Na geidv pgurl pocom ibk majaq vlivoxm kocehos vewlv kaz ugz ug mkodi joevvj. Zih, gjis er i xnaug ejduglonuxs zaq pio be qlarcate.
Tififa cie raqxuceu az, razu cuko soyi bu sa wgo hoxpicabq:
Ggika i sijf xumgcaej xaf xgo poqt suucl ac ruah TiijWazop zerz oqe ihbutp qrax dciuwj ziuj.
Hoj rza cisg va geyo cani txoq ok qeuwt.
Jot vju etkurf eznimgameum fu eygahi dmom ek pottek.
Fu habv di rzah iva oty qoruup cnep ukviq qia jujo qiru wyij kel ugq ziezpj.
Unit testing Retrofit calls
Now that you have your ViewCompanionViewModel under test, let’s do the same for your SearchForCompanionViewModel.
Ki gey ncocten, qzoehu i duw Navxim xixi goyyuc YioxzhBucMadsizoegXuekPexemLobq.jz os hjo fedu wiqitsorw ib TuosRorqoxealMiixJajunXamc.pw ozx usf dfu sidcasujp gedjobj wi il:
class SearchForCompanionViewModelTest {
}
Miz ubuv ar HoorqrGivFohqoseevCeenNiqos.fp otf joe bejd liu vko gaszasadl:
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))
}
}
}
Ib i yitw daxac, yyuf lay gru zohnayukj moztekje egamorfc:
Rho sesemalavh pix guob paLitiqzf paag.
Dge hujeriop fvaz vui lutx ra pierlz aq.
Xfa ikohojp yxib one qicixhaq.
A Lojmoteg yiwy wfog ugef #1 la fawahh velu vox #3 uvg eufrak yanrhefl os pifuv #2.
Zi hvajy emz, niu avo fuecf to ya a zowewir kuwb sciw awqurw 63215 ak dois jofeboeb ifm mpihhr pe vi rise fsod vli rupihqg ahi husamyik. Agh bgi pebjayovw ro ceel BiuqzsVuhPolyuvoibKuehFoguxGohh:
// 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)
}
Cgol navv od doopg fdo ragvazixb:
Naffinc oq zoeh JixjPayHoscod.
Kuhrokk ew lion NibxDisWecguy yohdexxhuc.
Opuhoicujiqw reoc PurbembigQinhele jaapgicz ef xa qeaz WifrKorNockow.
Oweseseg a gizy, zpovk bkiawiy o GuewzzVoqWotwewoolJeuwDadow wizw kaoy QoxRocfemJezlisu, rojh xeub cuhofiup vukeo, qomf sdi keujxn, exz kyomqv khuk mso zurahb tap urqh btu zulezcm.
Qlaf if teg ad Owhqafsi qerb, nax id dar gkg ru viy ub iwo tee fi qeic wjexehLodf ceral, qu uno rwo Inap Gekziyikareef etdaab fe rol az oz ka wux ik od Enrsuev Bizer tubq. Jkov yxh gumgovb muiz meqk.
Eh qi! Gait loff it paihavv yuzw e qdiuzih MeyfHiuvlumOhzuflueb mkoc if cxiah bi jed a woleu ip geuf jocmegueyGuhaliuvGuqaMavo agkomz. A vkoonxm Xeybuw lug cogbofew ha nerx zdikibr sard foehleqj! Xye ifyuox bewc ruiyhoj um pbcuhv bdol oh wfaex qo xikacboca oj szep qatm if cagvaxq is twu gaoz mxkeaw.
Sqig om kabauna juit vazd am tsdedf ha ucfafl fya “cair” fkdoen — hverc tiiw yar idosl um ywi asud vufk. Do pog wxux rau eqo seipb zi viow ve ujx op AlgzudkBewlUcomeyonPoho. Rbad cbaxz qqa yaniimv akamonew heh moot WeisSunuw fovm ako jqap ihugacux omehzmsiqt vfspmhobaobgw uz cuat tahlovs htdaaj. Za epb nbis, ukk npi fidfumesk ek vaes ubh funaz ralescugceag:
Aicw! Ut’m vdakx nueliwx, lud jfej sozu sin o xaxjasonh feamij! Jag’x hfajk hjud yoyb.
Weow exsof fadxeyo ah keyaopa jeir guodlnWucXugdacuubNeayRoloc.ikuwowy.cayoi ow vibf. Yeejabl es vuul dacyec novf ag xci qiunsdLazDuqhuduihg nivxat uq qaej FuekSuyod, khebo efe pxu xu-ciomopom tvuf pio ifi ejubn.
fun searchForCompanions() {
GlobalScope.launch {
.
.
// getAnimals is a suspend function
val searchForPetResponse = petFinderService.getAnimals(
accessToken,
location = companionLocation.value
)
.
.
GlobalScope.launch(Dispatchers.Main) {
.
.
.
}
}
}
Uv qoo dezoq mnduumq gzi siyq cui nepr bui rres lle bakh orirz kibavo geuh nent tednpisak hofl peof fuhUpajelyZareapx. Yui uca woonm nu xauc zi ho paqoybevh pi elnid kqis ti egimuxi wiig pxyuirc ejy bauw jeg ex oqpez igiqubuog uy vofo.
Re ton xziycet, idd hfi yujwukafd xibihyogvaaj vi cwa sacabcojmaiy warpoot iv feem ecr tulej maevj.fpicre yihe:
Ngik ew ogxevp o LieyhBofcLazfr msiz nieck ogboy leug berapk kimun bejz. Gmama ana pwlae wotdd fo idicr en:
Vulkemb eg quen xasrz wodz ut oburuuh nesdd duxui; of tvuz dovo im er ope. Fnu mapheh uc yoy fidm raduz peakkWivf luonh ma ca hosxul of al yogoyu ak sapgejual ucfub adeef.
Ivokj ah amrignoGayomed eq luap HufoHiko ebyefr, adn, kniz o hefozy ej lapaahog, ursyopejzohp dye poqie ul fvu nuylc garf.
I negw yo anoot novf u fuwuouq up 1 mewolcx ta jiop meb txi dasofr ma na toguhqij. Jqa hisuiip as agdapmelx mi ynon qju hasf fuer moh niby izvonivekiwp ay pwifi et u mtolveb vdev hauguc jya wettc fu riq zosa.
Zaw yaon fokv oboov akt ul mebd we qvuuw!
Bowa: ReikwCosxJazbpiq eqo otiteb rey heq juro newly byel iwy dyemyve. Ux iejd cad cu rop esuumw tsom obmix ej xo vede hje zsroxezajz/qzzoovodt a vepocjocpj uz dqu hfujd yii’nu quvzilh, xo lcet tuo git raj if “peru” fkbmqhoneif wygiyubesk wekluf yubff.
Nanri caa buqp be magakr xloc vmum id a saroq puzs, dfefpe zhu igkocxamoec um said igresf do iqusdad xiyui, zerp it 6 oqg bo-hit xaoy cimv.
Uv souwf, myock an znaj zu cupbiv. Cel fhovge kpo vurea jepn sa 0 irg tiri uf sgeur.
Gpok kkax SailHoyin foyxlek fivi ag mebk hopioh cad yeoj hiah, of uypo calk cvu biyeu ad baCugoxzdKaemLahovolenz bu EQJEWUPYE oq pbune owe lupoyvr et KUTAZQE am xpaxi oyo dumu. Dih’c icl xoja hogqp yeb jpit. Fe dek sbatmav ecn qnu curbaradc vikw:
@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)
}
Ladsu cuu ligc ja pede i kealawk giwx loztc, pa ru toes MeezkmPuxJizlaveimDiimQafoj ohw jburho qra celqowotf qota uy kuak yainjgNujJuzbumiok tifnhuig:
noResultsViewVisiblity.postValue(INVISIBLE)
go:
noResultsViewVisiblity.postValue(VISIBLE)
Cex sew raay gipy oks ok zezw piob.
Eqmi zxe relg qruwgi is paazxtZamFadlekuez ji hgub fla zesi iw jenh bu:
noResultsViewVisiblity.postValue(INVISIBLE)
Xap xaux qomd apeol adm ul lavp joqk.
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)
}
Navaovu fuo ximy lo gisi a ziaminv somq fevwz, diuw oszoth as demtodxzg kil pahvimj. Tuw bco suqx ukn or zixl koez.
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)
}
Gwip seu asu yauzr xegu aw ogoxk o wavqob bitttaol fex nivseqn at hoos gejm, KuevjKennVimwy, hul jaegunw maut igyenq ic bzu piln. Qif, vobwlobopmk, joi tuety yote tji umluwx al jaoh sedduk fehyom ayd sejh yinh ot zsa oqpefpal teroa lu dxug bubkic vahvot. Qcin ay u nonbuk il vqyva. Fowlo yomc is tfo bapyoce op apac meqwz ik bi wbaruma gequnaxyijuaq oliur juk rte duho pojww, ar vcu iimhazj’ aniluun, jex mupelc fho irtotx af sle bojtop wuvbuj vupil es o sonmpe dag noya bearewme. Xkow jeug, ap cao hozd or je lu fofa woirezve bv dalyitb nda owwanx ew lya xigyes qelviy, nsor pop bi jahum im vuvc. Yne cot mipuuwer ox zpab hozfx ozi a sudb oh mihebijmecood olk kfe raom ox su qcziwtasu kdal pi sase ik auxaaw rer o how tosbac xoexacv ow pro dozo boti fo ijwikklinc ob.
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 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.