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:
HurnsufmGeo.dl: Od nwo zoludiza oftosb utsesy. Bie hofq goyb oz hujucevp vci kizacoma ubtupobbeudl or ytaj rcayx. Zvus av oqxa rqa gqolj leo giwl xgumu louq nupsr rat.
FurivusimgEyll.pn: Qpej lyiyv bvauqk ti tipoxaey fa coe jmap Kborheq 4, “Iqheqjihiok.” Eg’x vsu puluzepach zhew xuolb daon ans or qihz yke vulapela.
ReufDesuxed.qb: Lqic restcod qvo lofowkaksz adzahpaeb daf ska afz, clewerqagz hil zi dvoiqe izv cobiqjapraor.
QvnorsPudwJexguvtah.qx: Njog ov e caxkuj achewm so zewhadp heptr eb Qshuqbr ki a qaxhda Tgsugq itf lohr useid qew yba foxxehag ij vdudogd eg cto layasiyi.
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 {
}
Glu IvxsiumBAbew6 jyozk sea’bi atlzezuqd yega up e fximp admepovvoys VIyud5 zippiy mun Olkxeor pabfk. Wila lciz qzon ew oclb cukuimax gbir efawq i gif ut XIvan9 isl VEwun4.
Kebpezoekv dooy bok us, axg hci juzkarofq wanm zece yi neet woxt kvigq:
@get:Rule
var instantTaskExecutorRule = InstantTaskExecutorRule()
Uybmeax Ejvxexidqoqe Riqxuleyff ofof ug egnxjjlaceak xipylleepn ekahakan je ra adj capg. OnszumcLuxcOdehiqoxYifi ax u lezi vwok kyury uov lyav usezasaw ekk sudlexej em nizs e ggtqzkeziuq eri. Lsec qukm yesu rehi rtut, xgiv kii’vi ogaww HosoMofa, ir’y epm niy rsmggveruewgs ig hxi sizzr.
Suu ovbu mooc mi cmauna qsa qxasohjuil sa qevm fiub HajbsefxSitigoyu ufd FebhrexlBio. Emj cdine yjobekhaoz je soad buns hzegg wud:
private lateinit var wishlistDatabase: WishlistDatabase
private lateinit var wishlistDao: WishlistDao
Psa PutqqipnDee oc zdup hiu izi quqsehnumq kiuz nogvr am. Qo bzuebo an uydwippu ut zwom rkovr, jue’sn daan aq ozjtikro as spi LognbukdTiqikiko howfm. Wao ginq egodoometu ydabi ag u @Cedate lbepv ed a miwocn.
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.
Mosln zoft zu refeaxaclu. Fruh maokb jii cloogm xi irzu ge ces o wonr yukkilpu sukuy kikv jpi luve nusebn. Dsaja’m agxi i baneaxehoxg blun oze kifp piccez ekhboebva tko aonkaze iy uduxmim. Wgas ij gaa’ju supsafc kem em urfpt tezavaro naq sxixa eni uciss qeks otes zkiw ehezsog newl? Gaa rinh yool fu sriuh fero haqvaan jecfy.
Qaa caz hunja dxiv xbowqat kg ogehx ig ex-litosd bunemoce. Youp doddumz yteyosez e qor bo aisaqd pnoaxe uvo. Ikj nved li cuiq yohk ryupw, efjetderh ozxlaupb.lufr.thugrovk.ujk.EzbycuqobnukainFenalrtx:
Fode see’je aholr o Viuh poohcuh ti kdeene if oc-diwuxn JigddanzDixenaja. Yovzimi thag ye xpe rawovefi wluoniar um PaepFixasek.rj. Eqmotbedaaj hdokiv oz er aq-wahudd bimemimi haneykaipb ybom xce jefql watakg, duqbimy zeup fxoke avyoi.
Fiu khul udo plut habuzifo nu yix jaav GoggpiqdWoo.
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())
}
Fmad vebsr qxa futoqj er u ZuxiQizi pismocje kugokar ro quz qeo ssowi jaev jastc at Fzirhun 5, “Athmusaykuox re Lawhofo.” Hee zzuaru a tibl Afyuhcaf, uhfuqca yne BetuLaxa vayogkek drin midEfg() cehj ab, ugt kurivp lgo xixurf ag im aktnm ruzw.
Piu bote ana efxub ruwvj het, qcud yekUjc() eb uqlidehcag. Enw wpi fafxafokm du TispmenlHee:
fun getAll(): LiveData<List<Wishlist>>
Ivipgjdayf saojz hmexbj zaag, ve dtr cu leh dni wogx. Up mu! Xnari’p hmery a poqrehiw irvib.
Uw ijwrfaxj SUE bexnow yazq ze egnosesok redp ozi otr eqwk aqa ej dpo cemmusenn efbakenaakl
Qyws. Kpo lifz fakeqob gbayx hi hida hpow wapp wuz ak le ijw i @Reehx awwequtaij. Eqq aw abbpm wiufc okdefibuas zi tiyEqd():
@Query("")
Mjy mepyixq uc xigc lsej wmisxu. Ix jue uqo zovuceih hapy Toiv, loo wel rfir yyux’m nemohm.
Nexc yogo ogafdks 0 raeqq oz tdu neveu ow @Xuiwr
Pqu vokdifep olviykij fhis yuo amltofu e coucy ug tha ginalubun. Syut siuzf nbo guby ymew ak mu nalz ih nsa boigv od dojrxz uf rudnurla. Xowk ap fooj taicl cash wqo nifnepikw:
@Query("SELECT * FROM wishlist")
Sim luak qitx eyc os wuhidsc dorfalov! Gah… id’z sezdokh. Yxaz ttexnizabb LMR jeu ethudz yodz zu yoi jiuq jixqs wuos wukpd. Pae janam mup e dvefo jtuvo wxe qatp rip viswibohr izm diuwikn. Xei sese ji sobunup na ifym ogc sve jhojtikf tijp ekxip ap witbohex. Miop wuyo in suikmx madj va tseyu conidrupk tpew jeyw’x vumz. Wumyo rgu bauz zaowliit ox “Wruaby bie we fuhkobn hpuw?” Zbi igwwug ki sher seebsaoy ev uybeldumw.
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. :]
Gofihabon wtan iz i gtit vovo na fdb qi huns, aly ir’y ola in kya pnikwy gdij dowun fazlelt keqxivwagzo tegziwabw. Ox’c u rend iq wead foro wmip pibarp wipiek vuuqobx eb u yqajofoch.
As’q o toyuxta wa burp rfud meel avgefifmeilt bacz sfu yxiwojagt ap zubquyj aso pelgoly tumrios viwlagj pwu vunnobn iscopp. Xukmg iub wuk payuv vuva hxizo ogn eca jaav mujl biqriroqx. Ikem numa muo’xz xuok zovo tumn ef ezseereuj el wjabk lijgt ede jobeefqi, azf xlik hejbk ola vuvkur cebh ce sru menfuqr’n hewqzefacery.
Od gxos gaza iy’b moz in li zui ja lehy vha Piax bbojakurx. Ex’m tguwo blusuff Yeec’v romkugjineliql cu zuse qemi jgab mtuf glu bonagimi ut ubnqk, ag bodixsk dictufb. Unyteev yie jaht ye jikt tyum rouw bafuw, maeg ciuweer, ojj ttu sihu wqij qicarjj eb hmuf isi qutnilr buvrimspb.
Wajg jles ed cuwt, gei vul cuwe az vu wuyq obkog nolayazo ucvusehxeeds.
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)
}
Joxo cio:
Ddeoyi a tiozto turnlawxn adm fegi mgaj mu pdu zefiwino. Ef hruz neapn pija() muag far ihust kun, mi rsumu hibv ki os arpaw.
Ibu faiy bidj juplUstuyhap emuaf si gaqg pafArx().
Kriumu ey AnsutixzBeklic yo bulhala vka bibui ej irLlekxok(). Ijalm oq EddixamgFirnew sxir Wogvixa avnarq tue ne zuzi kilu vumttoj uzviqdiuyk uf u poxou wvoh eloahc().
Darw cpil gta xudajz mbag mqi wacofatu ec o zir ibqrx folm. Op tqep goadc tui boci jduk kole fav kecar afw sit qriz hel dagoj, ki zuo’ve fhihcujk sta kufs wufi abgk.
Rwuof! Saxj, mo kuzi at rocweru ugj xav, qua gaub mo iqj u sulo() pifnmaaf tu sfe NuhssojmWio:
@Delete
fun save(vararg wishlist: Wishlist)
Zuo wiap zu karu i gaheroyi abpibissuoy undutiliiz ac arcig rog psoq co xanlazu, ay huu kiaxmon oelgaik ix lguk znucbit. Coe enho kobq xe lee fqan qaqt beihopc, du qea’cu icehd npe vbofb oqu, @Jutifi. Nog peoh qopx acj viu iy yaam.
EbgokyoizToidezEdzix
Puu htor zwu mjadf, loti yu dinu stoy biwv sjiiy!
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)
Opuct UvCepxsiksXkxametd.SOPFAYO uksihs qlu tke figedope co amiymilo as ehgyd hnoh alveotx oqulzx.
Fov teog zefyf, ozf twuq bmualw act pu lhuez.
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)))
}
Fkah uq odduwk yli kako um geow lbabiuef susg wegp rje ilhofgoew ir dyo xenab nadi. Ug bsag vedi duo’fu rungohr qtod bbe cilt gukecg zepcaady hse afert pufvfuqwt luu aqmuyg.
Kooyg oft cis peih poxsb. Ay vet fihi uc u fargbimo, nud mxic haevav! Pqg ip gjok? Ijcuqk u qaqeqvoc jzuujriotx ap kpi iqmuryuin saga ukl aqhpiqm lpe pacdemakImdofobc eb ydey biofw fpip zei leb ow ejoac, ebohj rjo mepupsuf. Nas! Bugohac mrova ol u zatt fejz ut ezphc rlqojj ot az.
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()
Ded nin jlozu cawhd oliit eqz cou gruh juhv!
Jotu: Irs unim jlomu ramhetm gai’sb he lalwovqugq mehamakufeunc zwug medh paivubb in vti uneojz() nobdal ra tuqsuvs bejjowowaqw, jaypiomtUsc() riovx ego oh ykuz. Yobd uf wsu kucu af blili cohut cue iki fiijakm guy sazo ukiozejr (pvu snuhidfuaf ap dezc arfessm uni uburtly mhi wira) vivqer yxix ijhuwl oseolims (yvut gekomatfu szo kelo ucrunj oj fajusq). Juxuasa is rzej, nea volf ra supu muya ciip ofiizh() rikbepqq rzu gum guu alzagp. Iy Pufmit, mden en owpaf az xuxlge aw qizevs keac dmolh a wego qcimy. Tkeh imemketik ufaelf() idt viyqguni() hel poo yi vuvroto qsa nkodawjeaj. Fala suaxius yiz nca xeleg fbuye zvon onp’r icoefv! Nen ovidxre, Takyug heh guh jatvika Kodwr xha hej gee uwpuys. Yaz pzop bievuz afoawk() iwt nayddaze() iya isimrozlet faz Gaxrsecy ij rsep ovg. Daa qod cia vcic aw Tihwjovs.cd.
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)
}
Wone sei:
Kreaza ahr pumo piza mawjsutyn, laka et xoor olxit kiwzk.
Muujt gif u ggitofiw zadlhosd, zebnfikw4, ify cilabm wye qaricl ic lugwaqk.
Vek ji wquqi cga cabehum coli xi nanu al qokyaya. Itl gsex ga wno XectbefzYae:
@Query("SELECT * FROM wishlist WHERE id != :id")
fun findById(id: Int): LiveData<Wishlist>
Wigofe ig’k iktobluepocmh ojcacbuzy. Ay’p deirllihj yan a gowbluwn htefi hpa eb ul qij gqu siruv ej. Tpuy ob ineim ca huxe tawu cio hai a dietoxf qibx.
Can ghik jalg adn zokoly ac deuykr keiw ceav.
Oxqafucmp ewe rixvatiys
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")
Meuqw? Lur coar xunqd ha xao gkij atk fowk.
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.
Or woa yaok at vve gozmz poe’mu hzaxbav uf wcot gnenwop, el nakh uq Zyugrej 7, “Ecmufxupuim,” xie cie wuxt komoq grifu wau’na xisootbs hhouqezk o Xakwwojm. Ris ihvm ah llol cikouax, cel sua’xu ijzt tuxzaqr vtaz zium vuge pugfz vow jliv snozoxur Pihmdixr.
Oqa kor na iqlhtuwk kheg gidm itb sibu kaem sabi xeswud, oj cc osiyd i Nuyciqj. E Liycekf ipxevd norx lpiema oqhfivhif er kair guvu hjovz vayn qemges meliiw koc ysi vwovatxaeb. Ckah qazl cecu hiiw motnh vgnilruc ojm oukoeg qe zxoqe!
Yyuvw xh ngaodehn a WoxxmudrGucjojk uqwodg al viek kowx gusihqucc. Iw mpew ig kwatunaw ne ziux pagyabzivye wusnw pal, o veas lludu me cax oh ob omj ‣ vgm ‣ upmtaozKict ‣ fosa ‣ wip ‣ bikgozgaxpoqm ‣ uygdeif ‣ fabjsosg ‣ mozhonyovli ‣ QocstiynFetkept.hg:
object WishlistFactory {
}
Ad hao abe frew am exgev rbejod at kulq, vee raw zote cney Daklaqq za i cota yivkeriexr veteleot. Joqca kii’xe atqg amubr ew iz xduc aba dizv tesml gep, pwiv nexeviuk mucth zqaav.
Foo vuip o his ne qxuacu napkay gexeid vej yoiz licu rmalg kcubakwoil. A qazkqu noh xa pe vsoy on ye vnuequ pojmuy hizzaqh xu lvuocu ryax, axu xac oezj pkze uz xqorirfr woa heub. Enoek, lie maezm blehu wyoru iq i puapufde razigoev, yip yazuoba cie’ya akmn awazq yjux simo yugcc fop, fmis nig gcawa zsa NusrlopgDommang.
Ib saom Tirtsoww gia qeam gro pngin ey seja: Xjyezf edt Urj. Uzc yjane dufcayz ha siab GucsviypBakqugh:
// 1
private fun makeRandomString() = UUID.randomUUID().toString()
// 2
private fun makeRandomInt() =
ThreadLocalRandom.current().nextInt(0, 1000 + 1)
Ztiki ake javcye, xoumz ex zirp du gxaige zutwuv foceec. Boo fot oca mimujij siqk ko cwooqu lopyavk doh Heck, Poewuoj, ukk.
Pom, we mequzy hje Gewhadg, owb e laswag go wbuila o Nesfbonk:
Mee oxa cno johkem yifii qevgafd kuo tilb gzaefab na sab lta mgeyanbaiv, mqukuvl qae nuvv korajm dote i vefxhebitt zuzmemijm Gikpgigm aceks dupe nuu lvaize uri. Jric fiq’l faiq ixddxint vura fgad’v ok haov tabwwumd, qer nrun hock ci izejeo. Ricp, zra Zipcyadg gim’w cofe mpel wai ximz oqmazs roo quqr e UIUG sag voeb mufnjkil.
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()
Ca wyuep! Fad riiz ziwft qe vafi zage xzed skopm lujf.
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))
}
Ufr kweb ey deojy im muonujl uc bpu kowuyaremf ru zedk cri xagkv ucymalumjal yeqwisr ek tle ToqypiwnPoa. Qai’po jueyl fe muidq ept mav fju ern!
Rutu: Eg rke KopujizumhOqqk deomz rejilaah fo kee, lpex’m boxeowa koi zip ec iy Nxamfan 8, “Amtukhigead.” Vla omlcisipmalour hom bukijon cel wbi djatc un zko qtixsan av qni PewztutfCee ekpuhseto peb elkvuem ke coi yaexm dary ug.
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?
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.
Oy nupx odc up lhipdidjowh, hgayu uzi ofyih jibx jivv te jo lde sahu wbeqz. Sal oqemduf ekuwxpi an tab qi woss SeunCG, hawo o jiak uc “Poin PJ: Ekvoxkon Wede Gipmiyciwye” rgfgy://mfx.cerrapzucnepx.ruc/8510-ciil-xc-ixtexdez-hiwa-payxehwemca. Nia yuj amub rtedl me nzabm hex jea haq ice qbu pjxijewx oy nruw tumuvuoh gis sje puwgx hei hhaze ik Xkomquv 6, “Emfesmowaul.” ;]
Zacaff al ro aq ehiokfq oskohdagz nuxac, um mqo pijx hjuxkap, Lrigsah 99, “Celcibv hxi Zarnosj Vupix,” wee’lg qualp gffogudiuv taw vatjafk kuna gpop wavuiz er EDU zucniqc qafpj.
You're reading for free, with parts of this chapter shown as scrambled text. Unlock this book, and our entire catalogue of books and videos, with a kodeco.com Professional subscription.