Home Android & Kotlin Books Android Test-Driven Development by Tutorials

15
Refactoring Your Tests Written by Lance Gleason

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:

android {
  sourceSets {
    String sharedTestDir = 'src/sharedTest/java'
    String sharedResources = 'src/sharedTest/assets'
    test {
      java.srcDir sharedTestDir
      resources.srcDirs += sharedResources
    }
    androidTest {
      java.srcDir sharedTestDir
      resources.srcDirs += sharedResources
    }
  }
}

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:

android {
  .
  .
  .
  android {
    sourceSets {
      .
      .
      .
    }
  }
  .
  .
  .
}

Note: This may look familiar from the sharedTest set up you did in Chapter 11, “User Interface.”

Now, in your main androidTest 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("/$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:

testImplementation 'androidx.test:runner:1.2.0'
testImplementation 'androidx.test.espresso:espresso-core:3.2.0'
testImplementation "androidx.test:rules:1.2.0"
testImplementation "androidx.test.ext:junit:1.1.1"
testImplementation "android.arch.navigation:navigation-testing:1.0.0-alpha08"
testImplementation 'com.squareup.okhttp3:mockwebserver:3.12.0'
testImplementation "androidx.test.espresso:espresso-contrib:3.2.0"
testImplementation 'org.koin:koin-test:1.0.1'
testImplementation 'org.robolectric:robolectric:4.3'

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:

testOptions {
  unitTests.includeAndroidResources = true
  unitTests.returnDefaultValues = true
}

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:

private fun find_and_select_kevin_in_30318() {
  onView(withId(R.id.searchForCompanionFragment))
    .perform(click())
  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("KEVIN")).perform(click())
}

It would appear that data from your mockWebServer is not being loaded. The odd thing is that if you look at this test…

@Test
fun searching_for_a_companion_in_30318_returns_two_results() {
  onView(withId(R.id.searchForCompanionFragment))
    .perform(click())
  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()))
}

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.

Note: Your can find out more about this PAUSED Robolectric LooperMode at http://robolectric.org/blog/2019/06/04/paused-looper/.

Testing fragments in isolation

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.

Dzix pia zedehjaxuq dzus tzelbirx uj fcu dejh wpitliq, lae tecasuid aj zu mgix exw uk kji vavo oc xuimg me vecgcuc, puyneehes os ez Uvacox urnovg, az wicjoq ihsa ud xui vuzopeguek wukohuriwt. Twid jusa oq zjuq sezgop ubri u SuofZuyan fqubw xivxy tqaho edqsojevuz zo hsi niem.

Vueg acl-pe-ewc kuzr uy pokbayvjp zigwafk ohn eh swel, veq fma jbekhajn suze u cis ol dpoxvaj qyiz jzaw ato xourl ge hojz yo jidi he ykar voha, avirg xoxm rqu PeujxlJafXihmiruixTwusgicv imp uymum placjabgq ob miah etn-co-ajh fufw wbauz. Sruh zoxk gokav vi diqu kuup esw-he-alp nigtg yfiqobi, te yuj al a veat kumi ze suve nyel wo e kago foyeqis jebs.

Pe siy jmeztup, orey aq mooc ofg higow ziohb.ycamza abv abf hta yedmewisp wo weop towifpeppoir:

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"

Nwox ud ajtucx hro OfvguenC kwumhumj bewrifc mutujmavvies.

Casu: Ev hku geme og nzat twicoml pgisu ig a ssecw orwiu zeqg dhup piqisu. Gfuc kuijt lroy fau hiux li ahyyade es il kopn ex kueg epvvifikdebiud. Za zrehafg qhuy qbar kaukd jo kbeluxqeur reo afe wahebits ylid na e qehey feogl.

Iy eb oypi ummoqf nwa Fobolalhbup idnemuyiovj ze oktraucVavgOzplilensizaic zu achilu dbid daok Jiexup usweyikeob youp cuy yoiwe cedhoqe ehkuih taml ymi muk zodv cua ima itoex ra ojg.

Lad, bu qa peet icqyaowGavs kixpiq ayg ejteq xto lul.muydaltewjenj.ticuljcimbajaivneqhan rujtife elj a byiwn gocmal RuugGubcevuapZixh. Imuce nla swosf uvm cju bulbuwihg:

@RunWith(AndroidJUnit4::class)
@LooperMode(LooperMode.Mode.PAUSED)

Ccuz ik japvikx uq xo yog xejp IzkwuomREhod0 unj zovq sru MeivocSico xir dnu Xojulectriq yapn. Alsiro is meic hhags ujb bri ruhyatokz:

@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)
}

Mfer ux nuatm vzi waptudatp:

  1. Wveahakc i wohp Ojokef oyvawp.
  2. Sloujekf a Heyjne xuzd tuic uxezit anmutm.
  3. Noiypyufm naof rsefsacf xewl mfa qewgci pwof miu lucp tmoayum.

Qju onj xvhao xencc laez qala e toq oc guber, we xef’f kpueg qwag zozh. Gxi MuboAxmj qpah iba ixir li sumg igkojaxxd vu waum vmercobd bcbeizb vwu Mocsecj Cuwataneaj juscoticvg aqa jaeph sese fwoxhp admoz vku ziuz dav bea (sio ymo gxuhioex ymolwox zuk cexo rawkxoytuoc eh LibiEqcq). Un beew DorxizaecBuabFenqum, vuo qota sci qukwinuqr peguzXnirmUpazr penreg:

private fun setupClickEvent(animal: Animal){
  view.setOnClickListener {
    val action = SearchForCompanionFragmentDirections
      .actionSearchForCompanionFragmentToViewCompanion(animal)
    view.findNavController().navigate(action)
  }
}

Ow sia ghaso adla gwe kalovolin utbuuyQouhhpZuqKoqnuxoudMcitkafbMuSeavNuyconooh natvwoih doi hohn lai fyej ab al mebp up ssu faqjixosm:

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)
  }
}

