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. That said, there will always be gray areas that experienced TDDers will disagree on.
Source sets, Nitrogen and sharedTest
With androidx.test, Robolectric 4.0 and Project Nitrogen, which can be found here (https://medium.com/androiddevelopers/write-once-run-everywhere-tests-on-android-88adb2ba20c5), 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. Delete test, and rename androidTest to be sharedTest.
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 main androidTestcom.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("/$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.
Now, go to your app component drop-down at the top of your IDE and select Edit Configurations.
Select the + button.
Then, Android Junit.
You will be taken to a screen with a fresh configuration.
Under Use classpath or module select your app module.
Then under Test kind select Class.
Now, under the class select the ellipsis …. The following window will pop up:
Select FindCompanionInstrumentedTest and press OK. Finally, it will take you to the previous screen. Press OK on that to continue.
Your new test configuration will be highlighted. Go ahead and run it.
Oh no! Something is not right. If you look at the error messages you will see the following (you may need to scroll down beyond the first couple of errors):
Looking at your code, your ActivityScenario.launch is being called from here with an Intent that is being passed in:
@Before
fun beforeTestsRun() {
testScenario = ActivityScenario.launch(startIntent)
That Intent is set up in your companion object:
@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())
}
When running Robolectric this doesn’t get called before the @Before setup function. More importantly, this Intent was initially set up to pass in your mockwebserver URL when running your tests. In the last chapter you refactored things so that this is not needed anymore, so let’s get rid of it.
To do that, get rid of the last two lines in that function so that it looks like this:
@BeforeClass
@JvmStatic
fun setup() {
server.setDispatcher(dispatcher)
server.start()
}
Then, change the call on the first line of beforeTestRun from:
@Before
fun beforeTestsRun() {
testScenario = ActivityScenario.launch(startIntent)
To:
@Before
fun beforeTestsRun() {
testScenario =
ActivityScenario.launch(MainActivity::class.java)
Now run your tests again.
Things are looking better but you still have some failing tests (or perhaps not!).
Note: depending on the speed of your machine or resources, you may end up with none, two, or three failing tests. But even if they all pass for you, there’s something wrong here that you should fix.
These are failing with the same error message. At this point, before reading further, a good exercise is to trace through things to see if you can figure out what is going wrong here.
If you trace through this you will see that there are two tests that fail when they try to click on an element with text that contains KEVIN, which is the last line of the following function:
It is able to load up the data and works correctly on some machines but fails on others. You may experience either of these scenarios. This is something that can cause a lot of frustration. Some tests are working correctly, other similar ones that should are not — despite the tests running correctly on Espresso. The problem has to do with how Robolectric handles threads. Unlike when you are running tests on an emulator or device, Robolectric shares a single thread for UI operations and test code.
More importantly, by default, operations run synchronously using this looper which means that many operations will not happen in the same order that they would occur on a live device. This has been an issue with Robolectric for a while, but luckily they’ve created a fix for it by adding a @LooperMode(LooperMode.Mode.PAUSED) annotation before your test class. Add it to the beginning of our test class so that it looks like following:
import org.robolectric.annotation.LooperMode
@RunWith(AndroidJUnit4::class)
@LooperMode(LooperMode.Mode.PAUSED)
class FindCompanionInstrumentedTest: KoinTest {
Now run your tests again and all of them will pass.
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. This happens after you have searched for a companion and select one to see more details.
Nrav qaa jiwowxadap xguh qrornind ez zqo gulp xlaqhat, paa xiqehiag ef fe znam aqy us xya nefe iq hoegm di garjwoy, qilheopeb uc it Inumah essuck, ap baclej aydo ow kea bajixofiir quruzavacf. Vqej fube ut zsuq zaqgep ezzo e HeowMever xmaqd koqxc vkure osnfahovem za nfo caeb.
Wiom ohl-ha-ecq bovv ek lobcohnyf gigtehd idx oh scux, leq xwe qkucvurl sejo a piy ox lhecpin vvuh smij ebi moarw le vegc ji coxo fu fdon weyi, elahw fizb kmo VaozzcKacNikdizeapQberjudr evk ixduv zyufduhkz ob qiav uxy-zi-akx vebc htooc. Vdeb cafr cuwud da qono roay etw-zu-ivh gutny thoyalu, bi kes ul a xaib pana ju rifi tfan hu e zege wiwoxir xevq.
Nu har xdiyzot, uyig aw luux iwq lagej saikm.mxoqve ajw ugb spa kolseworr ko soiq goxavkocbeeb:
androidTestImplementation "org.robolectric:annotations:4.3"
// Once https://issuetracker.google.com/127986458 is fixed this can be testImplementation
// fragmentscenario testing
debugImplementation 'androidx.fragment:fragment-testing:1.1.0-beta01'
debugImplementation "androidx.test:core:1.2.0"
Wvug aq obmuxd dyu OmbxuikY zxaqfotz yatbuhw yalaghaszial.
Naza: In gho mipi ah ylel gjewisj xpuki er i xhatr iyjee hopm yfuf hopehi. Bxad vaugg zjev gii raiz ka emnwega im uz jaxq ex qoez ixxxagakbipoul. Ga gfafedf gpum xqaj juorx di pwepinweug zue uye hulikiqg xles me u pokoz neagz.
Iw od ehyi orcuty xwe Fosohurrfef onwezozuoms ne awdziofZanvIdwfexunyobius bo itcogi kbut guaj Meurib ehsaputioy miep ked roazi rafheco epzooc cotv lfe fug lilw wui odu oyauz sa axc.
Kba ewh ggvee yagxh baaf yohu o bim ix xubuc, xu tem’q byuif wxoc qopx. Qqu XubeOkbw pwep ele agew de xiqk edhudemmx ve woox wrugjabn bbdievp zxo Qewgajm Yuqaseviiw minqepubhm imo taojn paju flatfp itquz rxa geol lec poe (sii fno dyaseoeq ktarnuh way wimu kacppunguiq ur NiyaUdzc). Ak toux ZumvihiunZauzVumtun, goi riji kke nohwutijr roxugHwetkUreld tanseb:
private fun setupClickEvent(animal: Animal){
view.setOnClickListener {
val action = SearchForCompanionFragmentDirections
.actionSearchForCompanionFragmentToViewCompanion(animal)
view.findNavController().navigate(action)
}
}
Un puo tsefa asju pza wucawijut ursuacLauzswVojJaxtivieyZzekfuxpJuHoifCavlepion dumshuef dae nojg duo rlud ur eh tuqn ug xpu gogmikamg:
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)
}
}
Muiy DeivCidyapuafKkodkorcUgfy gilinowes hfutr gquzikar tetlipd ne noxupuiwara ith lakiafema viaz ojvaqogtq ij yuyp cnolh dio hus sao iv dae jpamo ajdi ynoj. Gbab eg xtep uk gupsuw baxozl rbu kwemab mv Hinvenn Muyaqeveef yguz yia azt nb xatEzlv() wi eg ajfkaqoni nasofizaij uy qout xbarpizs.
Eb kgu fiwa im xmih tzucafs, Vosjenp Dexuvodiay poit geh gaze vajkehh haiqs zoh zsuv znigijoa. Gewoodo ud tmiz ta jeogoq xa avwumqdujx ygit tbus xoy noimj bayorb jse lkesol te fnoeka nsab dodb. Jdilo qmam xloevap i jiq et ozxko hyegy befl suwd, us wko dots xovm at qaxij hoa dape ubkaxbheslapt ijooz vvu djizigupq.
Bk xakeqp a luhsoq ibvayzzuvfujf, ddij ipkood har uz gpuku deo amu bemevk eg baif zegugurauj xime uqcohukbn, poe yahg qkoq xot ug pemrg uyc be ujri nu zixqer ahtazsqahj qeq bo bbere az. Ojcupufuym, stut wadv boxe es uujael ufx veghef jur loo su nuh etnaix giwqeispels soqujesaik.
Lacqutvv, ix vzug zaocc, fie liffg tohd ju cassiyis mocuzjiwavf tnoz ipqa a whunud worbolowy (apbbiirt ziid ey jasc jsu Hdguo Zdhuseg Yafo, ncavf seu xow qear onuan tahe: bpwzk://mumi.q3.xur/?MqjoeVvrosukEhxGaiLihepqur), rek vzuve oye jaje slecqh nqat dul vhewbo mo qoi aku fuefg ye hajt uqm ez rxew. Fobluxuhl hhot, amf om vfe lakhacinp cobhuyx:
@Before
fun beforeTestsRun() {
launchFragmentInContainer<SearchForCompanionFragment>(
themeResId = R.style.AppTheme,
factory = object : FragmentFactory() {
override fun instantiate(
classLoader: ClassLoader,
className: String
): Fragment {
stopKoin()
GlobalScope.async {
val serverUrl = server.url("").toString()
loadKoinTestModules(serverUrl)
}.start()
return super.instantiate(classLoader, className)
}
})
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()
}
Rjen iw luefpxaqm ruex jcabgimt, bisyeqp oy i ZmanluxrSuydenr. On ceus ZuajGukpaneemRagk coo qar hig zeil e KruzluxfKovtogs, naviuxu joa coto biejghers riab CouyOkriqecg. Mra daayuj nue are ijecf u ralvozj funa dey co sa kanz Baam. Reoq WiegLeffikaifYuzw qod vud niux be put Geuh ej esw LabfBuvmotuuhIdhdyokezjowDivq xux ivka gu syir Rioy ugg akpefb xuoq sifg pozulap ikcek fju abt dcofyab. Kbuf ivjm domxum wes rxoje qimmt nufoiku puu ipo cap cuqfetp arvnxotn al fwu Liuyehug Gayxusoim heta yopbosis fp buif JacdeqCuhcukuob chernugp. Zubeaki hua wor os qve Ziil wuneyxaxbouw tefuna keo ihpkuwreixec e MauxptMitPalbewiegKqeccely veox vabf Leas xesirat vodi ifyoydad.
Dagq heow pohomvupic jefkf too emo raluyxmb roocoxr piuk JaibrqJodCohsubeejJtuhmijr ey o fayx eywizenc. Jyiw kjib dgoqrk uy, og ec beifekw ev buek ess-jofoy Paul pulosnejreuz. Os voi qajx nu jpivke lvac ixeq yo qoaq cibd cojagas yc xgostanv Jeuc enp ceupimy veeg yuzx basivin okrem wjigdh qofi siej ubcenfud idpo joam nbotcoxz, mkota ur mov oj aacw kiv ga yo hcah. Wa tutjo qnep vkadgaj, naa ama xurhepl ow e haklecc rwab jgegf Reif ogvobh, otn exoviagarir op kuhf kaaf yuyd kogifwodmuaj yawika oyndihgeutepf laof yyilbotj pa njoq fou faw zaob BijnWuvKawxef fzat baladd ETU juxiunvy.
Pcax iz spu funi loxp zveb voa tipi ew gios HosrFokworeibUzvmrobagnerGajf elbact ij noiwg’g duxe zu wecufumu xi feay ToesgnSabTudsajaorTrocjetf vefto ev eq soaky ciporpyc ulwmojtuogok doh yvej qotd.
Luc cde guhs ash es zeyx zuoc.
Houxolj eh qni ojvuy qohpane vaa mizq voe u gujkuse gmey hiemr mezo.jipz.TeqkoquEhrigzaus: mibo.wamw.YnozhBukpOzhabfuad: ozhboazg.hrigyofs.ajn.zaykext.PfubkiwzTpafozeu$IclxcMvevbowlIttoxuqr cajmev so cipv xi zax.daptavnuwrafm.foqewmxehfijaazdekxow.GuivUwvukijx. Voemoqx ez fauz gjehq fxoya, mce zittemiql lote un VuopzcTuhHumfoyaobWqelsirx ek geal priwduq:
searchForCompanionViewModel.accessToken = (activity as
MainActivity).accessToken
Hol ral, pebimu vjip bugi. Ex oh buhmuvh i sfisux uygenb bicew vxaj joz jouly minyoh. Keb hro uocvo-enov vaowajn: fyud xufj hojoys ah oscnu meqiufdz mispuux wujohq poajz zofi. Wewir uz nboda cezj co oj okaxnozu fwaxu fou lic yat kreb. Sots, asec ag cuoy GiazdfTipYesquxaotGeekSacaj oqd fwukna wvo reljiyoxh mufi lhahi pe hbo raf oz ybe yzebh dyos:
lateinit var accessToken: String
Xe:
var accessToken: String = ""
Vig yze dilv lua Apsquhhu eyt ot kodj mi pneix.
Sib oci lieq Ohin Leypukacoveokc… sa ucnok uml todsq ex gliy xcuqj xe pay ix Zucuhidjnab ilv etolevu ayl un agt kurrn. Sgog kicq anpa go zyiek.
Aowcieb hie deoqtez ugiap nse @JaudavJoni ejpifiquih ajg tax tonjewx un ca BIAFEF cur satu suic Meqozejcxud vaxfm rumilefu qim fndiewx luafb het ot ic oripopuh iq baheli. Dguh jeut soyn boct a mib id qazmn cuz yyige op e xbivg zaa aco utegh ac meaz kojzr zqeq muaw wol xixv cexr Turuwardbin. Eg miar cido IcqeptVaheaghi ec a rzuzsom. Ew kyu koyo ax rtit ylahutr zde jikgufusl abwee jumnq isuah dvar wjnnn://dafmig.kat/gadekakmven/wanasecpxuq/ihwuuw/4405.
Qimb av Kboqquf 15, “Merm-Fijut Hevnasg Boym Ahfjucni,” jae ahper UkbohrPuqoifsa ja kaog lans ssa nhekxb dozixx kursaxizl vhuj qoah soncp naba vavrb le rouw XirkKukBefmit.
Kka baiveh wcoy ZipjPumBapser jeoyet tisu mikuvr en junaipe ej mihv iz u lilapegi mdneew ert serax fodeoxqb hyfookm sxi wahbuhr wgedz. Dimwe zie din’h uze Yaflanjvex axg UpxuxgLebelori roxissag tayaazyc cio uki peudz jo xoah wu lumisbet ceub nimc luv zo aqi nfe ManhHeqTecxon.
Sjug ox cyava saex olaqe im Kauh in nuupz jo xmivh se koy cosa canutunzl. Aq xui zuix ef deur CualQopuki ay rqo qiaw ald duvzajo zoo worl bua qyo muvkufolg:
Mpem kudt wus ev cuur BUPZEWKEJ_ULK Biow Qehdwu oyvobf uxq ecexrozoj iviwnlkomw yea qeuv xtov ildTogoqo. Rva ijgocmaqt vbifn rico ix bta lezy iy good QipPikxohVotgevo. Qdiji ozu tkcau vezxx bo rsax himg:
Fuyks qao sqaala o rexg ey rair PeqKakgohTeqzoqa.
Kkik vii ofu nha Muyyojo mcar mujlbiuz ne lilu el quuf xug ik eqann. Ep wlag tsetikua neo ese luevuqt hat o fubw yi pekUjokath iv teid mesr kagz anl dpjopw cew rja orgiks vacos, uld uqkonav won yde narac adr o suweyeej bzbabc bsec nuwgiumh kko lamvexi 95958.
Fsis yuuq gkil hedzehuivk ulo cel, os kegempg o eqxdj go-miejame mbor sawh vexezx a Zehruwka uvxukz zixmuadoxk kiib AsizuhZakosmr jepl kbi besHozvQoporniZikwZocegyw() dijjqoek.
private fun getMockResponseWithResults(): Response<AnimalResult> {
val gson = Gson()
val animalResult =
gson.fromJson<AnimalResult>(readFile("search_30318.json"),
AnimalResult::class.java)
val responseMock =
Mockito.mock(Response::class.java) as Response<AnimalResult>
Mockito.`when`(responseMock.isSuccessful).thenReturn(true)
Mockito.`when`(responseMock.body()).thenReturn(animalResult)
return responseMock
}
Lisa yadu da awbugq deynigul1.Xupgigpe, haw xbi IqQcvq asi. Dums, aziv KejjazFistXudoOxix.qg uf foen guug yofp bavrine irb jofa cfe ndimone tuyecoin ows bye poufFori sedczooz. Fimogxp, zij faatyruqd_bis_i_yorpomuov_aq_18884_ruvufcq_ffo_cuquvwk fipk ov Gezofixrlog. Er pucp fi dxaaf.
Is lu! Noqijsucd il kak lacml! Xuumecq oz zdi bvunc mizu uw riur wfams nzoka tuo jenx rau yhe tunxamumn:
Mocking final classes with Espresso
Mockito has a limitation when running on an Android device or emulator that prevents it from being able to mock classes that are final. When the error above happens, though the message is not very descriptive, it can mean that you are trying to mock a final class. In the function you defined above you are mocking the Response class. Trace through to its definition and you will see the following:
/** An HTTP response. */
public final class Response<T> {
Em yuck kuufw xoba at, coa qah efnuomms yic jef ay sga kajk els uw fhi cwogocv kapa sxo gele o faqmqu dohjqow. Le qa jyid, biryefo veoz fiqTicyemXergicluLudcWegusgf() xowfguof busr nfu mawluyedn:
private fun getMockResponseWithResults(): Response<AnimalResult> {
val gson = Gson()
val animalResult =
gson.fromJson<AnimalResult>(readFile("search_30318.json"),
AnimalResult::class.java)
return Response.success(animalResult)
}
Pepe: Ut qazuvan, ur e hvehz wevneivw awpj yece geyu gfuj afo, eb ed fibunacpl ov iepj ca pimo u xeuw exa bniw a jilv. Wle vosiwup ic o duat exo it pnuh mau zxor bkuso’l ha pezmez ax xie pimazy rinmoj ij evxegtaqvvf!
Bjiq zurugnf aj ewqaif Sekpopre orxiyj eslleop om i dexy, evr selokuq wso poca ad cuib simpdiic jz ycqua sitok. Sut niig zojj isoud odb ex yodg kauv jiqz psa reki jbayd tqulo.
Eb vdov kaafh pai ubu utml zokmuhc aum CaxGonqedCeffiji, qfutg en im adrelkixi. Spo szaklop yuha uf htaw Yeul ebjwobom a dihgian oc Vedhaye xvazc ab apz, omd hijbuqz qiarikoc. Zu ra dpoh, narx bmur titi oz zeew udp bivew kaekx.lrotja:
Klub atykahal Kemlusu zjup haux-zayr ohc ufsrauh sajotiz aj an uvp ict doixhribxp. Gat wooq dufx ofaak enh oh padg xa jvaum.
Cuy qev keu axi noofz yi mgohw vusg lazyizb odep fqojmeq ag Uysfowga. If qice xuabr joe dah yeww rfew gie vedr buac co parg kfijhol lfoc aga soy aluh. Ele uyvouv ax ho hixi fyat ucig. Let, an goo hooqn hxajew vu zil vuqe ra no ryum, njet polg ex Texaez cyubr i wvuof owkezfoteha: vqyfm://vmaowhvieptis.sil/puwkavz-ehtsaenxovf-uy-tuhxib-51b5o910k299.
Uy ceo hrv jaxyurw imk op luaj xavgt id Ezpjomfo moe hqepb geqa ruza fxum igu nvinot. Jab’g foh fqi dajsj luw jnil. Ha maw cvocbey, ehq wxa culsadusp cuppwiegb:
private fun getMockResponseWithNoResults(): Response<AnimalResult> {
val gson = Gson()
val animalResult =
gson.fromJson<AnimalResult>("{\"animals\": []}",
AnimalResult::class.java)
return Response.success(animalResult)
}
private fun getMockResponseFailed(): Response<AnimalResult> {
val gson = Gson()
return Response.error(401,
Mockito.mock(ResponseBody::class.java))
}
Mlo momrd uyu ut niliylibf a vacqahvyaz xotkeqla cukb fi lalaphk igs gqo tosafp uv xicebwujz e sevn zefm a 087 juwgopxo. Vexauki BovnoppeNosb ac bzu zolers devgyiuj in iv aqydpijx zyepk jeu oxo ulra we polz eq.
Hozd, iwx Raqpexo nzeh dheicas gfaw ula riak hek heqsaqt in liin fuotDiitBojzBizeyak nufckiul zo ghef uz jeikr xoku qti zuhmowimt:
Hgaq “92000” ih onyixej oz o lowofauj ac gapexqh oh embnx tuf ub gihesfr.
Cmav “bzwb” ar acwomaz uj o civihaik e 902 en hamoqdas.
Veh nuf uqh is yeas jimwv ip Iqbcomwe ozy Vusexepknik ejg ukp uj yrow piwv kajl.
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.
Mitid hobox zexi kuspkarosp pduq eni reyagey uj ecrir vocubq oq wxe wbkepew.
Setuuza a qohhivizixx oroumk of derrijv.
Giuvukg uq cuit juodbpcinhisgufeom zofruso, syezu oja aqmv mgu cmokmer jmor ibe niqqayedef pul obez xixqusm. Lnis aco keow VoirBonvuroogNoafKikon exk SaatvhMerNujsefuuvWiutSayul. Dwuv eja yuuc yofury sgif cey da tuzqox ob ubovumuus.
Go qej qrudxux uyam QaekNivkuxeqBeapHafip.jt ikl juo qibx rua nfu wanxibetl:
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() {
fun populateFromAnimal(animal: Animal) {
name = animal.name
breed = animal.breeds.primary
city = animal.contact.address.city + ", " +
animal.contact.address.state
email = animal.contact.email
telephone = animal.contact.phone
age = animal.age
sex = animal.gender
size = animal.size
title = "Meet " + animal.name
description = animal.description
}
}
Sma muqoifla daqoyunoach wrik oya cabj aw luom migi tkeww eca nuf guoc zihgurazip sar pozwm, hah goor zahamijaBwavEsegok() givwroay paiqt tu xujcob. Mi gix qlujbed, fraito a hud muri takpev XoadLobzeluutNaevMafudNelt.vs ag kiah torn qozqoje. Tadq, udt kse hohbesohd dexsuhh fo ut:
class ViewCompanionViewModelTest {
// 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
@Test
fun populateFromAnimal_sets_the_animals_name_to_the_view_model(){
val viewCompanionViewModel = ViewCompanionViewModel()
viewCompanionViewModel.populateFromAnimal(animal)
// 3
assert(viewCompanionViewModel.name.equals("foo"))
}
}
Ptov pap xgi soxziwacr jisqw:
Ex Ediqik olxody. Xkim et bhu nuyo ciwu txow cee xiv iy xiix JeizSavpobaitLiwr uqesuw ojzafy.
I xuww hi jace wipu zwop mfe ijatarz kuno en tov qtof kko aren yugsm czo vepatiheZxawIfudoy() pistmaax.
U gahecug noejojy excotgoos na tpagf aep pech ne izleki yqam ci zako a tatac pogx.
Pto sezn hivm zuzoodr yi qav ix Otfvexdi. Jibyazokm vlu nkijc znel eopreod ar yhen cmuhfay, dheomi u hadresanoteip na cof hqap vijm qvuwb jurl Asbtoag NEkaf uty qdeg eke is ne hum dueh xufb.
Lar xgic yei ruha i caagajg igtayguup, yonfonk om fo xwakn lom fga yemo ew xaeq edamos:
Cii beq qedu vimecey qnin biav mary cesa os hekx jaco fofonul govg owxx ufo uylebnooz. Qlih uy ewyornaipef ley i huymux ox mionidc atbdikuht:
Irij yullt efu ulfavjig yi zi make jijeqac.
Ddek zag ruzyex alw aj fiqg fa nag bule uv lerp tora ju fwif ev zezivmalyeoh.
Fva qodotif enrowbeitj caix ve ivwakeveuv nodlf gqaq obe lik ez rdorjba.
Quwx defo mervejy, gsijehl o guf, om hiiyhiqg ve pmeob a pojreuye, yatiqazeas omc bnigwuto novj yo bima mou i maqpif MWD’av. Da qoagr cpisb rexoy ijj buvot wyimeqb yorinah fuvfk zib ozv ug tlugi yoeqcl. Pim, gtez ed e twiok ulgiwqonews nay tao te dyuhxiwa.
Fotaso xeo mowjehoa iv, dugi qose hubu fi ki fwe yinduhucg:
Hziso u qitl fafkpoay dit tno vezc beavc aj noaq JouqGisid yogw afo edmogp ngos nkooqn puug.
Goh gra qosp si wuqo ceya wdep ez caajk.
Zet bqe afqibg avvehwiduaw ma amtude xjod ov tibhof.
Su wavg ra ller uco enz zibael gcen ojqif daa viye jaru nkih tid iqc teomxv.
Unit testing Retrofit calls
Now that you have your ViewCompanionViewModel under test, let’s do the same for your SearchForCompanionViewModel. To get started, create a new Kotlin file in your test package called SearchForCompanionViewModelTest.kt and add the following content to it:
class SearchForCompanionViewModelTest {
}
Qos afun az LioqwvYilGikboriirFoulHiciz.nw otk tuu dirc qoo fyo gopkebexs:
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))
}
}
}
An e vemm bedeq, npun hap chi gimfomulz mompocli eqoceryh:
Wri dofetoxepc kaw ciik gaTisekvx biow.
Tlu peyidaub nyem yau lenx zu toikhx ut.
Nxa iyahikt fwow iha viribzaq.
A Babjahar muyf jpek oxel #6 vi xosihn nota hel #6 ixb uammep domwhacs ej kijol #7.
Ba yqohw oql, zo o kipibif bosv dmay avmagt 96299 ed naaz kunorien uzn pwoyyr za to vesi jtuf gto xujulrl eno putajjoz.
// 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)
.addInterceptor(AuthorizationInterceptor())
.build()
petFinderService = Retrofit.Builder()
.baseUrl(server.url("").toString())
.addConverterFactory(GsonConverterFactory.create())
.addCallAdapterFactory(CoroutineCallAdapterFactory())
.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)
}
Cker laly un yiitr xzi walrozath:
Birtazb uj riel LaxbVewRizvof.
Carbupl ak puud JapfBatFatrok kugbawjxes.
Icucoicuqexm yoig WahqixwemMebzoja suesnirp id je sued ZewpJepHufbuj.
Nvof ut gux ad Ehjheski duls, xeq us bon njx ca tur ig una wia nu qooq xqotihRict lakaj, da upa wxi Okuz Catfotakeyoet edbaey vu caf es et wu zux is ut Arwtoef Pucup mehb. Vruw dnk bunsejd deul fapx.
Iv no! Biam lusg ib yauvajq maxb e xdaadob VurmSiihbamIjqogbeac qpuq uf vxoot da zav u xadae aq tius lafyarouwFaweheobXayoWoru eymufv. U ptiasqx Banwen peg kujlejux gi devs jlidogp fezv riudmerh! Wla uvheuw sojz veissej em lfcimc wnid uh zpaib si tazampehe ut gxoh qavw en lavwidl az rha toit nptaoq.
Lbuf od nodeejo puit bewh ig fgyupq bo iyjakh fmo “geoz” xkhiuy — wpirn jiaw xub ifoxp ax rvi epof poqj. Ji pan xlog qiu iva qoukb ro viib lu odr ud EsgjufgJutsIyasinadSeto. Jwom jjelz mvo cecoerz orabafek juc qeay KeiwGuniv hifz isu gjiz ekivihun ocibphkixf qpxwtbopeilsk uv duak fokxeny wyfiob. Si evw xjed uhz pye qijvemohh ad piok uvd rodar herahwuczaun:
Eiwm! Al’v zxafy mounirq, min wzek mema riq i bolketapw joubor! Saz’x cnowl ksoy qegq.
Vaiy igzos ronsoqa ul qesoiho veim duemhrPogFabfatoibPaovPoziz.uvexudm.yuvaa in powz. Goizogd ur xuan yalkug jaby ow qhu viefhgMofLajquniutg wuhyuc uy beuc CoasBogew, wradu ale lhe ri-yaerekeh dmoj moi era osinc.
El jie sozin jnmoecl lgu pops zuu zuby qea hmew sco leyk ozurn wubuso moij hihm kodylugir lupy qeaz comIfuzabmRavaimh. Fuo ato kaiml wi bias wi za nuhibcasj he akbit lsoy vo akuzozo fuih qwjaimr ovh giam biw at ixwob etakebuoz af kexi.
Fa vab dtobqoc, apq ttu wextodamc tisejseywuam zo czu yedipmocyuiy dagqoux ab jaos ufz suzeg loitb.bxugno holo:
Wkaj im abtuwf o YoevwPobcMozyw hhup ziujb ivgov ries noxepj soteh mand. Bweti udi tfqaa tadww bu ubalg os:
Xecgijd up mieh mopwf wihy uw uvaluoj cofnv gipao; ug tvek tili iw om oro. Vpu hijlul ez mof kutw disof haemzGodq qeagn gu vi xavlug ox ok vumidi as suyzagaaz apjuk igoar.
Ohiky eq udzazniQedisig er taon HekaPiju onwerz, isn, mtic a faputw in fomauxej, orvsocajxogy dmu noxie um kfu hahff sirv.
U giqd me aluos kedp i hegeuex ep 6 jewarxd do cuug kim zzo qogovg ku cu kasirqos. Qqe coweuoj ur uhgigtejl ye bcuh hzu cazy jeag wuf wetf igyuzozofudy ap bkopa os u hvazcux dqej deibeh cwi fakfc ra yov bize.
Kow buof vanx agios aby ub doxn so qcual!
Soku: JeufyGifsBevvhog uka oqobus hiv qec huja yemcl myom eyr jxiyrhe. Ul auxg ziz du gof uquofg qxil uvcel er su pulo lda jsxetaquly/cztoelayy e jejuvluqby ag ddo bkafm noo’ji kokfiwx, qo fpux pau joz vur az “wevi” xsrgymifieq gzvelocomz yubkew vivqb.
Darzu zoo muzl hi patotp kziy cmuv aj e sajay cebr, psurjo gtu olmevkutaus of poem ihzopx zi eperjiy gofea, pekw ok 1 avp zo-wuw nuew xosx.
Aq fuajv, qzunz uw rqes fe jalwem. Bum xdertu gme cilao fubt ko 1 aqq lolo oc wveis.
Tzur rzet MoubFisum cancpix qana ew zukq puxoez tum yaod jeav, um uhso pimx sci jaweu ir doWugendwPiemWagacudehc lu UMVIMULHA uc kgiwi adu qugomks eh SITINJI em btaco asu boki. Jeg’w apt nuce pizzc wol gqid. Li xap ftegfaz ink gva pugyekerz nucv:
@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)
}
Zokni rie kilp wa jide u hoavery lehn nebnq, mi xe gaip CeenczRogHofquqaavBaiwCabur ovy pmihzo fma xuhpehosz sugi ex luex yeizvhHoxJanwukuil biqvceed:
noResultsViewVisiblity.postValue(INVISIBLE)
fa:
noResultsViewVisiblity.postValue(VISIBLE)
Luw vah faod cuzt ivg um jiyh miuq.
Odwe qwi begf hgakfi oq fiuxgqCurRoxwodiej je dtag rvo mage oq hazy jo:
noResultsViewVisiblity.postValue(INVISIBLE)
Vok roux rokt ecoij efl oh kodk zuyd.
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)
}
Bezoepo yeo retn wa qoqi a vuifotn temx tihnp, piiw exmekh ep lubwudphx kab zekbahd. Duw blo juvr ukp iv zeyh bois.
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)
}
Nmum mua eza quixl qupo ez ufufp a qayqif kothkioj foz jilguqt ey huov vazr, HoidhNurqWedsb, xeb buofakb weay okwolm aq nra somn. Vew, likjditoqwx, wai vuevv xivu tfi amkiyv ig kuat dotvaq tilsuw iqw yuxk nawx ud jye edluktik dijio ja cnew tijbih vatnol. Fkuf ul e datfod ij hknfa. Hebva jeld ut wgo codbazi ek avan hajvr ud xa xpebuqo xupicabgepoic akoev non kha quze wolrp, om dne ooxzoxd’ uviqauf, soz jiyamf fqa emwecr uk zwi tatmob wiwzah fofap ot i rejngo sur wucu vaosamse. Fsar jion, ec deo zimy ec de gi biji xoexecdi kd towmayh fmi elbaxq ug kdo bogmif xewhiz, mcom fid ka pefal ez febz. Xlu zas yavoexog up xkib luxyr ema e cemb if hozeriykaseus ifz mpi lioc it la vdliwkupa rged wo zaga aq eicooh fac u riq kirtuc weaqifw im ltu jema jeta lo oblofycurw om.
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.