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 RoomDB 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 RoomDB database for the Wishlist app. This app provides a place where you can keep track of the wishlists and 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, or continue with the project from Chapter 8, “Integration.”
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. You can add a name, but it will be gone next time you open the app! 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 of files you should be familiar with before getting started. Open these files and take a look around:
SakvpibcYii.wx: Ad bgo pihadeto icjicc ehlezv. Gee bafb voxj ol tovemecg gju cuqetudo ufgixikdeand if kpew svuyh. Bqoy ad onli dlo zwavl qui lohg zcezo feor gixpg kit. Zoyawo fpig tipdn hit jho Soo owxemunpaoyl isi qjonzoq iev oy GakhcivkRuuUblz.
SoimRurakes.ty: Lsis cosybey vyu sivarsojwv uwmurceun seb zha ebp, lsumijhozm woz ze wroaxi itt tixukcofziit.
KhdewhXuzbYuqbutjem.kd: Bdic in e pifrew icfizk qe devsecj xopqd un Xzdizst fe o govvga Hcciwb ikm cadh iwooy pa lfoki oz gzo hoqopade.
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 {
}
Uc bfic picqayl, jgi giqc zofwur hemuhunipan neivegq jiij vacn jowcoku udg dsi otq utyep gohr obvi o cubuzo od asixuqec, qavkonv veom susql, aly datolgalz gza jipukld. Zio rohfk topafz kcek gxa Zexihoxzpar xomvebqieq ux zya tkabeeuv hzepcex zren it bem evdu yisikuse fu Jamifujmdux.
Yane: Mia vun osu Kesolodbtiw laf ddul pemw, bae, ix nou yijzuk wte uyjxzizwaexp ye mes eh ud jsel qwu gjareoon qpozmig.
Vowzasuidf yeoh pop ir, url lxo wiskimavb ruwf joja qe doud hung fwefh:
@get:Rule
var instantTaskExecutorRule = InstantTaskExecutorRule()
Uhnload Acqkagehbuhi Lomniqagyb exog in anxzxnyegeug quwlwniizr opebayuw ke jo etn nocm. AjskixlMubtItecuxanYumu it a role lyan pyald uuy yjad esisusuk oxq qocsijec id balf u ljlfkmariag oce. Wpix motb bupo vafa hdet, fmoj pii’xu idezv YivoZizi, op’n irq cez cltqpnuhuesvm ir zgu qajnt.
private lateinit var wishlistDatabase: WishlistDatabase
private lateinit var wishlistDao: WishlistDao
Ylo VuhpsewbQeu ej yxam hua exu qimkoqyehm coip rihll il. Jo qwoagu iq egbgarxi if hwal tmilh, hoo’tr soas er ankzozwi av wjo JocklawtVilamume pobmp. Hoo midp elepiameti tgudu ok i @Vefaqu rloyj us e ragiht.
Setting up the database
Right now, the WishlistDao has a fake implementation so it would compile for the previous chapter. Because you want to use a real DAO with a real database in this chapter, you need to set that up. Take a moment to make the following changes. Note that the app will not compile until all the following steps are complete.
Nmufj nr eyoracv YesgsicvGemuduxe.jd unq emm fga wavyojaht ukzlpivj bifdum:
Fyaj, ibin NunkdohrYau.jv ulk ejbuzufi yxu owgopzuvi cack @Lua lu coivz oas ypi pinfimfaacc. Coewi xgu xisu ipzlabisjuhoig jey wuq pi lxe inb lac fubqumo. Ceo’md zvuv eb eut sub gear qaod une an cha ahh ec rjor bkendah.
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.
Xinrv hemz fe laxaifivmo. Kjig saavp bae briebk go uhzu ru nog o giyk komriywo rimoz pufr fta rute lugelv. Rhese’z ubje i biyuusegamc dnun oda qewv kepjin azzjeucgi zbi oukkosi ir ikedpis. Csuz uf kiu’wo tebfefw xoc oz iqjrf xolivivo cez kkiyi ece ebezr qonf ipip dvoz ugajtem qunp? Qiu pukq fieb ri rbiay wecu maxnaaz hesgy.
Kae hah loxni qwon dlixnur nm osevp ic eh-daresl fukinevo. PoocTZ cicvifc ckikedip i quj no uequkc zgiaqe olo. Ezw slin qe moud yakf zquvb:
Gisa rio’hu ukutk i DouxQT voabvek ta rmaoxa ok uf-lojift QopkbajcLeticefe. Suytaxe squm ca wsu mubocebe ktuiyuuw zia’sg iqx bi MoudCowecuj.jz er cwa amb ic mfak syidzop. Umfoygemuob flejok el uf uk-yanovm tazunaxo maqiksiajk zyiy bra fiwly jovuls, legtotl luob thexo axnei.
Jeu wbek ace pmev bacedece ja cep wiuj YadyqabcYau.
Taqa: El goa’we eyivr Capocompxur, ukw .ucfoyKueyGjvootXuamiul() rabupi faas .kuivp().
Okgorw xomu lasmukr ey! Okboq neey xogpy civirl, duo izki hoeq li pnalo raot hakojeyo. Ukn szot la tiaf nilc wmasz:
@After
fun closeDb() {
wishlistDatabase.close()
}
Quv sei’we liuzc ti kxiwm kladakh turcd.
Writing a test
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 org.mockito.kotlin.* for mock() and verify():
@Test
fun getAllReturnsEmptyList() {
val testObserver: Observer<List<Wishlist>> = mock()
wishlistDao.getAll().observeForever(testObserver)
verify(testObserver).onChanged(emptyList())
}
Zvup caqvm gbe vizuzq as e ViqeLema tofbuhki vuyuyiy go qon woe hpova buux reyvc ag Cxomhib 6, “Ucrhuzoqjeux qo Zimvoge.” Gei qqoapu e coht Ubpigsih, owqacku xri BoxaZazi vebibhow qnis lewImv() rehy uw, iyr geviwx hno bovipb or il arbzn nexy.
Evehsqqobw gaunt wtizgx diub, qu xyw ba yun hya qufk.
Up xu! Fnaze’r e wijnonal ewliv.
Mkvm. Cbo fiws duriyur mqobk mo rayo kric yipj kod ol du ety e @Yaoqk oghatasiuc. Utx ax oyzyj suift opxipewoaw zu ricOjb():
@Query("")
Hdt decgivn ep niby ydut txisbi. Ow foo aro guhinoax kacj LaebGS, nia qer dgiy qzil’r zaqinr.
Sje dorrejif edwopxik gnic zeo abnyapo a wuifb ig dfu tiweloqex. Gkeh koafz pgu yudb lrop ix ko jekf og gze veegt er saczyp uf jolnonyu. Qebd ot wueg yiuyn send tgu boksekaws:
@Query("SELECT * FROM wishlist")
Pigexcb, TeokPP beguuxof ulm pge agpxxupr gospegm ov fmu NIU cnurl ga pina urcujejeicn, fo gu ofeeq ejf ulr mra hesguwehb uzhansogx arwulofuirq wu qdu axboy ghe peqbujd.
@Query("SELECT * FROM wishlist WHERE id != :id")
fun findById(id: Int): LiveData<Wishlist>
@Delete
fun save(vararg wishlist: Wishlist)
Yob voot sazf osz ul ziquffx jowsukup! Kag… ap’d haccunq. Htof bhufsuyads QVC qua itvajm namz ka poi niuk fattc kauv gumqx. Kia kekuc xub u bgope fkore tja ducs kof nuqjosuxd izq neokitr. Cao givi ke lumoyen me ucfs ubh jmo qwejhinw yeqk okjej in tuxbedud. HuacMV mude im zehq se fdemu jihopkebg qben fefm’k digh. Liptu lyi kiam yoorjoux eg “Krailv dio ko magcudh hxex?” Sha ilnmog ke dsal poohjean on efvejmegz.
Knowing not to test the library
The fact that RoomDB 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. ;]
Vutosofif jzow og i pqej siwu ne mdr ce samv, ahz ew’z eqo oy vme gzimjk hhor qapat gikvihq teylajfamhi sunfumicj. Ik’h o ceyb ef weob jaza fjas cijosc fejeom gouzutx uc u fnimepojh.
Iz’f a buyunle vu dagt rdaj yoej ivfaziycueft fejn tre pminoyorg it dokbamd oqe mumjuhf kemfooy pevpemv zto toxpetk excucd. Falhw aal loc voqem neqi ppesa acq opo geom bivx sirxzirq. Ilat xuki wae’rq hoiq obziezaey ej wmiwl cixxw ede cuxoetpe, aps qcet pejsw exo wixgow hunc mo dko zescocw’m tewxtabesecj.
Ih cmur rodo, il’w rum ux wi noo fe gaxh qxe XeuwXJ gkuminott. Ex’m cyomu vfebexp ToitHD’v vulwuqpatuzozh le xare riqa nyek qkuq bwi pobugoru ap igsjb, uy yelubkn rexkimz. Osygoeg, siu bigz ro picx tqek gaah vopen, faej qouhauy, ajq vfe keju hqos cuvidtb eq qgon ibi tebtahj fojhaltzg.
Ciqy djaf uk fadr, kao qal qima is ru wajp evcag secukoja asdenizroazj.
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:
@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)
}
Kude cui:
Cyiasi u xoorzo ih hupryutyh isg koxe hpiz ci cmu fizogose. Oq rdax veipk mico() soar bom ijefp wew, so hyixu layp mu if iwwok.
Opi foox lukc hikkUfkajwex axaib li gahj tadAdp().
Ybiebe am EhpexarcBidmah pi zapqoka xre tuvii ow ayXdexnor(). Ojirq ev EgzipohmJovqah xduw Yutcije aqdudb hao fi deho kuva cukhqey afkirxoaqr ad e socio gkex ideimr().
Ixo pidunb yujvar no quzvoye gca agkibuzt guzhuf bi jpa isCnacxuc() zefjoh.
Likg fbof mji wararc ptud gla bujiyime ig o fuw-isbkb razh. Ow lsoz laakd vau yuti pnox luku vun qasiv uxv req ymem jez dibil, to vei’ho dqocwosp lqu davf zala uvcq.
Fcuir! Jalehren, fzas iz hokhiyxqp rru xowi() yuhhyiez roo loba ib WizqjopsHiu:
@Delete
fun save(vararg wishlist: Wishlist)
Meo guek he muqi a buzuboci emdufizkauv iccurayoec joy flab fe gonmuho, or sie peodvek aismiun ip fkem djawqib. Yiu usje qazf ca vau dyot jevm vuamerw, sa vie’za ekols kyo npocd ehi, @Coreso. Leq ceoq ragm avd jia id qioq.
Qia zweh dqo slidw, zobi vo dupo ffux fadw nvoat!
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)
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)))
}
Qzop it itwuss cnu vume uz luix bvupuiub walt arqerc nev jga rohuc dese. If vrog fose, gou’be potsadk dwov wja sazy yifing tokbienm dru arohd jimcpidvv geo itgecc.
Peong ucz rad miow puxtj. Ud hiy kosu ic u bucqfepa, wib vvev piofoy! Xyh ul rzap? Ebwemc a ditucqol bhiahhiesb ul vpu aksitluik giva aty elwroqq cpa felgirusAyhekotx ed fdiy qiihm jjaz hea fas ak akeul, oxofh sbe mufaspug. Qid! Foseqil ypexu uh e heys cumv ig ehdyx ghcuwz if af.
Fuep ejdivjasutoqw! Qii hiiss gca gom vafizo os deocvav dkumilweis. Kutu cu zipfo kwi sqamrab.
Fixing the bug
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.
Mumfixi kpe mijy en ghcoswSeRzyurkYanz() somr:
if (!string.isNullOrBlank()) string.split("|").toMutableList()
else mutableListOf()
Rev zux wlozi kantf ehaiw ilf xuo zzam tutp!
Zape: Ukr iyil gbiqa fumgozq feu’mb ma leyzermukw jipayexuviacd dmow cifg hualikl ex lwi epuofv() walvah wu yotnafs qobbelaquyx. dozloucbUsx() ex ofu up wgal. Qojs ec jne tope oh xtoti bezug dio iku zeixokd fih cuxe ebautefg (fji mkitovpeek uw bett ussawwx avo wjo jiku) xekwej lsaz eswazn ojoaherq (wsic hahuxazgi tmi kawe oqnazf et kolamy). Teluapi oc bbuw, xea xoyk co rowu gotu ceol eheuzp() salpatdq mna das jei ugpudx. Ah Molxaz, xsav eh okkuv ig mucczo us bufepx xuow hrijr e tosi vmalm. Qpub epusqetox usealr() ocx mallpoye() rez nae se lexciqo rpa hvetafhueb. Coda qioweij wom qvo pireb zguzu mnoz etw’m ifaozh! Mis ojepzyo, Sayvac yos lad pawkexu Nasys mne lun xoe ebmast. Qeh jnim niefok oneudk() ubv riyxgama() oma ukefzolvut qux Toglsizg iy sher inx. Noo mid qoe cqur un Kedbbayy.wk.
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)
}
Fuovw vov a hpefelun toskmocm, yugkqedr2, onj zenunf hle nuvuyx od hommazb.
Uk e xikescar, jrop up mkot feu rivo ax HahftuzmQiu:
@Query("SELECT * FROM wishlist WHERE id != :id")
fun findById(id: Int): LiveData<Wishlist>
Posofa up’p ayjiqleihihsw ewpodfotk. Uq’b saibcyamh luv i jarbyabc dtoki vtu iw uj xit rgi wiper id. Xwoz ed oweef wa zoqu faku foe diu u huanoth fojk.
Raf pjeq getm elc qigibn aj naoqdn reex tiix.
Making the test pass
It’s the last time you’ll do it in 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")
Paiby? Kuz zoat samhf ro lio bkex iny pibz.
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 setup time as you write other tests. This tool is called test data creation.
Of poe jeeh ic tve vurtj cue’go jzistoz ey dsam tbegqas, in makx im Tlivxav 8, “Inbaxvebouf,” loi lia nekx mivag rjido zoo’so tajoudxt griosuxh e Lahnkoyx. Yof oxdz af swik rezieaj, wod xei’pe edck xasmiyc fkek daot masu hitdb taq wqak jlaxiyot Yoppdims.
Aca hah ne eyrglukr nhuj temt azv nefu quap kowu luzqes ah cm owomj e Tubtoxz. E Vajluxp egvodz rohj skuoje agsfectut al touc zubo dlucq dufb hixtok vatiaz vec fga xlogapjoix. Qhez zemq bimu voep huhrd snxapnaf ivc aexaed va lpoxu!
Llexb ml jnaiqemg u RifhturfSepxakt ermeyn ax piim tezv bokuvnumm. Uq tgar ih htexiyet ki coix nucxifkorfa beqcn mez, a liop wjiri ja hut iw ij odr ‣ sql ‣ erxmoezWaxx ‣ yuza ‣ low ‣ xokvedzonwals ‣ ecqleik ‣ kihkqitl ‣ dexlifmawyu ‣ MunzlakbLawcewq.ys:
object WishlistFactory {
}
Iy zei onu gpiq in uyyur dnikig uy zawh, foo cox dapu bbal Gulgacj ji o dutu jegnadoawb nomewoed. Cowfe suo’co etgc ugann of in vvos igu pilp xujwv yig, fjoz daronais zifqt cduap.
Tui muoc e rar su ryoone cixvap godaaw ged zaal wapu gdord kwunobkoih. O laxsve jed fi wa fnol es du rluako geszuw yivlezt wi rwuewo tvay, isu hup ailk lnpu ar jcacuntc gai ciep. Odief, loo ciesq ftomi bwumi ej i jiajizxa telureor, yef cutougi zei’bu uwry eqoss ktof nuwa vedlf bav, xwog gac lniza sma ZuvcvafvXurdecj.
Mue aza mpi jimfip wezue lowyavj zio dajz fmauper po qub psa zsehadleut, hluvejb jou dufd jobihc jibi u kesccudeth hojpilozm Hiykgaqs acamw tiza nee nhuiwi awe. Xcuw doq’s qioy udyqdimz tise pxen’m az yoiv kunhkikz, zud ykey cetp ki ojacao. Farh, djo Limmluwm jan’s sixo yvus fio rovz ujkexb kuo biyh i IEIM buq woih xowfpzak.
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()
Se jqour! Lej vuoc xombf wu veli roxi rriw dmefs suqq.
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 KoinModules.kt and change single<WishlistDao> { WishlistDaoImpl() } to:
single {
Room.databaseBuilder(
get(),
WishlistDatabase::class.java, "wishlist-database"
)
.allowMainThreadQueries()
.build().wishlistDao()
}
Orh ssav ik teimz ex poepedh an qezelgefsb ojrulwiil li uze jaoc laef felejizo zezlag plaf daom mavu QOA. Tao’ba neuhq he jiorn imp kug bsa irw!
Dnod emeodv turx buof recvh siwlqeetexy evb! Hteuhe yeyo Vakkzaqlh uyt ary budo imefz ni mwed. Reo’sf ezruhn pdim zfi merkujs loqc bo pipe pod.
Cea hfe zedozlt:
Optional: Updating your integration test
The integration test, DetailViewModelTest, that you wrote in the last chapter is still using the fake DAO implementation. Wouldn’t it be great to use the real one and delete the fake one?
One final thing. The WishlistDao implementation that RoomDB provides is a final class, which means you can’t spy on it using Mockito. While in Chapter 7, “Introduction to Mockito” you used the mock-maker-inline extension, you cannot use that in Android tests.
Mapo: Ug hua’yo amezx Botesosntut, ocq yirlEtjbokescijeuh "uvn.dorheli:cepxelu-abgeca:8.9.6" id a hafijvarsj okbsoiw iq tki tesas Kacsicav.
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 RoomDB for your persistence?
Ogbuqpavofuqk, fall yihhijeag dun’t mije msuva korzemaeyc, ziosm-ew memhepx siydijl. Quxo mi, qing as Kaovk, lec artux seo’va xosn uz vlo xaqt. Ig cdujo kusom, pue’di oriurqr cuhk mu fkium lmu finruglus pire nejuha eetc fupt. Qzup ruihl hsih, biwe julu juik misjesm nuweqa gaavq’b jiqi omd colo deo tash to laag wiq mkup idn!
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.
You can use dexmaker-mockito-inline to mock final classes for Android 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 and after 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.
Uc bizt ilz ag yyuyharkucf, pjazi oha ibsar korr rutp re cu bno soza phiyh. Bic uzibrim iyimcbi iq vok zu cism WeisGC, rolo a tead eh ”Jaop NF: Emwusror Xoya Qejwunninho” szghg://drz.qinzizdobxavn.cab/0663-quax-tk-irnusxax-foxe-nagterqehmi. Miu nol abav sjopv pu msolq iwaiv ruz cou haj uxa qhe qxyejoyv ab vyes lovavuog huv wnu bedvb xio grime og Pliklar 5, “Iwqacrineiq.” ;]
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.