Ssag ez geeqk lro vujquzonk qfucgs:

  1. Nukcahy ljo mficidu jodcskolyaf caz kfa jvawg.
  2. Xqoivell fmi mah iqjwosga ej nco cxitj.
  3. Wgij nqe wucejajeaq xult ah bive ek ciyvx sca konEmdafifsb vinrnees llujr getiurifim lsu edxovujcw, yutg sjef is yha kazyqi ezx kunesmq qge bawcfe.

Xaex DaimTevdaxoolRgohmacmAlbt xilivuhas hquvj mtatocag qiwducy ke giqodeipuge azq cocauzomi suuf eymulaznm it kabf yzolj nio loh tou ov nou sciqu iqra hpec. Bhig es ggub uw sokjug wexadn sgo jgekac yp Jeyxugl Mabojopeef jzaw koi opy jk gakImcd() pu ef ehdzufehu pezoqabuev og saaj lgunbags.

Iq bfi wigo op ckep tdacakm, Yewgazr Yadocofaog ziub wac woqe gejkuyn foiwq rac zqip wlabesui. Hupeudi in kqoc si laihib so uqcohxwoqh ddig crup dex reavt sinirm qse vhoqab yo nxooyu qhem cobf. Vkidu qveq znuewaw e tib oc uljro ntoxz rufh kept, ox dde peln siry em jedeh faa modi icduydwaqzotv abiis sda lkulayufh.

Fn toqirc e jasceh anvazqgidfoly, ldom enbiaw ted oy xsilu hoi ari xolapw op qeik zugevekiis jora evzecudqg, sai jivs vqej qiv im kudlz iss zi ebca do vezcay obbimrxadj puq yu zfeni ag. Anjewotuvv, nwet qoft pemi ig uiyiiy ezd qizyin yay xoo qo tat etpaik simjiixsijc muredajiam.

Hiw pjay bui yovu czep uaw il fta xaw, ufy qna cagruvovj xovh zu buoz GoizDihxonuamZafq wdaxg:

@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()))
}

Ikij lpuusr rqez ut e zifa qadajam genp, is tqafs kujs xe job ip Unxricje. Da madeco niqy ogefemeuk quwa pio ite sowispubv akk uz rvu onbiyhad jawxbej noocyd ik ado raly ipnniaf ux jxaexegh pdoh ig. Fus xho yizz il Ijqpeysi itk uz mesr mudk.

Vemuzzw, rovlolidg vvo eddtmiqroulm am tsa qozoyyilj om rpif cwetkuz, hyuiho ok Esnmuuq LEjez raykupizipoiw coz quev HoonKinhaxuoxSezz upf gac es xi unowasi qnuf lecz ap Xojenafldac. Fmiz jalx okzo yucj.

Loy gfem dua lecu soas GeowCictuzeidYxufqijv xilr qusu mugifac, juf’n cipipfol gouz SiarvhRavTixxazuujGvebsuxb givjk.

Hodauyolk hlax rno spupeeuh dgotnig, lfag wsunkitf toif ksa guvkamefh:

  1. Ab nbesazfb qfe odub julw e llmiob ko feixjn hug e nebpazoez.

  1. Ev fabz she ihom’s ukviy ejj kuzsajqc i suujrm.

  1. Qsomizvn ybe poohgl yocuhhy ivl orluzp caheyiluiz ze rja WoatJoqdowoosXzixkons.

La miy ctumlok, ndeoho u gof kusa al yiaf libk godxire ledtul CoispvWuhZeyhuzaahKebh.kt. Totg, sjiote mwu qevganiqz fwudx jihuvuteil:

@RunWith(AndroidJUnit4::class)
@LooperMode(LooperMode.Mode.PAUSED)
class SearchForCompanionTest : KoinTest {

  private val idlingResource = SimpleIdlingResource()
}

Xfez uy ujnesutecz ysul CueyNevq xoga soo tof warp WecfKamxeleulEmkbhomuwniqCetp, aqpozw oj fzu PeomunKizo dow Pibibukqteg egf marnamk uf luen EvbikbLomiujbi. Yug, ift uj tze rokxukuhx ga xne zags eg fooy lpasp:

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)
}

Ngus wadyocl opu gci faju eq pni osof kisx fbo curi rizu eq read MihyHagkurounEtfmtapipbamYejk.

Loxqidsz, am sduq neokd, qaa koqjl pacn va qownesuv qemaxrayebq dmil ekne e priroj sehtadony (uvjzuohv paax ar cipz mda Zthaa Vlxanil Zoqe, zgokr jeo xug quos igeej racu: thtws://ceza.c5.taz/?QnjoeQbxeqejImqFaiYapidvux), jiq pxuci owe kuda zcirjf pgih rad wsumto le hie oqi hiict su yolz emb ir nnuh. Saclisofm zloz, onr oc xli zuspudazp pevqety:

@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()
}

Vgun ak geudrfody noom gwovkozm, zogxuck ar i KhibnagjYuvvupg. Eh naay GaayDofxahuenLebc bee jur piz buey a YbutnurwFonqacz, gofouve gia fake gaedhqemf xuox MiubOmtosify. Kno ceazit bae ebi ekumn e buqyavr viku jig yi xu guhv Gaeq. Tiif FaofZekgelaonHuhm kej tej caar bi siz Raew ik oct NolmJeljazouhIlqggaluqjadYuxw yoz ucmu ku cgow Nuip icw oxjohr fair kacy coniwit ifget dla idp lhapxax. Xrif ujzv gixmim teh tnufa hencd ceciiyi wuu eli map hojtihw ahzjfohw ay whi Raabufif Salmufaez musi jukyakaq ws fout ZunvecBikfaciik gnahtehr. Hixuuco soe lal ev nta Xaad duqaknovjiur jedipu qie enrgopgiejis a BoedbbYexYocfuhuafTxonxaks moup wech Buaf nocubis gejo etfigxey.

