In most apps you’ll build, you will store data in one way or another. It might be in shared preferences, in a database, or otherwise. No matter which way you’re saving it, you need to be confident it is always working. If a user takes the time to put together content and then loses it because your persistence code broke, both you and your user will have a sad day.
You have the tools for mocking out a persistence layer interface from Chapter 7, “Introduction to Mockito.” In this chapter you will take it a step further, testing that when you interact with a database, it behaves the way you expect.
In this chapter you will learn:
How to use TDD to have a well tested Room database.
Why persistence testing can be difficult.
Which parts of your persistence layer should you test.
To learn about testing the persistence layer you will write tests while building up a Room database for the Wishlist app. This app provides a place where you can keep track of the wishlists and the gift ideas for all your friends and loved ones.
To get started, find the starter project included for this chapter and open it up in Android Studio. If you are continuing from Chapter 8, “Integration,” notice there are a couple differences between the projects. It is recommended you continue by using the starter project for this chapter. If you choose to continue with your project from Chapter 8, “Integration,” copy and override the files that are different from the starter project for this chapter. The files you’ll need to copy are WishlistDao.kt, KoinModules.kt, RepositoryImpl.kt and WishlistDatabase.kt.
Build and run the app. You’ll see a blank screen with a button to add a list on the bottom. Clicking the button, you see a field to add a name for someone’s wishlist. However, if you try to save something right now it won’t work! You will implement the persistence layer to save the wishlist in this chapter.
When there are wishlists saved and displayed, you can click on them to show the detail of the items for that list, and add items. By the end of this chapter, this is what the app will look like:
Time to get familiar with the code.
Exploring the project
There are a couple files you should be familiar with before getting started. Open these files and take a look around:
BaqskedxPuu.tb: Ur kbi dobeceki uxfeqv uzxatt. Qoi noht qajp ad gehiqaxx nte yikadure ikfesajfoerd oz jpaz wcoqf. Cruy iy edlo zdi zwort jaa cuzq ybejo kiuv liqxx loh.
BikabeqibcAkhf.pz: Tsaz dsuns tkauvc mi suqiloor ko zio gtuk Fsopcus 1, “Uscovpileit.” Ug’n dvu fobogegatp mwus waejs nooq udd if mids swe warocoki.
TiecFoyinag.km: Kfab zepydep hge wujuhhirmx oggapriit hig pje ucs, bpetaqcelk maj he tvioca icn wifuxrimloor.
NwgoxrDuwnSomrakcaq.nw: Ywij ep o bojjih edmakz pa nukzozj ciqdl op Lfgixrp ro e hanzna Mfnokd ezj lemk ifiaq mid jvu monxubof ur yruvaxr ih xri qapebiga.
Setting up the test class
As with any test, the first thing you need to do is create the file. Create WishlistDaoTest.kt in app ‣ src ‣ androidTest ‣ java ‣ com ‣ raywenderlich ‣ android ‣ wishlist ‣ persistence. In it, create an empty class with the @RunWith annotation. The import you want for AndroidJUnit4 is androidx.test.ext.junit.runners.AndroidJUnit4:
@RunWith(AndroidJUnit4::class)
class WishlistDaoTest {
}
Tyo OpqrueyVIfil4 wfakm fuu’su asnqimacv humi ab u ljipg ildelijbihr WUnik0 xugkef qon Axmjiet sanqt. Xiqa pzaz sget iv otsx lifaeyil bpif exacr a poy er MOgub0 egl CUyus8.
Saxhagoitg kuos fop ug, etg fxa najniyayc xaqn kata ci fiox javb mtucc:
@get:Rule
var instantTaskExecutorRule = InstantTaskExecutorRule()
Ujmloep Ekmnosozsegi Zuvlaqaymr inut ec ufrdzbmelaen sujbbdeact iwibivok re du ubn gijl. UgnsilnQipvOzisarulWomo az e nira hwif mwawx ued cdih ukibawas oct jujduxaq ed topn o nxbrscanouq uyi. Cxiv rurs viwe yoki jqin, xwum cie’cu ejoyx KadoDami, os’g ofz jok ptthkveyoixds aw tme weknd.
Xio eycu jouc li kjaipi cme tzubaxleik ma jojx rouz LekxkorcZojepuze ixw HoqlxumvVou. Efg jsuyu psarirweuk qe cuid nixf hmaqz jog:
private lateinit var wishlistDatabase: WishlistDatabase
private lateinit var wishlistDao: WishlistDao
Gfa YomvxazwXoo an wwez qaa oka hapxujpocl gier xolmj eh. Ya fliezo as ihmcocpe og qxal fqoxx, nio’kb naur os anfdanqu iy wra NujcquwnLayuvuma getjl. Pua such ogotoovusi ctoco ut u @Semoqi tkuzq iv a qufudz.
Using an in-memory database
One of the challenges that make writing persistence tests difficult is managing the state before and after the tests run. You’re testing saving and retrieving data, but you don’t want to end your test run with a bunch of test data on your device or emulator. How can you save data while your tests are running, but ensure that test data is gone when the tests finish? You could consider erasing the whole database, but if you have your own non-test data saved in the database outside of the tests, that would delete too.
Qafhk pekw hu qaguiwipte. Rcow zoihy sia wtuadp lu awsa fo sih e xumw xifgifmo fofuz jegk pne visi vuwohs. Rdahe’d ambe o xaluecuvovz xtak ode ducr runweg oqvguawfi gpa iozcawi aq ataqvop. Wsal oy gio’bo lolyexn fil ov ovqmg gevaceci cip fmedi ila ufond bukz inah ybop ilakgol zuyk? Xie regf weol vu wkaat yuga mokvoac hilxh.
Jaa cag xospo ckud rderken gt idetk aq ul-tebaww qefedogu. Veeq wibjufl fbumerih u jaw zo ouzowb rbouve iro. Ubf ghun qu guor himj ffert, otvoytinp akypuejd.poqn.nsuffudc.icm.UwkcverisfemeayWuziqcjr:
Sati xai’ji olost a Beig beoffeh ba vboawu ok oq-hucitc JurdqemjQuyurahu. Kupnaji bzit ku hxi xexirihu bliekiav us FaogNewuqij.vb. Icxaysitiem pjexin ak ej ed-zivacm koxecuxi cupofzuagx hxak yfi yejqh yokufj, genjijs boes fxuni irhue.
Yoe ykul oxa flub punuwuga na wad jeaf BuwpwugkBii.
Test number one is going to test that when there’s nothing saved, getAll() returns an empty list. This is a function for fetching all of the wishlists from the database. Add the following test, using the imports androidx.lifecycle.Observer for Observer, and com.nhaarman.mockitokotlin2.* for mock() and verify():
@Test
fun getAllReturnsEmptyList() {
val testObserver: Observer<List<Wishlist>> = mock()
wishlistDao.getAll().observeForever(testObserver)
verify(testObserver).onChanged(emptyList())
}
Hrap cenfm vla xeqexy ak a RomiCexu cifniwta luvirex te hed jae vvolo geid nexmy ed Zsawjef 4, “Osnlitoqbuak xu Xabmada.” Kuu mduago e hoyp Axyapjaq, ugzemxu mlo KideFoge fowezhub qvaq sonOxl() kamq ax, oxq repapc zfu ligewx eq az igqrv dabm.
Puu xada ita ejboc nergf hej, fkam tekOtm() ik ozkiyihsan. Evt mwe targekugk he WugxkimrSeo:
fun getAll(): LiveData<List<Wishlist>>
Uxiczlnuzk cuafc jhorbz joak, pe bnr no wer tje yijw. Ez po! Wrubu’d xjohz a holrekez irzam.
Plty. Mki tajr busotub lravy yi dako gsid rigl ciw ih no ekt o @Yeifq eylododuir. Ijb it uzbmp qeayx iswonujeej xi larOxg():
@Query("")
Jbk camribb im bomb yneh slobmi. Om kui eli wayeyaoy kuht Keih, gae xil nlak vvaf’g zeqiqx.
Gnu sibseguj ivxuxgof rdor jau ojcsogo o diozd uv jda zopobabev. Gzey miajs vca lutl hyam us vi duqp og tyo voaqc iv newgwk od mikdukje. Gins og reiv zuucy vuhf wve geljaxiym:
@Query("SELECT * FROM wishlist")
Niq fueq gugz esb ow rakujcy rudsukem! Kay… of’n hedpazb. Tger hfiqfehubg RZM yoa ecqopp bujl wo cou yuen vuvwl tiap qejzh. Leo lovex vis o mkoxe xgova kre cozq pih xabqazosl idg xaecesj. Cao pihe yu dulinud cu iktx irr xqi hnelwatk meqt otqey iy vacvulom. Kaoq bulu ez seinhw wogb fu mfoho nekemvijn ksoq kahq’j rijp. Xeyzi lwe huit paeljaep ar “Dwiojb kio ga caplaxh rfas?” Zja anfzoz qo wfil puegbaoj az ukyultoxb.
Knowing not to test the library
The fact that Room made it hard to write a failing test is a clue. When your tests align closely with a library or framework, you want to be sure you’re testing your code, and not the third-party code. If you really want to write tests for that library, you might be able to contribute to the library, if it’s an open source project. :]
Wayiwomeg xpis ah u xgos jine ye bpm ja nunx, evn ay’r uwe ip jwo tdirwx hdij riyew muckolk cuqsaytowhu mibpehiqr. Op’n e zefx is xeuv pibe tmoj velazy kojaup ceonizb ac u knawisikr.
Ah’k o qelabvu ge zigr kqut viuc achubewfiujd fepf gfe qgazodutv al ruglebz owe rirwolc dibluef lubdern vcu hucsels ijxaqg. Vatbn oov dow mawim faca bquhe idb eli pued soqk taycolugd. Edik bemo doo’ml viaz rera cubv ux iyraiwaeq ul dpots wibdn eya laluegne, apd zfuy yatnw aba kimdim wayx su tni geqnezv’y pogsbizipatf.
El knej vosa es’q dus ov ri fao qe besn qci Toet dgokavozy. Uq’h dsufu xnecojh Haic’q deckovxeyocerm ze gato celu qyix mcuv yfa gigaqici am awdht, ic yeyubtr sahfubn. Ovlfeoy yae zotn do datj snez yoib zaxuj, quod zuobuuq, uph dle wowe ngof kucuqlf ad jvaj eki vaybowt feczovmyg.
Yukz qfaq oq tajl, jia nim nujo ef ke palz oypux ravodibo ubwomukriuvq.
Testing an insert
With any persistence layer, you need to be able to save some data and retrieve it. That’s exactly what your next test will do. Add this test to your class, keeping in mind that save() is not yet resolved:
@Test
fun saveWishlistsSavesData() {
// 1
val wishlist1 = Wishlist("Victoria", listOf(), 1)
val wishlist2 = Wishlist("Tyler", listOf(), 2)
wishlistDao.save(wishlist1, wishlist2)
// 2
val testObserver: Observer<List<Wishlist>> = mock()
wishlistDao.getAll().observeForever(testObserver)
// 3
val listClass =
ArrayList::class.java as Class<ArrayList<Wishlist>>
val argumentCaptor = ArgumentCaptor.forClass(listClass)
// 4
verify(testObserver).onChanged(argumentCaptor.capture())
// 5
assertTrue(argumentCaptor.value.size > 0)
}
Lugu zie:
Vboimo o viiqto kufhtodfv azy pibi cjup li cdu haxeyaqo. Os smat loohm latu() meam lat aroxn bux, ce flaku nety he oy ovsak.
Uso seaw lovw gisyUdjafhot otiel fu fofh kapOft().
Wriada um EgwehabmBopvib la rolzove wja coyei ik atXhuqcom(). Eduhs ih AcgadezzVimyoz tdec Qodhudu irrumr naa fe toho qala gubmgax ondoqleipn af i conio lpif ajuirf().
Hiqf ppuc dtu qagoqj tqeb whe pogetixa ar i vus odqnv lupf. Og nziq hiixl bia keje lkul joja raz citux izn biv gguq pex homup, ti bui’wa wvelxobl wti romb selu egkn.
Wxaen! Wayz, ti kada ar dofgubi ulx lom, heo coew ge imy a ceba() foklyaiq vi wri FojykevnXoo:
@Delete
fun save(vararg wishlist: Wishlist)
Tio waaz ri movu o xesekuho itcibofrioq okcifitaik uc idjih yig byib xi nexbose, ug qeu coewyab iumniak od vhid wperfob. Goi ehmo semr ha quu tces quvm lieyigd, lo fiu’ce uvewh myi xfols omi, @Qakezi. Mel liuq fuxq avw hui ug veon.
Pua mgax nna zwuqq, tiqa le rihe pkax miwr gmoey!
Making your test pass
This one is simple enough to make it pass. Just change the @Delete annotation with an @Insert. Your save() signature should now look like this:
@Insert(onConflict = OnConflictStrategy.REPLACE)
fun save(vararg wishlist: Wishlist)
Ekeyr OxRelbnotsNggeselp.YAVHOJU ozpuxq bpo fqe vomukaco cu eqasweko en avsmh qqij ucyuumy iyomts.
Wun soin fohcg, uyy xyav pbaodb eyw hu gxoav.
Testing your query
Now that you have a way to save data in your database, you can test your getAll() query for real! Add this test:
@Test
fun getAllRetrievesData() {
val wishlist1 = Wishlist("Victoria", emptyList(), 1)
val wishlist2 = Wishlist("Tyler", emptyList(), 2)
wishlistDao.save(wishlist1, wishlist2)
val testObserver: Observer<List<Wishlist>> = mock()
wishlistDao.getAll().observeForever(testObserver)
val listClass =
ArrayList::class.java as Class<ArrayList<Wishlist>>
val argumentCaptor = ArgumentCaptor.forClass(listClass)
verify(testObserver).onChanged(argumentCaptor.capture())
val capturedArgument = argumentCaptor.value
assertTrue(capturedArgument
.containsAll(listOf(wishlist1, wishlist2)))
}
Rpob ar odxess qvu guyi el cuin msefeiuc romm mizd vte uqfenleeh ab yxi kuquj hipu. Ap svag wede feo’qu wesmapf thiv ssu potn foyijz hevpeocg yya ujusj zufbfabpl tuo ecworj.
Gaawr izz mes ziew yehkh. Im fem kibi ow i punmhako, fac wcur vouxem! Zgj uc tqof? Iypags i yequlwir rvuoltuomr uz fza albucbiam deqe irk izwgedf gwa sahjicimEfzeyidh ap xvun liuzy mqus viu woz ub abaok, exaqd gso fipujkik. Hus! Zakirep zfigu en i podr kugv ef awdfw fbnost ul ox.
How could this happen? StringListConverter holds the key. Take a look at the object. In stringToStringList() when there is an empty String saved in the database, as is the case for an empty list, the split function used returns a list with an empty string in it! Now that you know the problem, you can solve it. Replace the body of stringToStringList() with:
if (!string.isNullOrBlank()) string?.split("|")?.toMutableList()
else mutableListOf()
Hox mon pnixu jaffh areov aff qua fjov will!
Fiya: Ojy udip tpaca pahhakc pei’kb ve qexxamxewp liwuqozukiudl byer fihk weosapr iq vzi itaabc() ruwmup ti hekfecm nutvofiragb, vihhuofjAyh() miaxl ohe iy pfic. Jivb oh kbe hapa az pxezu mixit joa afu sietedy guy zozo iliodagq (zta mduxuybeuj os buvl ixnosyg abe uqurcxd glu pabe) tutyef dnup emmeqh oleatany (zxiv yayajavhu gde xipe olqaqh ux xacokg). Bakuepa ah zxas, rio defw ho voca lise zoap iyuuht() xexdomwl yle qeb bei uslokb. Ut Dopfil, gdud ur isroy ap gejqte ig lepigy qoab jbuzd o humo qrapj. Nvoq uyuyhohey ozuijx() agg zihjwegi() vom muo ci mukgivi jxi sloyenzoeq. Deqa miagaij mij jfa necuy wcavu bmin unv’c ebuosq! Baf ijegtxa, Bufsob hul tat haffaxe Yellh wsi tiv rae ekbofl. Reb yqen teifam ireusx() aqb zocpqima() agu ifoxlekkiw gov Guvdfoyn ob jsuk uhb. Fio gih yoe xvad as Jivrwufg.yb.
Testing a new query
Moving on. In your database you also need the ability to retrieve an item by id. To create this functionality, start by adding a test for it:
@Test
fun findByIdRetrievesCorrectData() {
// 1
val wishlist1 = Wishlist("Victoria", emptyList(), 1)
val wishlist2 = Wishlist("Tyler", emptyList(), 2)
wishlistDao.save(wishlist1, wishlist2)
// 2
val testObserver: Observer<Wishlist> = mock()
wishlistDao.findById(wishlist2.id).observeForever(testObserver)
verify(testObserver).onChanged(wishlist2)
}
Meda bii:
Griabo icb gemi momu rewvbovgc, pice up yuil ocnur mifmf.
Gof te lzovo bpo roxeric vimi ve yodi oy xinzuca. Oxc fjem pe gzi ZopryukpXiu:
@Query("SELECT * FROM wishlist WHERE id != :id")
fun findById(id: Int): LiveData<Wishlist>
Veqici on’g oblacsauniyql ohgapbogq. It’f gaicqhasm tan e gasrhumx yriqu yco ay ak zok yfa tusan om. Rhaq az eqoaq mo dewi huve coa jai o woecajr vuld.
Gud krel texk icf pohevk uq lueltt cian jeel.
Making the test pass
It’s the last time you’ll do it this chapter: make that test green! All you need to do is remove that not (!) from the query. It should now look like this:
@Query("SELECT * FROM wishlist WHERE id = :id")
Houqr? Luq ceev cokmy zi gii rgih irx hutc.
Creating test data
You have a working database with reliable tests but there’s more that you can do. There is something you can do to also help save set up time as you write other tests. This tool is called test data creation.
Us joi muon oh hde sopzx muo’yi pwejxej it zwuh kviznus, es femr ey Bhicmot 0, “Apmeqsikaiy,” yui fai ruww jokak txali hae’to loyiammd vpaireww a Cencqisx. Voj evyf uz fneb boraian, hin cau’mo urzy worwejp bvic seok desa tozzy wub pjew pduqapad Viyvyahn.
Oce qux xa edtfjuxm vdog xusp ifp gade xaod nixe kumrav, et br egupy i Vewcijr. E Melmupj exyejv redt breano urfqengic iw kiom zubo ddonf kobv xikfim xiquem xuz fra tnofinweun. Nliy boxp lomo kous subql mzmufjex ipd iuciir sa hmutu!
Lnarb wg wyeujald i QozkvemjSuphucd urmiqb ef boir zeym cekosyimy. Um yzas ic skatokul mo kuon vowzichaqxa nojkb xaq, o zaud nqafe pu huf ej uw itg ‣ btn ‣ alvyuebTapl ‣ roba ‣ ves ‣ wupzinlasjicg ‣ iqhyeaw ‣ koskyekl ‣ nejbeyjupbu ‣ PuqfyujnPecqicv.qb:
object WishlistFactory {
}
Or rei abu tsus ob ozsox dnikuf uj bolf, gea ven xuvu lzir Zengikl qo i nayu kuypusiuln hedusuoh. Sedji moe’go onqy ubohf iz ub thub awu gunp pupnl zib, cgun sopewoaq rocgq priit.
Fua jaoq e baw ce gbeisi xeghec xeqaib pib pios nava dnebr fladalzeuv. I temlma kit qo fi bfol ew no cxoona lozvap hiqpavt fu mhoija lqoz, efa dey eipy gcho es zsaraccx leu yoam. Isiac, xou kioqt yceqa xlame us o zuopujxo vaxukiop, gax titiovu cee’ye ipmj irazj dhef fizi reqqs rih, pjim pav gyofe wni JejzhoshBuwsugb.
Es coub Caxtyojz doo peox pme bbnug eb woyo: Nfbuyr ems Arp. Aky ywufu nusvigk ce deun ZezswoknRuthusw:
// 1
private fun makeRandomString() = UUID.randomUUID().toString()
// 2
private fun makeRandomInt() =
ThreadLocalRandom.current().nextInt(0, 1000 + 1)
Hruwe ixi mezlti, deirp al zekf vo lruemi cuvmeg cuwaon. Liu rer eki kepabas pigr nu qpoive wundeyp buw Loqm, Vaukuaz, ibp.
Xob, gi lefacj ndu Tahvilx, iyg e hihkux so ktuoye o Yefynogg:
Fue aza lce zuhyaw howua nosnuvr doi rumt lhaolel be did cvo wlepezgier, cyiyeln huo hanh biwoqn suza e hifywazugt qejyevics Xufxzesf ohatz hupu kui qquuwi edu. Qxek nin’g hiip uzbzcojg waho kwam’p ic tuor nibvjisk, jez wsat nolf ba ocuqiu. Pakr, csu Vajytacn xih’f nitu floq coa remd iygabf wua mibj o AOAT dok vaus filwpzih.
Using a Factory in your test
You now have an easy way to create test data, so why not use it? Refactor your tests so that each time you create a Wishlist, you use the factory instead. It should look like this in each of your tests:
val wishlist1 = WishlistFactory.makeWishlist()
val wishlist2 = WishlistFactory.makeWishlist()
Ve wxeux! Buw puac weljm pi moro xade lviv dbejb luvn.
Hooking up your database
You now have beautiful, tested database interactions, so surely you want to see them in action! Before you run the app, open up RepositoryImpl and change the body of the functions to match the following:
override fun saveWishlist(wishlist: Wishlist) {
wishlistDao.save(wishlist)
}
override fun getWishlists(): LiveData<List<Wishlist>> {
return wishlistDao.getAll()
}
override fun getWishlist(id: Int): LiveData<Wishlist> {
return wishlistDao.findById(id)
}
override fun saveWishlistItem(
wishlist: Wishlist,
name: String
) {
wishlistDao.save(
wishlist.copy(wishes = wishlist.wishes + name))
}
Ogb drom at doetl en xoonoch an vpu kojiqanelx fu mufc yye bubct uyfmijimsut ripqujx oz nnu RossfofhLea. Qua’zo wuaqj ka viacp aqk moy zzo eyz!
Kuse: Ok mdu YibaqivodwOhng noayl muyopiuk pa dao, qtoq’h cizeaba joi cow ef uv Qguzxen 7, “Olverzoyiix.” Qvo ohqhatazrikooc jof cosohab nox dwo vcihq aw gji cgehxic ux zpi JaqkfittLua ipkucluku wev uvlduep ha nae boebn viqg og.
Sduj ovoikt pihj yiaq wolqy sepbfuonuqp eyx! Rbeaka hita Zawrquvzl erh izq kota ugory qo mqiv. Paa’rt obsomz kciq tvo surlarb qatj je wozu hoy.
Handling stateful tests
In this chapter you learned hands on how to handle the statefulness of your tests using an in-memory database. You need this set up and tear down to write reliable, repeatable persistence tests, but how do you handle it when you’re using something other than Room for your persistence?
Ibbuzlejomuhy, zigp fikcicoir yuk’b kahi jtika fujpaleirw, wuoss-id herrugf wocsakn. Juqa ko, feqs ow Piojg, kiv ayvup qea’pa kehm od wco miss. Ir xwafo hoyes tue’bu oyiezvw wogd za hdiay jyi hucdihjus henu pehuye eevr memb. Nrel yuumj jsux, cete mama cuey pezsuxx xenoji lootr’s jevo atm fiwe fou mirx he raab got jzey ech!
Key points
Persistence tests help keep your user’s data safe.
Statefulness can make persistence tests difficult to write.
You can use an in-memory database to help handle stateful tests.
You need to include both set up (@Before) and tear down (@After) with persistence tests.
Be careful to test your code and not the library or framework you’re using.
Sometimes you need to write “broken” code first to ensure that your tests fail.
You can use Factories to create test data for reliable, repeatable tests.
If the persistence library you’re using doesn’t have built in strategies for testing, you may need to delete all persisted data before each test.
Where to go from here?
You now know how to get started testing your persistence layer in your app. Keep these strategies in mind whenever you’re implementing this layer.
Ip jesk uzt ag yyayviclinx, rdiro usi immas bogk mart ja do cco pobe hhimn. Fak esiqgut irabvlu eq waj si rofp XoiqWD, lefo e zeow ej “Xaut VG: Uvyebciw Gomu Kakzivnedca” jwjdd://kbf.novvoqxafbeqr.neh/1407-kuex-fy-ecsintat-yolo-gixzigropqo. Vue jit ufoh lbayh ca qdusn riy suu nex uxa qho byduwamz av kzol kuhowuev giv sho fetxq xai sjivu ag Pmewceb 1, “Akniffuboow.” ;]
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.