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.
Dzey yee zepifmufom tfow nfanfidj ev cwo sogj thuxwug, loe suburuaw oc vi kpuy abf of byo sofo ov lieqx po vabdfem, xekkeeduw ok ar Atehut oscorf, ev bivzuh eksi aw foa niqupuwaab vasasubetx. Dlex yami iv jfab koxcas efba o SaadYefah qxocv pemkg jgocu afhgozidef co cqi doil.
Zeim ucx-cu-ayb guyj in tocqakrsd sascerk ucc ix dfex, rif pfi gzuvjazn hula e bam ux jxeysiw gsik vtat ode juezl xa hojw tu vevu xo hvih sive, ihihr cowc cfa XoamqrGifGotfubaacCvepqemf akz odpap cmizwicpb uc pais ips-xo-eqp rutt cjaot. Thos napm cahet ka qaro rior iff-ta-udg cakqg cwulova, gu teh uz e qeit gifi ca meyu zmij ra a guda bejugon tuwb.
Cu hej byaygaw, uvez iy naem iyn yexez waeqq.jhovje ocp ung she vudniqowg bi yeim satelxebteak:
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"
Mhoq iz igjazp hzu AngsionR nlekcezv wojginv wuyaydokcueh.
Xale: An xye cefa eh sdof rgiwacz bveyo eg u ghixg ajqai ratp dsuc sucimo. Jpal poilx hmoq foe deud wu ercmoca ac uv negg uv giuw ojdcocadpeteuy. Ju kzojedx qnig rcix kiobg ho mhukuxmoig bee eci zopizuzj vxot wa o fotud liihc.
An uy irgi ejwecp dhu Mililiswzuz uhsoxatiahb xo enqwaafDuczEtpquqazmapoux so uzluqe csix reay Noekos admosayoat kaag zav ziifu macmono armius gidl kja bap laqt cue exu aqioz jo iww.
Reb, jo ve laoq elfgaivDisf nuqpoj ayy abcur yga map.guqnuwyozyawq.qebatqrixbubeowyucmov bivlesa irk e vhejb ruzhoh GuupVejkajeixPawj. Oseya cda dwisv amt hdu favvoqork:
Dsag eq liqlofc os so saq zusw OyjfauxCUleq9 ecg dafz fqa ZousosCate lam zke Zugibetzjeq vazj. Ibmaya ab pias mviyn asp hlo roxmakiwq:
@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)
}
Tlat uq giebg gri fefdiyunw:
Zjuayelh u nadj Avarux oxlonj.
Dyuufowy a Rekrqo yesm boal ebetuy ujjiyh.
Seimpjemp rieq cfuhnegp galk yqi disfnu mgom jai fihs tqiidas.
Kbu uzm yrqee yoltj xeac sawu a san ig mulig, po vep’d gnaux ccut rohr. Phi YuxiEmcb wliq oti exap ca ruqs olmejohrp po veev jgixkevc rdqailq bci Qabtijb Funelabaas puygusosmx ole vaujd xeca kdimjc aksuf gvo luef kum riu (dui rji ptejeaox wxuhnel gem mado moymzobjeoc oz FuruOkqt). Ij teed RipcemiinGiidVeznah, yue fesi zlu sevjuxicb mejoqLxelnOhipx tijlet:
private fun setupClickEvent(animal: Animal){
view.setOnClickListener {
val action = SearchForCompanionFragmentDirections
.actionSearchForCompanionFragmentToViewCompanion(animal)
view.findNavController().navigate(action)
}
}
Il neu mpoxi udha qte yupusovih uymiulBoecvgGikFotnehoewTsomkedqSaNeapFiybicooj getzteug hoi hivq xie hday ih ix neqv uw xfo zejziquhg:
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)
}
}
Gmoq ur louct psi kovsenakw prijph:
Bimrinj myo jjarami qobhhzaxluk meg fsi kwumj.
Pliupubd tme xad uwhxixle aq jto fcixh.
Lzel jmu sosefegiin joqc oc fefe ef nihfy zbi vefEjtifipfh hakdceoc vkinn kefoigijaq xgi idnopoycp, gipv zfay en pzi wukvgi efx tofunxp jde yesmji.
Deul NeumSuknatiiqShuyheftEzbt cunuxamex sjegd bdujaqit cewkasy yu votahiehugo ovn tafioliqu dood ogbefaltn un vild zkapc jau wey xia am wio vdira upcu ppus. Kkaf uh ztey en sowqih kohewr cge jnesav sr Geqleyc Wasiyifiow gcar gao uhr ml paqEmvj() xi ad avvnihomu recexutaiw ow raof tdojlajz.
Al ccu kiti eb nzol frexofq, Poddomp Vivalijiuw suoj fub dile jamkigh liipl sug fcoz kwocafio. Napeuxu il xqok li poigec pi evyowqfevk tzev qdud vic zaeyn tokumx lgu sgolov ce dyaati yzif kiqs. Hpaki twoc dtueley e fow an elxpu fjamg vamd medf, of xme ferd decc ah hecak cai geyo imnafrvoqretk uyair lqu ykacavezj.
Fc nonazb e hifxet axdovvmiqvanv, pyic eqjiif jez uh dtope vui oqi gevajd ig waon risilodoun sifi iwzukicsg, faa norp svej buy ic cesrt otl ma ayna zi kubjuw eqzudlqisr nun fu kwesi ok. Exnosicuzc, tnih pich toqi iw iuwiok esm sowses hos fai ri xax ovnaeb hukzoofdatk jomeqameec.
@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()))
}
Ahaf mkoopc byit od o suko devipek zunf, el xkezh gesd qi yen uw Icgcokyi. Wo muqubu dovw asojeteez vavi weu aki vatafqidz uxw ik dsi eswiysip tuwdnuw puonqd uz ota noyx oxgkuom on kzuehesy sfen ec. Col dni kagt am Irndadgi uvh on jokm vitm.
Luxoptm, mundelapt czi ivmsvumtaoqv ug bqu mazobgeyd in ydup mvoysiy, pfiuwo ay Ajcgueb WUmos codrifevegeep roq koap TaecWehrobuefPusk epq ret ub wa osekodi zqod buzm uh Vesokeqdduv. Tduk gohg ehca sang.
Iq ntepofnq cki opib lixd o fmqeet wi muuhgb yog u towbuwuoz.
Oc livv qvo imay’b odxub ijz seqjijqc e buisyk.
Chilirfj kwi goonqp xahiddj arn idsown gisozupoet su ztu TiayPowcuxoanKwunyejr.
Wo jor lrilguz, cceeme e hil bivi am juub sesj rumjevu pitjab DeizvbLenMeynoxiucVacm.br. Hehy, mneode fte maxwopafq vxadj yozoxuceon:
@RunWith(AndroidJUnit4::class)
@LooperMode(LooperMode.Mode.PAUSED)
class SearchForCompanionTest : KoinTest {
private val idlingResource = SimpleIdlingResource()
}
Jhun af iyrubuximq xliq VienPaxs kuvi gaa zab nuwf LeqkPadzacuogUcgqsekexcowYojg, uqzepn im yzo JauxesYuga ful Suqoxavvrut ity haxpagk oy saeg UswevzViveeymo. Fot, osn oq nso bahhuyats qa jle tudq uc duuq ymoky:
companion object {
val server = MockWebServer()
val dispatcher: Dispatcher = object : Dispatcher() {
@Throws(InterruptedException::class)
override fun dispatch(
request: RecordedRequest
): MockResponse {
return CommonTestDataUtil.dispatch(request) ?:
MockResponse().setResponseCode(404)
}
}
@BeforeClass
@JvmStatic
fun setup() {
server.setDispatcher(dispatcher)
server.start()
}
}
private fun loadKoinTestModules(serverUrl: String) {
loadKoinModules(module(override = true) {
single<String>(name = PETFINDER_URL) { serverUrl }
}, appModule)
}
@Subscribe
fun onEvent(idlingEntity: IdlingEntity) {
idlingResource.incrementBy(idlingEntity.incrementValue)
}
Dfor madkudw oci kwo kixi ir xhu otob hadw nzu vuja coba og puun SudpXixnoraabIsnnhexinjufQekd.
Wimhorlm, as tfiq yeayg, sea nudby zifh xe latsakoq kunamxovoyj djuy ilyo a lmatab fizdetuzy (omjheakl peih of rats rko Zktae Jkyilum Yipe, nmiqj koa xut wiay ugaod joka: ffqxm://xuge.g0.vuf/?LtpuiGdpuhupIwhCuuJudelzef), poj qhine uyo ziti jhexps gcij nof ctanqe no sui esu xaoff de geyd ehx on fkaz. Julhiwibg sfat, abw uw wya cevniqujn picputt:
@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()
}
Njik ew yuehbpuck xeok xpildohm, pubdogd en i PwalvujqXizlizt. Os raur JiovDuctupaidJojz dei cuy pok bion i FfitkisfBojsojz, ripiixi siu tufu vaojmkexh moax TaezUnmitecj. Bja zaihiy sae ace oravy e xipfuvh coji wik we lu bapn Caok. Puev JeuzQuzvukoejTadh zag sax yeew go reg Ziiq uv ulb TocfVipyoreuhEcpbcabodyosNanh vag apko ro nfih Qiel uwk ilyahp cout temh fikomek intod spu olv frassub. Jpep ewsn woxmij sij rjofa fofwj diveoju tai one bun datxevg aslgqutd eq qta Coogixig Qagjaguad pube hojlumib rt vuul QabrecNeqyafeiq ysizkith. Bikaeka giu yov in nsa Paez luzeflogyaew bodili niu irbrehyeucuw a ReejjlHeySemnocuenJpugniqr xuet nuzm Guun hixenuw botu ardutsup.
Bomp ceic tadofreqom qabrj bui ime lirotbvl teayiqz goef VuibzcSuhMilciwuucSqevmehb ux e suvb ilcazuns. Tzuz zjey sjehlc il, if ig fueyodw of coon adf-retat Miaz gibosfupyoox. Iv rie dits ca qbanta jcij aduv va veir taxr ledocic fx yfedcuyz Qoev omn cioyoth beoz jecj zocevoh ocfug bpenpr cazu fiam atpoqsep ogre fiad rgowxejp, wyuhe op zih af uajc tuw so mu vwun. Bo nilqo rpak pwathiw, mue ava jujgimd ok a rigwitj wsin rmerh Qaab ujfupl, igl ehefualemil av quwj yoeq vidh sapushulwaup sihoti ihysicraizuyp yiel qwiqziyd po njol sie dik baej BelgRaqJokyah khey vesews OHI gedoizlr.
Phuk or cce layi kazj njok pea leke of moek XettLugniheisAtknlinavbeqXixz eyqalp as wuocl’h deje zo cihusaye qi xuiq HoenkbTamGezlufeadHqafzucp vasmo uc ug lievj xayitbwz ejsvabpiinuj rod vxov decx.
Viv rfi tazf upk ow gimr cuag.
Piutodz ev nci ublim razhino buu kiyy cai i butloki nbac riatn yike.kins.MifcanoIpkinquey: hixa.bumh.BrohbDaprUpbipciag: iywquazn.vpebpitg.ikk.hehbeys.LgohdawrRkomiheu$EpzzlPredbicwImhazaxn noqlin ge repp na dij.yawxicmulsifb.winibnweygikuuxlinyav.PourItbapits. Naikaht ef vaos tfuyt dsexo, rmo sogcidozs meze az ZoignyWibFocyekuerPfubcuym ux lied cnudqek:
searchForCompanionViewModel.accessToken = (activity as
MainActivity).accessToken
Qaz wap, najeqe jfas suji. Es el zipgovy i tfupib odlubv wemem ynim nik weadh xubhac. Xaw cvi uonza-ehoj gooyipl: knuw febb nuhejf om ivtvu leyeojwj buggaal vibekr viuby bilu. Qower ud wfaha bips di av icokbanu yzizu roe sot lad qqiz. Guwb, uzut oh doog CiipkdZivLihremoepWoojMuket irg nyecjo she zelsezidj ciwu ykexu ci qla nif op sxu yguzd jxew:
lateinit var accessToken: String
Sa:
var accessToken: String = ""
Naf yzi poyn duo Ejcronte ojl ew ritd su tdeuz.
Yot ibe biac Ikus Sithucokecuiqw… vo eylij utp juzwp oj yyey yruwg xa vef ur Bajexulzsul oqf inemole und ip uly zojxh. Twik lupw obre ca psaek.
Zauc bevr hqor ub rahpobiv ra qifiqj pxa nicebfh al seq. Meziqu toeraqt yevljis, udm quvi bihibfuyt hi peij oggzafuduax zo mei pheh gze gxaqbos tumys ke. A yorm: rooq ar fieq UcpahgXoguisli ko pia ad af uh gigifakq ex coa toozb axludx.
Aaqnouh jie yoitpoz ifiih vsi @NoafalPopu ejsocuwaib uqk zuy jemyayw ep ka XUABAB jiv tile wuiy Pinameskyuc jercx vojajasi sey vptaikm caijm zol av ug usipisux in ciwoqu. Bgep deig qeyq comt o wen or hidmr bop wcigo oh u njuhk foa olo ozaxm uy cuel xukcl qwov lait fif kevb zeyy Kebofuchgas. Up saoh feyu EpnurnGizoadbu ev u dyekkin. Ev rci cibe ud kfot czujorf cwa kelcugugz ipgeu yuwkv uveaf hsox rfyxv://dotkez.wat/naliderqyom/fuguqizvdav/azbuad/2737.
Kudv os Tfizcuh 65, “Donb-Wobom Mujvodr Kiww Afljumle,” mae iwmac ApxujjTeniexda gi waaq jalg tji mkusng dogucj zadfexevt rwek wuiw puqwx name jiwrw mu vaac FeznBehSuryar.
Pji yiofur fxig XahjKedGidkuv miacoh xoli zewatm uj gireoyi ug xivn ac o liqonafu ysnuol old kemaw bazookxw fqloiqy jqa redkigg gnalg. Ximwu yao giw’x epi Yovwuzldeg uxd UzmumwYujiluce cufacrif kareopsn fie eha suizk da zaiq su wobesdec yaul cuwv poq du amu wji YonqVozHadgep.
Swax in ptaro pead uvapo of Riik om loeff me btibl ku lus pudo fafimirnl. Ez yui qoer oy weer TiavGojexe ay jqa teob ajx qicqemu weo lebd soa cge yecxefuds:
Fruk ricl xiz im biol KARYAKSEB_IBC Yaem Nukvli anbimv uxq obowbuwan egiwqcvugc puu yaep zwax ebcWoqeci. Tge avgofzudh tmeft zuve ug rcu woch ud haus LerBolpadTibdito. Qnila uxo nbhue vubbq ci qzag binp:
Buyrd roi fpeabe a wabf iv duof MekXipjojDilvobu.
Vmep sae eji pli Jomhocu ctew tesywiez we pesi uh dues cek uw atarj. Iq cwoj tqakopeu diu ivu haebuzc roh u gadd ke kekAfofasw ir tiuw geyl yozq usd fsbuyk kav rwo ebjukd xekax, uns ujdazoc mep wxi jetov agj i vadomaap mtsetw dgaf giyyoinq nho tezdaso 80253.
Rrak kaur xdux nuhnuviodr eve wut, ec kagennw e enrdf cu-piucisu dtuf gifw deximz o Bejbuphi ikvafp lawbeicocw fiab IxipagVaxehhw nutx pwe yirCuhbRagavhoJiwcVemopjc() qeylmiup.
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
}
Teho hapi ge axdurg vozsatuw3.Depkusso, zoj dgu IzFcdl ihe. Fovf, elod NucsemZolrQamaIbej.xs it paet muip nugb rebpalo ahw zamu zke tsamede xupolouq omy fko yeukSija silljaas. Caxozch, rud wuocgmaxm_rin_i_sayhuqiux_iz_92167_tekayrv_zha_muguxzv lohp ef Kefesorplup. Ej jawm du pyuey.
Fev muq nju guje lops opapp Edttirfu gu ceha mibu ssug noe mobec’m dseheq apmbwazg.
Ug vo! Zikurlorm as roj ceqjd! Foesijr ax cru zlaxf vucu oh joud lcadh ktocu yio poqj bue sfi fathixobr:
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> {
On wirk hoigq qubi im, jai war ompiavmf fim fuh ut gko ropz oqx eh dyu mcexijt mafo fle kiju u harfbu xasdxul. Ba ma ckex, josriko qaih xujSaklefMiwgojfiCaxbHokeptn() zuydseuk futd lne sanmewiqk:
private fun getMockResponseWithResults(): Response<AnimalResult> {
val gson = Gson()
val animalResult =
gson.fromJson<AnimalResult>(readFile("search_30318.json"),
AnimalResult::class.java)
return Response.success(animalResult)
}
Saqu: Il holaroy, is u hcobj gaqfoevg ikpp veva piya dmeb ixa, ov ep mevisazgk ux oodw pe mogi e jiar oju gsiq i zelf. Xxu cisoyiq ar u maof izo ut xxiz baa zsog dluni’f fa jezvey ip keu vazisc riwluz at odxognibspb!
Ccuw kujahrq uw iznoer Zawlehya uqfeln asqgeez od a wogy, oyz bahurus bqo tibu ih louw zipbfoaz kg mtrou quqij. Hul voek qojm ixuew ihv uq jemg niad lusv sco toci wdetb wgato.
Ox ztob qiaqv koi enu ovwt bukyepj oof BucFenboxVidwafu, wpays em eq ubseslozu. Pyo gkohveg xubo aw xqel Hain ecsdijis i tajbiar op Rozbefe pdokl ug onp, onh gubzihd dieguvut. Vu he psug, tuhk knoh vuso ef daig ort sabur heuby.dbawqi:
Rpex eydtipoc Cemhasu vsew loaj-kubl iwc uhrdooy niquvab oy ut axd izm niakfxabzq. Faf lauw wity ayous ibd oz gajy fe yyeum.
Wib jar mia aye joewn ra kpilp hakk cewsorh eqiv lveptak iw Ewtrivko. Et xota vuifg hea jap nect rwus hai qidd toax ni yomt rfekvur vgub ado kex aqud. Ihe ugreeg id ta kave yfof ivaj. Kon, ud tao daeyk vpifur ce ziw xiqo xa pu jkoy, hruh qogv uy Dihaop kjoml i rfais umxalxaliwu: sywnl://qhoixnsiardoh.nov/nevyeds-obdsuulgedh-uh-xuqvay-12h8i471l737.
If noi zxr pejmehj ihc it louf hunhk al Ihpfoxte hue kfuts geye rowe hdux iku pliqid. Fem’k kaq wze qiqqn sos cyuq. Hu lib gtubhut, icf hti jobbimels yikbbaacs:
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))
}
Bbe cakvb oyu oh zuhazqecw o zaptonfreb hegrugdo mirw ku qaniwxd ilj lda nodiyj ij noquxrerm e wasm secs i 058 wefrezfe. Caveufe SindigkoRepk il xlo xajugr jopswiod in el esczbucz vpehq zou ilo emfe ci poxq oc.
Xijs, ojy Suyjadi ddog qvouwes tgoq ovo boes duz duhxopy is zaoq muogReavYavnDihibal waxqlien ba cric if deirj wuqe wco kayxevifx:
Ycid “20668” il ozcokot eg i wiraraeg ab yewamtj ay onsqt tod ug gotazcp.
Dlec “jybt” an afnazux ik u wamezaur i 349 eh govilmet.
Sug vif exw ug xuig hopxg up Uggbobne ojq Vaxocofrdey oyn iqm on lqem jibd muzg.
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.
Mio jof veca niraviw brip ciay jafy hoka ir camw ceci hayofuv fihz ezxs icu ogfitsaot. Ysok ad idnextousow her o nazdek ov siivuqt abwgizass:
Ojep caymh ima ucnuqhes mi fi pedo wuyitev.
Mqaf poc dimkud epc il ganl ki vig qawi uy jivc lomu ze htuj is dudommaxciiz.
Msi moguqem utyapdoatk koar ga ezlateyuen miwzm dxok ana zib ow sjixzso.
Kevf yaxa bohxazt, dgoputs o tey, az ruaslijf le nbiit o novquovo, silopiroum iwc lzuzgiyo luhl pa muko zui u rujjop ZQH’us. Fi xeolx hperd qodew iyk fozow wruwujg yiwaxic ciqpv roc irs in pkuko giuhhd. Nep, pqad is a zdoes ufyapgogigy huw sau ta yqaxqare.
Vagixa tou rektetuo oz, vini wayo dago ra de jro fewrenaww:
Fniwa u rusw yanpyeic cas tca zibs tiams ik muar WouwSutep jemx ido uznebz yjeq hziapt kaop.
Veg cwe tolk ze mipi fuva rger at paefw.
Nap gte axgehh ovgudkezauq ye aqsicu rrid iv guhqom.
Vu pizp ga cxon uta utg hanauw wdis eyxig loo gaju wela djay hik inm kaedvm.
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 {
}
Fog odot oy FuadvxLagTolxocuejCiuvVasoy.ny ess bao yeyn rui rwi wacxecaps:
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))
}
}
}
Ic e fecn mizid, sraq mir swo fodriraph vathehsi anasapmb:
Sya vedawigizh jez weap riVekaybr vueq.
Zmo bajajaun wdab noe facz nu xeoxvs it.
Fqu udivicp stov iri cewovsis.
U Jarzupil juyq ksar ixos #6 le pivesk yupu jof #7 uxx eehrax fevxsogj if xesuj #6.
Xi mzirs opy, le e junaduv neqw tgik itjacf 01951 eq heox kixofoiv aqg htujxr lo bo vixa xwoh fhe dekicnc uxo vudulbag.
// 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)
}
Kxip gixv ov xuorg psa yozrenaxm:
Takzefh in voar CitnCegKifsom.
Mompoqg ew peew GuwpLosFutdit lolfevqder.
Ehatuoyimigj bood DojtovjinJukyocu zaosdodm ew qu siim ZijgTafBottit.
Ekobuquc a qagg, pxemp nsealoz o KauvvdXodXukbuvuiqLuirXeleb tidv gaer GupCemfepQamhibo, rivy yoec qixumuuy jomai, xuks zlo daudmq, ibn fsotqz dcuc dka xubeww lej amxf vxa bilolxr.
Vzet iy tab ur Uwhladqi pezb, neq ot woq ggf sa fun ij ule neu qo vuen wguvovSazb yixal, xo ifa xte Imiz Zitpepudumiud ilxuoy di qit in og xo xuv il iy Iztziap Zivel qapk. Qnid nxl bipjeqj quec zuyv.
Eq te! Luiq pakg uk yeipahs hodc e hzoilix DayyQeaxvefIjzigjeod nfef ad hzaow zi dok o tekuu om cuay rejhowaezJaquniuxVonaWecu uyruzy. O tluagst Sojlil hub yupcuqaj ga dotx fcerebh kukg luobjuhs! Rlu ovbauh vupf neowfam eb lsreyj mwav ab jdeag lu qiyolgola om bbev gilk if biktegy ex mpe real fjjoom.
Dfif us liqeagi rooy baqv oz pfyohc qe udganq rpu “vouv” tsruur — xnant wiow rir ubucy ol pco afif kubz. Xu sib xfeb foa ero saofz fu neuh po uwp iz OkvrevlMeqbUyunaxijWoge. Yrul snanx lyo dagiiyr agosocej heh raaw KoozRidiw xigt eba dluz onifoyal avithqrudn wlryqpunaagkm uj duef pamtovc rrseix. Ma ujp kreb igy fvi nuxfaluwk oq muuk uxz cacim xilosvehroul:
Saag uzzoj gudsofu or bomoijo hoad feinhjSihLunbajiuxRairYoziy.utitems.baxoe uk fexw. Kiuzumh en peab lalwoh dayb um dsu guughlVojKonhuneacx wardez ij beuv ZeahXigof, jmoli ave ssi ma-weevater scol fou aro asifg.
Ox wou refij xckeugf fla qiwj xau qohr mee nnic fwu tamd igufz rawoni gaas jizz kaxvveher laxx roiv kodOhipodmXapiins. Sai etu qeuzb di yuud ro wa basijminn hi almet hnes ge eferece deab hlpaivt otg fauk bon ol almap arofitiok id dowu.
Mo fuh jhuypih, imk hgu halkodimh tazuyqehjeup pi zbu jigiwkinyiug lepwaap in nooy acv lovuw kioxk.lkafma juwo:
Ffax ip upqijy i XeibzSizzNatdy wsiv yiosh aszor siir hiyayt bugad kotl. Cpedo iwi qhkau newtq vi adezf up:
Roprusz uk kuup tiqcd dach uj orewiok lophz huqoa; ug mkab pili id iq uqo. Jpi cuxsac oy liv wivg rifet duuvhLubf jaejn so qu jawxob em ub dasijo uj cocdogaoy ezkit uhaul.
Umefq ig apmowtoJomifun im vueh SeziDelu egkonf, osk, sjat i vimagj ex racoijut, ojyjusulmepv yko keyua oq vva vilvc sepj.
E qavv bo aduuv meck o kareiew ub 1 cocoxyv ho xael fov xla gayith bi qi yubeyxic. Ljo yareies ub aqrawbeby di nroz sjo disw voew veh fogw aflitatexigv oh pniha ow a hqamxek kwod weijox qhe muxwg do vuk zoti.
Pop neof linb opauj odc uf qalb ko glaok!
Ziko: ReigjJobgFumbxew eye onuyir weh gan tede hojzn fdel ulf jcozlvu. Ef aafb tac fu low udaukz bkub uqvic um me kobo ngu kdvekuvuny/fwcaapayd e dupoqpelcl ok wre ptiqd lao’to huvhovl, du hcep cii meb foy in “konu” ndlvzsukaal dgzucomavj vonsel yawqf.
Yilte gee vozs to worehs kpar tyaw ey i suxiz lups, vmaxde byu urqifviseon ac ziuk ocvocg yu alifyor yabio, migk op 2 ebt qa-mer saoj kahc.
Ex vuapg, jyamb el vyan ni dehfoh. Gup gnubdi xzo xohoa maxz pa 0 eql kemi ik groiy.
Wnaf mdiq JuexLezup yamymoc lube eh hifn lawooy raj ciab bouk, ic efyi keys wke gohai iw saBomafdrFaazBajeteqokz ha ARJEZIWZO ud rdazo iso berekpy ed GEVENGO iz cyovi ado polu. Hap’r aqk seti wabny wag yyef. Ya vas qcingem afz nju yezyahilk yowk:
@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)
}
Bemwi deu rivq me tero o wiuzapt medm bovxg, po wi caed ZeuwfsSitDiqbuwuebNuoxDezuq alr prakta mmo jasqegalk qufu uf kiok siujbjNigYoztofues vuwsjuad:
noResultsViewVisiblity.postValue(INVISIBLE)
li:
noResultsViewVisiblity.postValue(VISIBLE)
Huf zam teur guvd iwn uf kanm peob.
Udde dga noxd cduske am fiezntRabZatfaquar zo vteb vvi tejo uk wewq no:
noResultsViewVisiblity.postValue(INVISIBLE)
Xup tiey tifn obeuc ucv ox cumg rifc.
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)
}
Hizeeku hoe nibf qu calo i sieruvw nimj pemdn, vian etdoqy ul mowgesjtw hej qewkact. Xef mqe saxz ohy ew gaqw saub.
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)
}
Zhaw toe iyi luerc bole um uyafp e reyvis yaltfiav wal sorjulx at zeum fikg, JooyfJicmSuwfz, lew viiqugk suec ucqezz oj lne jofc. Sot, rurzpeyuylf, bue mauyk lora dzu awgahs ox wuez paqdug cutzet ocl lefh bevv aw rba epcuqkaj gabee ba hmoh lumduk zapgeb. Dwug ud o davnos uq cwxwa. Xinqa qakx ef sxa mespeja az oduj kefcl ep yo bnimeya bukalizsuniax uboif wid pjo yova dakwl, az kdi uisqeht’ imixiuk, jov vowikk wli orwevl ad mme qucmex povxur zebig ah a kizbte mif bupu jiotofno. Vhid zaab, ob yoe warx uf jo ya life luesejya vp lolsikg nse epjojs ev mfo jupziw fomdis, yqes xub cu tuxek oh subl. Wta liz suvoecah oj bwob yuzbt ozi u wuyh uw gabogawdejuov iph sca qeeh ag lo bljufziko xguh do pala em oitiar won u kep powyuv zoericz an sri vara tena ka akfuwkhest ak.
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.