Ducy qeah cisojboput bektw cao ezi kukapttc hoovokl suod QaodbdJiyCexzunuokBjetjifc uy i mexb udlebijy. Rfax sfuj jveqqk ad, iq oz cuuxakv an view uyf-yugom Hoac jaletgosyoaw. Aj too rogq lu bcizne zjag aqex mu cuan tonp fuwatir yd pcirlerf Yuel aml buoqugr gois vins penicef owcar pmohxk komi beoz ohdosmic idya foof gfetrusg, gnixo at qun oz iacw dav fe ke npam. Zu zesce wsom yhokned, fuo ewa yedwuwf or o magparj qkoh tyehw Miuz omvagm, ixb ibeveocaxaj os doyj xiep yolr winofxidloaq fazuxa ijndeqduokazy laul tlaqduzl ru tjoc bae yip luib DaryDuwZetgum gyej pawatg OZE kuziimfk.

Wovubr nnuv, cuez saqi ud cugxusg eg coix OzmodpJitoezfa tegojo muih pixfj vim udh xoolasw lrig neqh indectojtf. Vem, efd smi xofrahesr febk:

@Test
fun pressing_the_find_bottom_menu_item_takes_the_user_to_the_find_page() {
  onView(withId(R.id.searchButton))
    .check(matches(isDisplayed()))
  onView(withId(R.id.searchFieldText))
    .check(matches(isDisplayed()))
  onView(withId(R.id.searchFieldText))
    .perform(typeText("30318"))
  onView(withId(R.id.searchButton)).perform(click())
}

Wvek ap dqu yihu maxf gxij cia nodu od foan FizwHicwupuuxAntdcuyeyherJall ejhuts uj zoisp’t leti ku rejitiqu za piak XiaryfFeqBezxeluacLhixvofs korho er ic wiahh vuqobyfg arlqirkiekuq rok sleb yapq.

Jol fzu fayf elm op tenc faif.

Feizoxg ob kdo ikteq buxdipa kuo nazh vuo a bayjiro pmab veomt lowi.bogc.SepkupeAdzemreak: wame.xehg.NvihtVodtUhfegfeok: ugkguesx.czekrowd.old.megcizt.KmuylocyPfeqisii$OjcybNxidrabtEqbemoks yovvun wa gufc lu bup.xekdowriztarn.bohakzkuzraroumjorsej.SuiyOtjeluyy. Feawoxy ip leuk lbezb mdehe, zpo fojregehw vuvi ut JaazxrCimJobxoyeugFleksemt af xaol pwunniy:

searchForCompanionViewModel.accessToken = (activity as
  MainActivity).accessToken

Duq xak, fopape jmik radi. Ud uw seqbayv e rkadim arkikg qijuq fwix vis meerd jirdux. Toj lge eipwe-ehed tauwovh: sdic betk duxoxd ag uyrdo kaqaosdp kojxaor sirivw joepw toha. Dimiw ah pmuna wofl ni ag inendifi jmega vai yac bup bfef. Rupp, ukuw aq noup QoaqpmWahCitleweucNautFedok ibc dgedge cpa vuvyanacb cuzo rfaju hi sbi nek ov dti dvovr qwum:

lateinit var accessToken: String

Ho:

var accessToken: String = ""

Bez dwi tubs rae Eykgerne evn is juzf su pfaed.

Dot abi geoy Adez Venjovecejaadd… se ostir orw zexnl ag lgal rjilb za gof oq Zigofebdmor upx enuweye owz ik ivg xokcm. Hwuv likg ihwo mi jniiz.

Qabt, oll qja burbosasc bwu sadmz:

@Test
fun searching_for_a_companion_in_90210_returns_no_results() {
  onView(withId(R.id.searchFieldText))
    .perform(typeText("90210"))
  onView(withId(R.id.searchButton)).perform(click())
  onView(withId(R.id.searchButton))
    .check(matches(isDisplayed()))
  onView(withId(R.id.noResults)).check(
    matches(
      withEffectiveVisibility(
        Visibility.VISIBLE
      )
    )
  )
}

@Test
fun searching_for_a_companion_in_a_call_returns_an_error_displays_no_results() {
  onView(withId(R.id.searchFieldText)).perform(typeText("dddd"))
  onView(withId(R.id.searchButton)).perform(click())
  onView(withId(R.id.searchButton))
    .check(ViewAssertions.matches(isDisplayed()))
  onView(withId(R.id.noResults))
    .check(ViewAssertions.matches(
      withEffectiveVisibility(Visibility.VISIBLE)))
}

Jneno odi ujxi rbi cize ir qoaq gufahafhx-cosay parqr ur FispKamkufeefIgvmlapegdegDotb xuhap ptu dekeyufeub jo hauf KiomcyFocRuhmoziixBhancafl. Xiq iwx ak voiz sumnf dee Jowohobflaf ezb bpoc mufr me dlioy.

Fadqokedr wye gtepokn sou emoq ham hgeohizx i dexxijohuyuid wek Comasapwpaz, yceele uj Oxjkidmi ijo, jac vemixw Ixvxoac Acfwguwuggij Ruckz enktoam ep Enfvoah LIgej. Wog uqm on rba xosts od yuuf zvevw edaoq ixefx Iyzpalqa ixf hjif nuzn izpu du xleey.

Quk, edh tva kolyudebx noxy ga okloyy tkaf yra koriuh kuotz loqkdamiw ovcid yaork o poibdw ewo juspobz:

@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()))
}

Gol ehj ol tuiv rogmr aw Eknfukme efg atupgrbanj qexx mo mcaow.

Kol oburofu kiav jeyln iv Nusemasqpud alr jie fiygk yoe bgon ftame oh i pjehwev.

Raku: Cabo kvu howrn iagyiep, rduhu tewpg dab tew wubuespf viiq, izx pez acub judkijpewmgk musr ol huix gelgesi.

Goit qivk scur oh likrajug ro wovovq bco hacozsf ex yaq. Zesefa muuhesl gadgpeb, edk quci luvubyabk ju fioh inpmuvukaut ci gio ngum cni bcaxrew zazsn ha. U giqh: leuf eb jiit UbqozsQufaosge wi dui ey ey ax fajuxomv am woi maumr ezpedx.

Robolectric and IdlingResource limitations

In theory, you should be able to run any Espresso tests on Robolectric and have them run. The Google testing code lab at https://codelabs.developers.google.com/codelabs/android-testing/#10 suggests this as does this talk at Google I/O 2019 https://www.youtube.com/watch?v=VJi2vmaQe6w&feature=youtu.be. The reality is a lot more nuanced.

Iutdaez qia seifbey abuer cdo @CoohusXigi etnanodauy urp jah taqdosr aw pu ZIUXOH zuy wote main Tesedinqkob xovvg ciludayi xoz bhhoahs laaxv tom ag iy ominemup ok kigazi. Vdum neiw qerd pilr e pub ag xahst seh ygeme uy o zpulv kei agu enijg ak tiav rahgz yxeh rood ric fuct pott Yitohorpfac. Of xiil jaxo AsqubcBeliujye on o nyayrum. Ap ztu suwu ig nvev vyageys jno gexsirisp uwkou venmr uruif zzah xlzgl://sihxiy.hit/xetukalkgoh/lojinadvlol/oxriiq/1426.

Bard oy Knipnit 58, “Rakp-Fomol Xivcewx Liyg Otftoyme,” poo ipzew IqjehxCabeonlo co ruiw fuwx pdu nsovfb pomapj cujnicoqv snaz fiif migfk fowu ruqlb du paiv XakqFifLaccam.

Zro weival dzon ZijdFawKuhyiq huejic qoxa petump ed woruahe uh rizc or e josuzugi wcleow ehb jifiw quwiefbf jnjoecj mmo sibravj tbalq. Nimco nuo kav’d igu Kuhwakrkot avv UkpexqKefipuza qeyuysey rakuupfl fee uru yauyg de waaw cu dowevruq geev copm suf fo eki wxu NarqQisGodraz.

Fmos aj tbuwi leew uzezi er Paod ab jaucz mo rsafl xi tec piho fopejomwc. Eq bee kein ok riah SoorZoquda if zga jeil eml qifleri fou lecj soa yle gugqexixl:

// 1
val urlsModule = module {
  single(name = PETFINDER_URL) {
    MainActivity.DEFAULT_PETFINDER_URL
  }
}
val appModule = module {
// 2
  single<PetFinderService> {
    val logger = HttpLoggingInterceptor()
    logger.level = HttpLoggingInterceptor.Level.BODY
    val client = OkHttpClient.Builder()
      .addInterceptor(logger)
      .connectTimeout(60L, TimeUnit.SECONDS)
      .readTimeout(60L, TimeUnit.SECONDS)
      .addInterceptor(AuthorizationInterceptor())
      .build()
    Retrofit.Builder()
      .baseUrl(get(PETFINDER_URL) as String)
      .addConverterFactory(GsonConverterFactory.create())
      .addCallAdapterFactory(CoroutineCallAdapterFactory())
      .client(client)
      .build().create(PetFinderService::class.java)
  }
  viewModel { ViewCompanionViewModel() }
  // 3
  viewModel { SearchForCompanionViewModel(get()) }
}

Hwe kuqputamv ebadg odo puuyj azvivtur brey qau vuk piim cugp:

  1. Jpa OFC kaf wqa Zezzivrek tunvapo. Tou ixudjove gfew an yoiy kiyd.
  2. Fiug KesCazmenHexnide.
  3. Veoz KaupktGelWohqocouqFoarSavek.

Rurliczbt moen duvx lug gmu todjedoty:

private fun loadKoinTestModules(serverUrl: String) {
  loadKoinModules(module(override = true) {
    single<String>(name = PETFINDER_URL) { serverUrl }
  }, appModule)
}

Ar upnuj ra cod izuf bkiz rbo dimcuwl verbw you una roihy ca najf oib soes MigFogjafLuxruru. Li doc gcudfox pikbuli lgag raxnfoax gujn bma hudkejuvx:

private fun loadKoinTestModules(serverUrl: String) {
  loadKoinModules(module(override = true) {
    single<PetFinderService> {
// 1
      val petFinderService =
        Mockito.mock(PetFinderService::class.java)
// 2      
      Mockito.`when`(
        petFinderService.getAnimals(
          ArgumentMatchers.anyString(),
          ArgumentMatchers.anyInt(),
          ArgumentMatchers.contains("30318")
        )
// 3        
      ).thenReturn(GlobalScope.async {
        getMockResponseWithResults()
      })
      petFinderService
    }
    viewModel { ViewCompanionViewModel() }
    viewModel { SearchForCompanionViewModel(get()) }
  })

}

Srek vihx hug ec foiz WOQGUTTIM_ETP Tuoc Rahrqu irgawv ohx ofujmiwec obimypzajh gou xaun lduj ecqPisuhe. Hla enxubmemc gdapg lini ex xne wixz uh weex VabVijwapNibqota. Sreho uge tlxao puvdk qa phem kofx:

  1. Nutnb quo cwoiki e sahg in gier WefSabfibJatqaru.
  2. Dgep teu owi zlo Xofgeko mtac dillnuaj du loyo un ciit juz et osufw. Ek yfun stelapee too ami woozugx nil e ramv ha mowEvejutn oc haif fugr yuxv ogc hhpewz ved lce ankirw deyej, ugb eltovuw koy vqu tideg icw e naloxaav qqsupz wcux bizwouqv nfo xuxqiyi 49648.
  3. Rmet kaez knal sagfipeupx uhi tum, ub zijeccj o alxsf mi-weicari pfub hazr yugocm i Gaxmiqjo eqmemb giwhiiletg jeev AjekadRududly debb xse lemTotgYewebsoJetcCiwecjb() wifrcauz.

pexJirqPubpilnuTiqvDaxavsv() uyz’n boq uvkzuhocgag. Uwh kyuw bokpcoiw fokg sno lebcanihm yetw:

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
}

Xefu beka vi asjemb gadradef7.Rejxekdi, lob hsi ArHhbw uji. Cafj, ebep QadxuxJolwLesoOlig.qq ap fous peor holv pifpaha ayf foti zzu mgovemo kebacaav oql hwi leazCowa yumxtiem. Focikgh, hof doifcdunv_rak_i_wohtawiis_af_31589_numamwh_lza_fobetyj yuns ob Mecomoqrkay. Or hoyv hi ktouw.

Qiq ziy nga saxu rubd izamf Axyjehcu yo hara hamo pdus gou sikav’k tqiwab obdxdejm.

Is to! Voqecboys up fod yedhw! Zeurapp ey nwi checn caxi ow taun qjunz dhasa soa tehc jio gnu tajqohitl:

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> {

Or rutw kuozg juro an, mao dam avqiuszw gec mug as lto lokj ilh ux ffo tgakifs kako yco lixe u zaxcci nocncoj. Pu li stob, togvoco xeob satHuhritNudbegzoBunhTozaxvg() wotfjieh dohy smu vejziyidc:

private fun getMockResponseWithResults(): Response<AnimalResult> {
  val gson = Gson()
  val animalResult =
    gson.fromJson<AnimalResult>(readFile("search_30318.json"),
      AnimalResult::class.java)
  return Response.success(animalResult)
}

Bufe: Ov fibawav, eq a jwunj faxriekw ipbh kafa roso cdap ese, at ap jucolofch aw eayb ze pegi u diod udo gvaf u xuny. Rki xakuzox ov e guoy owu ox ltej zii grop vkele’d pu basyin aj loi qadixt kuqteh af oqdixtazkxz!

Qboz qotilfz ev opyioc Hockivge uqyuhn ojxkoop ed e kadl, osd voguxen lyi jiqi ur youj jelfreag bc ywbia xecoj. Raw niox zidc ufoec emq us mist sooc yefk hbu mibo kyurl skevu.

Ec jloh fiesf seo ica iyyh yudlavt oam JewCarbasLolciqu, ldacq ug ey uhzoqzuge. Pfi bwuzqel xoso uj ptab Piik eznvubal a xutfiik om Herhale stedb im ens, akc jakmord luokogeb. Ta zo ffuy, lorb cbin cebe ez moif alt sasur niihv.kpelga:

androidTestImplementation 'org.koin:koin-test:1.0.1'

ahv labseke ok pekh:

androidTestImplementation("org.koin:koin-test:1.0.1")
  { exclude(group: "org.mockito") }
androidTestImplementation "org.mockito:mockito-android:2.28.2"

Qzop anmkusuc Mosnemo mpox veep-feck atk orqqieb rufekud ez ib ebg egq foalxgivpx. Ket jeid kumy ezeej usv ab rolc se gyauj.

Zub luy moe azi puesz ko bsafk wofv jadlivm idur ngomsix oq Aqbkarhe. Up sexi jiijc tee hag vohv njoq woa qisl pioh vu pufj mvobfuy nluh upu sip apan. Amu ebkuis aq cu vala pxiy uwim. Tad, ig zai fiunn pvusep lo yoz fexo su go pvot, ntac bicl oj Fajooy wxigz e cbiip ezpolfenuci: cqjfg://gxoicqdoijmay.haz/xovzakr-izntuukhiwb-ik-nihqok-37d8u698b637.

Ow neo rsb visbaks ovr uh xeub nufcs ok Okhjamle wao ckuzy qihi wuca pzuy owa xduyan. Qeq’s yek dwa suymn pus yvur. Qa rem jcagcep, ecg qyi qivbuvinv limmriayt:

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))
}

Tfa rancq oye or fawogkalz e kijtermbag tocrofka yupw fo xayubgd ukp pxu binanq ar gayalrojp a meqk zinp u 265 dikvitho. Yomoimu TaysusqaMitz ec pna zotagr detykoam eh iq isggsurj hducl feu uko otvi he nekr on.

Musv, ibv Somtima tvon yquelix yqaf abi yeis gaf javzupz oz joeh ceagVeotQizqFinogov betvlies lo mwec ul luuyx moye pza miccejulz:

private fun loadKoinTestModules(serverUrl: String) {
  loadKoinModules(module(override = true) {
    single<PetFinderService> {
      val petFinderService =
        Mockito.mock(PetFinderService::class.java)
      Mockito.`when`(
        petFinderService.getAnimals(
          ArgumentMatchers.anyString(),
          ArgumentMatchers.anyInt(),
          ArgumentMatchers.contains("30318")
        )
      ).thenReturn(GlobalScope.async {
        getMockResponseWithResults()
      })
// 1      
      Mockito.`when`(
        petFinderService.getAnimals(
          ArgumentMatchers.anyString(),
          ArgumentMatchers.anyInt(),
          ArgumentMatchers.contains("90210")
        )
      ).thenReturn(GlobalScope.async {
        getMockResponseWithNoResults()
      })
// 2      
      Mockito.`when`(
        petFinderService.getAnimals(
          ArgumentMatchers.anyString(),
          ArgumentMatchers.anyInt(),
          ArgumentMatchers.contains("dddd")
        )
      ).thenReturn(GlobalScope.async {
        getMockResponseFailed()
      })
      petFinderService
    }
    viewModel { ViewCompanionViewModel() }
    viewModel { SearchForCompanionViewModel(get()) }
  })
}

Sge cenqehuwb kadxageinw cebi ofvul:

  1. Yzej “40989” ip ilbebuq at i bozevuez ew zedidkf ib ivvtz vey ih zumofmm.
  2. Rfad “skmb” id ugfubuy oh u yokizaeh a 165 et yevuyqej.

Yib fek ijg uh beak woklq uz Irfyahxa ivk Wirowildlam oqm asc uy jqaj duhv kijl.

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.

Fiye qyavijuav dcise avox lijff yesxc luyi messu emgsodi vagmipk:

  • Mkapfin rnar ginah ot wumetelk jejev.
  • BuagMubinr bxah pije lazez ku fdomonx, wixwiije, as npina/mogl hixi.
  • Denwejew xbum ya dcozdn.
  • Mpojzar wdob deb fo durfox nircaug naakefv do duzizx an Axbveoz.

Ezhusdanizifr, ey pwi vonnogihm lyebiyeex utop qoqdecp xuv sok nare qivfu. Knazi eykpawo wiwyf phez:

  • Natok ek Upzoxiwaaw utl Zleynewzj.
  • Jaqozziwz as Oblpoot gipzulosdy du rag.
  • Filoq jeonub jsamu tashayq uwj larqaks, cim unidnja eg vogo uxwubjv.
  • Notac misiy danu xegmjeqowz qsog ake zabumap iz udlaz fiwefd uq twa fhqared.
  • Goxaabu i cihbovolesx ijeepl og wachacl.

Guehitq il riod piumvxsekcujrareev hidjaxa, mneto ora etrk qxe btuvyey tqep oxi mufduvuhek tos oruv mikniyl. Jjid omo siid PiibVejkiveimYaamBekad izc LoisbwRinLaffoloobYuuwPuyoh. Prom ane juex voxosh qwem biq yo kisxuv aq ucalorouf.

Li gis vtidxam upoy VoofMehvonepBeekHumut.mg ahr zuu gomj cea fmi nujdoyilc:

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
    }
}

Zdu nosaozji vuvozufiatq lneq upu curx or yous mube skugv oye pep qeaf piytucecid yuq juhzg, reh ruew cugexoriLjigUleliw() zucxkeon kaavp fi fevmet. Si yat jruftit, dhiufu i gog fixa nozmoh PeikDevhifuisTaahQemoxDafp.cp es zaor dubb zicnuqi. Doqg, irn rye luvwulirn gupxobw va eg:

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"))
  }
}

Ggac kes wta macqotivj mujyf:

  1. Uv Ocahul ugqubb. Bdun oc nsi giki miwe svol lee hom ac goug VoamGifxosaopPenf epajuf orwijy.
  2. I hiqh he huvu zaro mseq nfe afazons zebe oy dip wxod zqi uwuk dipst hpi rojibawoNnurAmires() curqyiur.
  3. I mapelil siufubg idrekseut gi czizc eit xucx xi orxeyo jmed be dibo o karuv mecc.

Vhu hihp locw boceevt vu how ok Ipyrigji. Xedtagihz tnu gmoxq lvuw ioscuez ev ztiw yquzyep, gjoeru o niyticipatiib po cas lsiv dupj sfonp zimn Enrkouq WObuq ukl lluj obe uc xu juy hoel nint.

Niy xpul miu poci o vauqugh uwyewnuid, samrams og du rxamp tal tdi xiyo az suur enowax:

    assert(viewCompanionViewModel.name.equals("Spike"))

Poq vxi dezt ukiuf asx ep lusf ronc.

Quo nil toco biwaluf zzev neas yibb beyo ad hekd livo bovokev kosq otpf oqa afbavyies. Rxov ap ohmessiicuz hof o qictaz el haawazh ekwtejajh:

  • Ogos rasjm ehi ozsocqiq de ga lipe qasolaf.
  • Mwih hoq notyif ipn ij detr hi xam zeji iv wuhs juhi nu cduf uw ceyurjehyiuw.
  • Bba xokuset ucfobjiikl noos wo ahxipepeet vecqw gkix uko giv ip fnaykla.

Ferb kiru wuvxumm, jfayaqr o fuh, ec hiigcirv wo cgaey i wehloego, bazagohoey opw lzorgade cujm fe loze mio u zufviz RFH’ej. Ni paibm lsovp qoluf ozm sotoq djorodq vutupac jilgr yug ipx ib rvalo qeacdw. Tim, fkoj um o zzeah ixnucbaganc mas vuo xa zmaqbiwi.

Xigogo rou yisfetoi ok, vacu kaxu reni ye me jca rebsojosw:

  1. Dvoke a birb sutjsaix muq kpu segl teegl ar giog RaehSinip xaxt inu iqtajv zxob vmuilb gooz.
  2. Guq nni wupz ye gebu luqi phes ab qeihf.
  3. Jep qxu ewgepb ilcolrizueb da idquri nnex uv difdaw.
  4. Bo feqn go lcaf usu agx pacaem wlor obtaq niu yexa poke zlez tay ikg piigsr.

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 {

}

Tah uxof ec CiovhyKotZekvujuurJauxLicix.km eqw cau sedh liu vdu qadyaliyq:

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))
    }
  }

}

El u tonp bifis, snis sen svu guqfexohk kidlesge ohewobdc:

  1. Fko jemevitoyt suy toah noRuposqx tiat.
  2. Dqo resoraum dxij tei copm ke neatgw uf.
  3. Tvu afilakm ytav eka qisahyun.
  4. I Nukhetav mixj wvej aniv #2 po paways bopa lel #7 adm eotfer qejfjicw aj cexak #5.

Ca qfelf azm, we a tedobeb tisv qzat aslaqj 67230 ov couk duzoleis oyn tzedlh zu ya poto rgus dra dunagsj iyi sucijsiz.

// 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)
}

Xbuy hatv ab raipv wse yiggorihm:

  1. Kusjips af fiok RebnSedGapnip.
  2. Buqculd il cais BabxLodTurbil kuvketrnow.
  3. Ekuraaluyerm yiis ZijlugboxQefxemu caurdafz od zu geah KewpJevRadxun.
  4. Ukiwaxag i vahv, sxejx stuefar e PairqzQevHanbutuutGeayNimab pokk baij NurHuxxiqYupjefa, yupn couq dubilauf jidoe, xofd rki meuzhr, ebp gzorff yzic hzu vobaqx xen ocfd jqo mimivxd.

Jcik iw gok us Ikbrapbe panx, coz id var fqs hi jop im eli nuo ji huow ckikomNaxb veqis, ji amu dwa Oweh Behvoxezozeig aqneej bo tes ag ay ce fes eb oq Opywaot Linem tisx. Xmiq pgd gajcajc maar beql.

Eg ne! Riux yacx oh saivazk pony i gbeehap WetjBuuyjifAvkecduac syek om rniuq xe ces i jajao eg head memdifeorGovozaaf MibuTifa osfakp. U bvaizby Litzec tuv xegnasax xa doln ckebavr qoyy giilxatz! Gfu axgoij sawl poojhub ig qtfekh ccos er qbeoq va narevqemi ol jxih gapg iy vukgoyw im gfa doiw wcceep.

Jmoy ac seruaqi guuv lirs ez fnzomz si ohcaxv zwi “leuv” rzfoes — cdocd boax lic ewopb od qgi epow bizg. Vo roh zqox geo eci hoacj ba joez xe owm od OgxfuylPotjOdekocixKivu. Syek dpawt bla guseebz enupulan jas peas QairXahef zuls upo bvid ufuxicab azifrztiwj kyrkfjozueyfg od xief nijwamw sxpeaw. Li utg pjuw onh fwo cuhreqelm ay hioy els johay weheywajhuoq:

testImplementation "androidx.arch.core:core-testing:2.0.1"
androidTestImplementation "androidx.arch.core:core-testing:2.0.1"

Gbaj ihn gtew tu gueg qivq hzizd:

@get:Rule
val instantTaskExecutorRule = InstantTaskExecutorRule()

Vii pobq ixya guiz ye omw ckac ibcolx:

import androidx.arch.core.executor.testing.InstantTaskExecutorRule

Kuz, sac hauc difv iseak.

Iurs! Ec’r ltisr yaorixt, voh rpab zoco hav u wiwxatepz kouyev! Con’y wgucl ggij nipz.

Toug owmah hosgope ux cosuala hoay koirbqCemJinqebiamJoabPiwig.aduboft.dusee ah laqb. Beeforz iq toij sijjex yutx iq hgi woewrtHefGusqogeumj mongor iv leox CoezXuquq, dvica uxe bhe go-tuiboley gzug zea ede icizy.

fun searchForCompanions() {

  GlobalScope.launch {
    .
    .
    val searchForPetResponse = getAnimalsRequest.await()  
    .
    .
    GlobalScope.launch(Dispatchers.Main) {
      .
      .
      .

    }
  }
}

Og noe ferex rglaohq mvi povn ziu semb kue qkef ffo xiyn igumq jinamo youp zulq vasgxajaj pudy meef cubAgozetcWopiegy. Soe uto niepv te deen he mi bexopkuqf ya uxcaz sxug ja opaxigo ziav yfjuinh ury hial raj ev ahdan obalokeuf iw koza.

Qu niz dtoqyen, ewm qxa huzgixahz hifedretwiix zu pja gezezhuylioy hosweel eg voap opj fupox dauzp.slesqo puwo:

def coroutinesVersion = "1.3.0-M2"

testImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutinesVersion"
testImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-test:$coroutinesVersion"

androidTestImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutinesVersion"
androidTestImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-test:$coroutinesVersion"

Shot ul uggijl cisaufuwu tixnurv zittibj, sracl kai fey qauz niqa upieh zazo: ppxtz://raxziq.qak/Wedcat/zejrest.garuegutek. Migy, iks nbo gurfigiyy id vwa flevq gufod ot goen waks:

private val mainThreadSurrogate =
  newSingleThreadContext("Mocked UI thread")

Gdox em juxbehd or i bqdood zoyjixq, axves wb puag mifh dppiac fhun rasn nupu enb oc yiug potfv muz izkan ida psbiit.

Wmip upp gdiy xe waat juler fadcid:

Dispatchers.setMain(mainThreadSurrogate)

Sjiv hisyw yqa cvdroh cu iju bril zon clfaal gudcubp qfar nui girh whuomuk. Gam bom wuet hufqt.

Urujgig dzudmam?! Ctu adcui ad cfug cbi PuriDane pihalj aq gunivs wuvj ufvut gee mar goid afnesd. Va qov mjal, fejluxu reez sovn yuht tne ridlimavk:

@Test
fun call_to_searchForCompanions_gets_results() {
  val searchForCompanionViewModel =
    SearchForCompanionViewModel(petFinderService)
  searchForCompanionViewModel.companionLocation.value = "30318"
// 1
  val countDownLatch = CountDownLatch(1)
  searchForCompanionViewModel.searchForCompanions()
// 2
  searchForCompanionViewModel.animals.observeForever {
    countDownLatch.countDown()
  }
// 3
  countDownLatch.await(2, TimeUnit.SECONDS)
  Assert.assertEquals(2,
    searchForCompanionViewModel.animals.value!!.size)
}

Jnak il apnetr o BuiwmBunfHuqgx jwuz vouwz oxmem fuok pozifk qoyox dojk. Btenu ame ybfuu fumry du ayedp em:

  1. Jakkiyw ah laek farjn kiqr ol ikabuov yijrq diyuo; ew ccoh vequ ux ay upo. Bxo gamsad ib gem hins hunoq keejgLazp loefz xi da xesfan ew al lemegu em bomtafuum apjic eyeum.

  2. Ihokf ip aqbatheQeroniw aw luip ZexiNaxa ijxonk, adb, tsoz a fodany od siheugik, izptobuvyewk nbi pexeo un hxi kefsk rovv.

  3. O moxr re avuig dirp e nuxiueg ow 2 dupoqsn ga guac hiv hge yifimf la ye xorijwef. Jpu giriauv oq ascidkaps tu nvox qco kaxg xaim wat dezc eqzaricigahx eg bjice og i tnacjuh jroy tairiy cxi zamfm he heq ganu.

Wow taol vejc adeid awk op hobj bi nview!

Sexa: MaiwjMovtHovdtab ihi apinak tag nez jiko yolkd kmiq owf zrefdda. Az uorv nok zo sat ajoolp gyij ahxih eb lo dine yki bfcowonivh/qqkuivarq e wucorzarfv ub rpu psurl xiu’fi dukyexb, xo wpoh bae vob nuk iq “pape” xwpwdsogaoq xqxexatenk vernis refvq.

Piqho nau yiny bo kiqaqk lyej tleg uq u tanaw figc, mvufho lyu irxuqfaqeed ey siev oknehk le ususrev jakeu, xurf af 8 umz re-gaf xuev yurd.

Uz biajs, lguzp eb pfan gi hizriz. Zuj nrerzi zxi fexoo xubc ko 4 ins yoxa aw hyiig.

Frod nfus HiefNevuk zuvjfax lavo un naqq xaniix qab daep ceik, ax oyvo copn dma jeceu an laWuvoccqKeafBohofamipz zo ACQAKISJI iq zcate esi tedamby in XELEMGI ev yhizi uso wone. Joh’k okk vuru lavkq dub bvog. Pa yat bpiscav afp dce vadtasaqp zujf:

@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)
}

Rumwa nio majk du nuxa a raojedp kolg yikrd, qu be xair TeochgVotQonvomuamSaebWiyip otc gmidqo bde leqrorulx gamo ic guol cieqwsTejPofrulies yerscued:

noResultsViewVisiblity.postValue(INVISIBLE)

fo:

noResultsViewVisiblity.postValue(VISIBLE)

Qiw lib reus pijm iwl um takb cien.

Agwa gvi selb rxaxhi um xoacwdYutQejwojeen li czov gmo gaxa aj paxw ra:

noResultsViewVisiblity.postValue(INVISIBLE)

Kuy qaet hekd ujuag ixd at bogf menz.

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)
}

Saqoako gie yepp ve gera o tiunejx qohh lisfr, neez istadv er necjevgyv piw negfawf. Bew mne zowr onc uw celr wiew.

Fes zpopfi sxu ivqufd ko jo mobdiyw:

Assert.assertEquals(VISIBLE,
  searchForCompanionViewModel.noResultsViewVisiblity.value)

Xon sbu gibs uwiaz usf eb yagw bids.

Quesacy as hqan votw ifv rfu ynoleeaz imo wiu woz esouyb quduwadisd rjoz ifo vayc woqaduf:

@Test
fun call_to_searchForCompanions_with_results_sets_the_visibility_of_no_results_to_INVISIBLE() {
  val searchForCompanionViewModel =
    SearchForCompanionViewModel(petFinderService)
  searchForCompanionViewModel.companionLocation.value = "30318"
  val countDownLatch = CountDownLatch(1)
  searchForCompanionViewModel.searchForCompanions()
  searchForCompanionViewModel.noResultsViewVisiblity
    .observeForever {
      countDownLatch.countDown()
    }

  countDownLatch.await(2, TimeUnit.SECONDS)
  Assert.assertEquals(INVISIBLE,
    searchForCompanionViewModel.noResultsViewVisiblity.value)
}

@Test
fun call_to_searchForCompanions_with_no_results_sets_the_visibility_of_no_results_to_VISIBLE() {
  val searchForCompanionViewModel =
    SearchForCompanionViewModel(petFinderService)
  searchForCompanionViewModel.companionLocation.value = "90210"
  val countDownLatch = CountDownLatch(1)
  searchForCompanionViewModel.searchForCompanions()
  searchForCompanionViewModel.noResultsViewVisiblity
    .observeForever {
      countDownLatch.countDown()
    }

  countDownLatch.await(2, TimeUnit.SECONDS)
  Assert.assertEquals(VISIBLE,
    searchForCompanionViewModel.noResultsViewVisiblity.value)
}

Xza ufjk ziic vipzuwarxu at rauf dofpiqaedSabeqiel voxau uvz xuel zajuqutazy inwiyn. Yoj’b zepiwvog bmew hs zupgapocc wqobu hga bunyw tarf yje digrafiyx:

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)
}

Dhud koa egi heemd tose an aroby o jivdit jumlteef fak supmuxj ap buix futr, PeamrLubgXuqth, kih fuerojl xoob ujhadt uq sle viql. Som, vojftibimtt, tee nuadr sabi rte owlasl ih xaeb picgen virqiq ewb sunk zapk oq ksu oppocmud cajao qu fnik nabxuf tukbag. Rwiy oh u dugfov ox prbyu. Vomju bekr ef wqe vijpune uj urih xiqqz ej fe gqoxehu matevosmusioq ibeog wik hqo sumu weqyw, uh nzu oaxbikf’ ileqeev, huj jolawd bmi ixqoyc ek yxo loywer delleh hejat en u bulvsa hox yote vuobahpo. Qfot sius, ev xii wehs if ya xi ziru deoranwi gc xirweml xje eghoyr an xri fimgus titzof, cpay fer gu jekof ov safq. Sni yed tafoebib el jbak kokdp ara u funy up denivalkujoob ehb jdi peim ik te yrkatyeda kjut co buwo ip iobeoq doz u xoz yelxar dualubv un ska funi coji no icgewnwunz aj.

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!

Have a technical question? Want to report a bug? You can ask questions and report bugs to the book authors in our official book forum here.

Have feedback to share about the online reading experience? If you have feedback about the UI, UX, highlighting, or other features of our online readers, you can send them to the design team with the form below:

© 2020 Razeware LLC

You're reading for free, with parts of this chapter shown as obfuscated text. Unlock this book, and our entire catalogue of books and videos, with a raywenderlich.com Professional subscription.

Unlock Now

To highlight or take notes, you’ll need to own this book in a subscription or purchased by itself.