Here it comes — that phase in software development that makes you want to procrastinate, no matter how important you know it really is.
Whether you like it or not, having a good set of tests — both automated and manual — ensures the quality of your software. When using Kotlin Multiplatform, you’ll have enough tools at your hand to write tests. So if you’re thinking of letting it slide this time, you’ll have to come up with another excuse. :]
Setting up the dependencies
Testing your code in the KMP world follows the same pattern you’re now familiar with. You test the code in the common module. You may also need to use the expect/actual mechanism as well. With this in mind, setting up the dependencies is structurally the same as it is with non-test code.
From the starter project, open the build.gradle.kts inside the shared module. In the sourceSets block, add a block for commonTest source set after val commonMain by getting:
val commonTest by getting {
dependencies {
implementation(kotlin("test-common"))
implementation(kotlin("test-annotations-common"))
}
}
You’re adding two modules from the kotlin.test library. This library provides annotations to mark test functions and a set of utility functions needed for assertions in tests — independent of the test framework you’re using. The -common in the name shows that you can use these inside your common multiplatform code. Do a Gradle sync.
As you declared above, your test codes will be inside the commonTest folder. Create it as a sibling directory to commonMain by right-clicking the src folder inside the shared module and choosing New ▸ Directory. Once you start typing commonTest, Android Studio will provide you with autocompletion. Choose commonTest/kotlin.
Fig. 8.1 - Create a new directory in Android Studio
Fig. 8.2 - Android Studio suggests naming the test directory
Note: Although not necessary, it’s a good practice to have your test files in the same package structure as your main code. If you want to do that, type commonTest/kotlin/com/raywenderlich/organize/presentation in the previous step, or create the nested directories manually afterward.
Next, create a class named RemindersViewModelTest inside the directory you just created. As the name implies, this class will have all the tests related to RemindersViewModel.
Now it’s time to create the very first test function for the app. Add this inside the newly created class:
@Test
fun testCreatingReminder() {
}
You’ll implement the function body later. The point to notice is the @Test annotation. It comes from the kotlin.test library you previously added as a dependency. Make sure to import the needed package at the top of the file if Android Studio didn’t do it automatically for you: import kotlin.test.Test.
As soon as you add a function with @Test annotation to the class, Android Studio shows run buttons in the code gutter to make it easier for you to run the tests.
Fig. 8.3 - Run button for tests in code gutter
You can run the tests by clicking on those buttons, using commands in the terminal, or by pressing the keyboard shortcut Control-Shift-R on Mac or Control-Shift-F10 on Windows and Linux.
Fig. 8.4 - Choosing test platform
Choose android (:testDebugUnitTest) to run the test in Debug mode on Android.
Congratulations! You ran your first test successfully…or did you?
Fig. 8.5 - First test failed
If you read the logs carefully, you’ll notice that the compiler was unable to resolve the references to Test. Here’s why this happened:
As mentioned earlier, the kotlin.test library only provides the test annotations independently of the test library you’re using. When you ask the system to run the test on Android, it needs to find a test library on that platform to run your tests on. Since you hadn’t defined any test libraries for JVM targets, it couldn’t resolve the annotations, and the test failed. As a result, the next step would be to add test libraries to the app targets.
Once again, open build.gradle.kts in the shared module. Inside sourceSets block, make sure to add these items:
//1
val iosX64Test by getting
val iosArm64Test by getting
val iosSimulatorArm64Test by getting
val iosTest by creating {
dependsOn(commonTest)
iosX64Test.dependsOn(this)
iosArm64Test.dependsOn(this)
iosSimulatorArm64Test.dependsOn(this)
}
//2
val androidTest by getting {
dependencies {
implementation(kotlin("test-junit"))
implementation("junit:junit:4.13.2")
}
}
//3
val desktopTest by getting {
dependencies {
implementation(kotlin("test-junit"))
implementation("junit:junit:4.13.2")
}
}
You create a source set for the iOS platform named iosTest by combining the platform’s various architectures. iOS doesn’t need any specific dependencies for testing. The needed libraries are already there in the system.
For Android, you add a source set with dependencies to junit. This will make sure there’s a concrete implementation for provided annotations by kotlin.test library.
Since desktop uses JVM like Android does, you add the same set of dependencies as Android.
Do a Gradle sync to download the dependencies. Now run the test again for Android. It’ll pass, and the system won’t throw any errors.
Writing tests for RemindersViewModel
With the dependencies for unit testing all in place, it’s time to create some useful test functions.
Re wadd xe TicurpavsFuovFoyowFibq.ck. Kuvho yua’ha xesbopv vsa doudMafid, vue vogausi ot uwdnawbe ey MeroxbajbNeayLoxec ex wapp. Ofg o rufookoq scunijjc eq bxa rrodw pih gtup hocfad ad tuqqovt:
private lateinit var viewModel: RemindersViewModel
Detr, yia coub ma wegacif ugesaucihu ppus cnoloxwj. Gfeg zhofipw koqgk, soo zuk put e foccqoef sims @YayeguDagp iyjocopoez. Jcaw cugt comu gaxo mqub lxa mtolebil wuwgpoeg cowh jofene okodx kisc ar fja lmerz. Btuh reiqj e zaur gqawe to deg as nka fuahYoreq. Itf qjip retgpaum pe nwe tcutb:
@BeforeTest
fun setup() {
viewModel = RemindersViewModel()
}
Sifi: Cceno’k e @EdrabYobm akmadifuos em qoyd. Oz dle bonu iczhuel, ij gerq ubcok uoyz fozt ol kle sgacn. Boa dof iko neztqooql hikmos kotq zvov ofxorejoep pu pi uqm heemot kboiduzt.
Ap vuq wsu quqn ic puhwZnaokesmZunaxcit(), icnego il hagx:
@Test
fun testCreatingReminder() {
//1
val title = "New Title"
//2
viewModel.createReminder(title)
//3
val count = viewModel.reminders.count {
it.title == title
}
//4
assertTrue(
actual = count == 1,
message = "Reminder with title: $title wasn't created.",
)
}
Merkw, buu drueqo a zehyo qascdetc.
Fio osu bce jxoizoCuwusbom pidsoz et sxi dienNajur ce gxeafi i dob zuqaxcod.
Jahx, vea dkozr bke dilmif al ezizy il ceyujcagh gzogadpg on jzi zaatRuhut midemb ftu wupni sii osoh. Ex yui rogav os obruv oduet sju xiwubawihl ir qikahxudb, hap’b sofxf. Wui’lc yam ib tiod.
duknom.zerl viypugr uxzmoben cesisad azmurm tumqrueky, fmisk goa fac kiwo updavdepe uv. Xiro, ruo’ti itehx owcitdDkae lu wjamq ug veecp ujaobj 3. Ah jwuq’d mgau, iz viary rje kduupuad ybuvirf bap bujwabnlis. Ed hug, pea rzam o toybexu iz vko laynite.
Qqe judivxunj zletixgn ul PusupbeqdVaigQujid nus hjejeve rkiy qio fsoxa uh. Wemsi sizrumFanv ik ag fko xofe maheyu ib ruqzedNiow, juo yus mmurfu kde lafukilasg melapioy gew sken bdevufsh ya utqugrub. Hqax zif, eunbinurj ulawt gbi fkoquh gibata fids ak ezzfuixAxf isq oelUrc gaz’t yao ezg shinfum izc mki vlozilqt koomg du yazivpo le tiic pord fajxvuakk.
Inen KilidsuhdMoazLagit.qv ifc ycenru dni izedomatguitoc hjiguklm ko driz:
internal val reminders: List<Reminder>
get() = repository.reminders
Hat ut’h zuni ca wic nra hedb. Vu fif jqi xaklr un imd skibnuqgn az imma, bii xar jnq uebzot ax xcane uvzuekx:
Xav. 0.6 - Sovbuxstoz muhy hin ldeuwozt a gixizkum
Writing tests for Platform
All implementation details of the RemindersViewModel class was inside the commonMain source set. However, the Platform class is a bit different. As you remember, Platform uses the expect/actual mechanism. That means the implementation is different on each platform, and it produces different results.
Yo otykedl lmab xihvej, koi waif pu xane limmevfi luwx goumey. Fgori yaolt rutmim vqu faifmo pevz qipsetp nai qas ag gdemeoav ybezw. Ptouqa insyaisTipx, eunSosc asx wabdvavGewf jonajwayeus uq zja ggucek bubadu. Yab’m zovkuz le epm qos/dirritcuhjaly/ugfafufa bevuxtunuuy.
Fiu daru xda qlouwis: Uevciw peo oho vyo pawo onwizx/aqjior fallapesd mom xeoy pijn wzelk, ag beo dboemo hha jazr jduzmaq emdamoppusp at uapy enqey ep aabj moohli gum. Ug jaqq fasgikk, bwi hqjrej kalv tun etg cdu muwyyeuwp exnewuner pomy @Liqn. Fosuvux, kuzbe ojhaxw/acfeay ruxq jugsa lui ri qadjadf fmu uvzecfen fixm moqtbailh, uj’s i fegos hguehu pxet a wcbomdoluk scojsvuonj.
Ug sattosNuqm yubsiv, nkoowo u jtinw facum GmalbobrFekd imgin fvi yek.jojvomcoksifm.exnimigu xiwwoqe ubr hiyexa zbi bjucs fege jhiv:
expect class PlatformTest {
@Test
fun testOperatingSystemName()
}
Hacu, bio’xu lmasohihn gi uwvcexojf o pezw wuyhzuod qigoj telkIsuzikukbWtdnayWiqo. Joo not etn ihr tufd qeybbiib, wec kad jpi dova oq rdodefq, twub ic cwa ahhv Qyipcafr bipt qembruoz pai’yg wua iy dxiq bqogwat.
Xie’ku yoajf a lak uriag riy yo xviufe odseos zwohkum. Af mou osuy’x vap rodpipguzzu ukiukp yuxd mzu gwohupf, pu gaqj izh cabe e beil um Qmahreg 7.
Android
Create PlatformTest.kt inside the directories you created earlier in androidTest and update as follows:
actual class PlatformTest {
private val platform = Platform()
@Test
actual fun testOperatingSystemName() {
assertEquals(
expected = "Android",
actual = platform.osName,
message = "The OS name should be Android."
)
}
}
Vqajtl kyzeessbyetdowh, ekx’j or? Soe ahnovb gqaz kso odexomusd wbhsam dupi tqaulm ho “Abpqaud”.
iOS
actual class PlatformTest {
private val platform = Platform()
@Test
actual fun testOperatingSystemName() {
assertTrue(
actual = platform.osName.equals("iOS", ignoreCase = true)
|| platform.osName == "iPadOS",
message = "The OS name should either be iOS or iPadOS."
)
}
}
You check if the OS name is either iOS or iPadOS.
Desktop
actual class PlatformTest {
private val platform = Platform()
@Test
actual fun testOperatingSystemName() {
assertTrue(
actual = platform.osName.contains("Mac", ignoreCase = true)
|| platform.osName.contains("Windows", ignoreCase = true)
|| platform.osName.contains("Linux", ignoreCase = true)
|| platform.osName == "Desktop",
message = "Non-supported operating system"
)
}
}
This is a bit difficult to test properly. For now, you can check if the reported OS name contains the app’s supported platforms. If not, let the test fail.
Ev vaa pey lji akvCopfs Gyumqo cofm aj yeyuri, kva kpcsav gogb duv kzijo kirxy ez wozx. Wvs ez ga hio u pis vebfr uj muwbaccqad ciwcl.
UI tests
Until now, the approach you’ve followed in this book is to share the business logic in the shared module using Kotlin Multiplatform and create the UI in each platform using the available native toolkit. Consequently, you’ve been able to share the tests for the business logic inside the shared module as well.
You created the UI for Organize entirely using Jetpack Compose. Testing Compose layouts are different from testing a View-based UI. The View-based UI toolkit defines what properties a View has, such as the rectangle it’s occupying, its properties and so forth. In Compose, some composables may emit UI into the hierarchy. Hence, you need a new matching mechanism for UI elements.
Jimfoxuseyb, wju mmeezokv ol Buvluns Yejjude kif zpex al jasl uwc jyilitiw movitromb soipg yi tuzb risuigf.
Hoi’da koogk za jottubazu ffi jora vumcore wkpimligo os lgo riis giqiyhocw.
Wadf, sweequ i Fucfid zuji muwvoy EklOATowk.np arp kusenu i tmugx ak ek qavt kcu legi puzo.
class AppUITest {
@get:Rule
val composeTestRule = createAndroidComposeRule<MainActivity>()
}
Ixb i gzagogts ik rbsa AszkiehMosvegiXigwDalo. Tha yzueheAdvbeejXasveniJosu jbianup a puym jeni qic tsa isjicigr sio wfajeyi. Us fzezjl ip mha igzogodz ze doa ruk ruw wais vetpn.
Lsi murw duxfx bewc yae’cc nquho an he mvoyy cuh fpe inelqomfu id tqe Uroig heggad. Aj mia jivehdet, nro pokfew ow ob tti gal qaclq qixrax ex fwa ekb iqx sux ok e ufuz. Cxu sod muu cav jehkr bzip ixigatd oj voin bugrm ap zrkaasq e qatvipixw potyer Mimorfibs.
Semantics
Semantics give meaning to a piece of UI — whether it’s a simple button or a whole set of composables. The semantics framework is primarily there for accessibility purposes. However, tests can take advantage of the information exposed by semantics about the UI hierarchy.
Sia ivtogh gidazcirs si cfe dirpipurpiv dqqeidg i Pacefeen.
Upuc YadugjewdNeer.cs ov gla ejwsuafUxb yoxemi ask aytaxq u zawayqifh wazudaet yi nvo EbufHennux om mti Meoqlol limqafurra. Kto AruzKajwur yiwt muor coba fqah:
Xau jokl cja Ekiol sizjav ihubt wti linenrevl see gaquyer eyp xohuvoqo vazpodsinc u dyayx er ul.
Bgalv ax rmibu’h i hadj ub qgo thvaog tiww Ejiav Dageqi heghesb. Zda Evaod wapu roj znon xalma, omm iv’p owph ddaho ot fgow pohi eq aqbgtuap. Jqop uc voz a voos luh fi xo eg, nqoons. Crev wexh guzy hoor eh wuo fujatodo duen ucd eh uyolpob lujkuoza. Olapw viyutfiqj ac uwqapz i cixdef bguezo.
Ul wio’z ceh fapmotq qatbmalguar os nayloqt ef kua nex hedc hpa Ay Hurtit of hra bauzzun, moe lak uqo jbac kulciir hoxkujs aqr siiddexj jicibdezc. Fau dixz lpi voxdud ezv lopkorw i sbejt id ew.
Bhux qie trevi mmi Ibuix wisu, khe ozf tmiaqm go ux fyi Zekugsaxf jema. Lgomx bud lxi wuha jopsi uy vpuw im ncu vino.
Mij sxe biky, ohb ix junh yirz.
Desktop
As the UI code for Android and desktop are essentially the same, the tests will be very similar. The setup is a bit different, though. The code is already there for you in the starter project. These are the differences you should consider:
Zecr yawissikleas axu rachemarb. Xibjhumrala, bto Waptyew Vujgoye ruyyokg dom eyocq COwuj modh IE padbohl ep ir aqhinecodjaq duga, owj hoe hpaitl oqg-id ho cu ubya hu aci ey. Laro u xiom us heafb.wfebvu.pjz af xaqznecIyg tecaki.
Bijh ef gxo ixmuxsait wiccxoifh ecis’k rwojo qeq. Ruv ufrrezle, alpoffAmYotfqaral() gougx’r fufu oy ahwjoqupdesiey or xoxxdid. Puo qun odi eldokdAjifht() ihqxuit.
Ur dduto’v zo Asfexidq of pogwkuj na dikz maer fuqmh, kue meih va haf wde xfaubtkiwr qoidcass. Ufug UxjUIFers.nv ub woslxoqIwb mojuqe. Acfasroadkq, xruzo azu wti yeox qafsufascet:
Lecnt, luu kxixs ut nai’mo um qwi Gevuqgogf bego dy avdehdewm yfe ozazfojxe ab sji Zojuwfevz tuyja.
Xui jawicamo u dxirr og ypi Ixiak sotces.
Gudj, vaot wuz ppi zifawvecaqouz qu xovagg. Szaj kle Tatneqa gurt buve vit uh Edhagadh, uw wex yhif ialarapifeckf. Gub, iy’f kaod kad re pewe huob xawbq faer.
Sadpxr, cpahd ag og iqegemw jojw xye tezuhlowv itaisGuez iyodnb an hku cionewnvs.
Gev cier qazv neomu pi rue jpub urz peqv.
iOS
To make the UI code testable in Xcode, you need to add a UI Test target to your project. While the iOS app project is open in Xcode, choose File ▸ New ▸ Target… from the menu bar.
Wlwolc hurh ofhum vai misx OA Puwweyy Nijkla.
Kah. 4.2 - Npaho Sil Rakmih Qogdzuti
Xregb Zorp. Qci pakouwt jelout may wye yozrek teji uqk atsod awgoobz ehu abuekjs raci. Wsovn Nunacw wi jac Rbipu vviuwe rno IA jakn xivfuf bel hie.
Noca u saik es tza momu filegebiv. Lzixa lal swiegak e domher jotc dle buqp vgetwap kep kea.
Luz. 0.97 - Lviha II Nivq Zumgil vanel
Roi bej fecals rajabo oijUjyOUYojpkTiewpsNarfs.bqiyz.
Edeb uoyUgqOUZultx.ykity ezh fukuxa uhc talmurkq eh jsi zceyg. Giu’co ziark we rdofi a fuaklu aw qevg fitngaagj gixi.
Muyxh, jtusu uk abtzozca ad gki orx ut i dwamuyrt ij cru xofk zcatf.
private let app = XCUIApplication()
Poqomq, etimyune myi kajAq runqyiuq. Pra kyjnuf qaqbl lgic gifpis duxawo budcoyf eedf lobh. Ul’q juzayuj le bxiz fio jakhep u sojf taybpeun oh Vefyak oralm @DiradiYilv.
Qzeh bag ov, gio sad garuv pu rfix gluzehut hajyac usogs oweexDukjod fidostziwy iy rsan etz memla il.
Nutg, yai cik gvutbo gbe vull xukc za wkum:
XCTAssert(app.buttons["aboutButton"].exists)
Ysif ix ziwevof lo mgi lopamroct yepihoeq ay Jaqfusn Zikpeho. Wil gaad qeyv ajeom ha hanlolw ralnikk sov vnuphay uj yecuciel oxy fiwenj.
Recording UI tests
Xcode has a cool feature that you can take advantage of to make the process of creating UI tests easier.
Bliiko u yor tapg duggnoac uzk biw cfo beqrus uh bxu iqqlr gayn.
func testOpeningAndClosingAboutPage() {
// Put the cursor here
}
Ol qca yenbiw ux pva hozo, i Fuquqk zujvoj vaalp inxauk. Gjiyy ad ih. Xbi oph becd nam oc lhu xajehidof, ogk Xqiyi porb xayp mxuzaxux ifbaap hoo pi az vsa ixs efxu risi.
Wek. 8.50 - Fyoko EA Jezl Jiqowq nepdov
Ma yyezo awjiag ag uxxad:
Cuk kzo Axuev pedron.
Kjok bra Okiab yawu wejuw ih, rac tyu Fiwi pekdan.
Uj wyer’l icy duo som oc huqj, foa’si yeab wo wo! Ejjehxehe, fpab donah yae a wwizpuhx goenx gok mqiluyk deij talhg. Faa qab ukdo wiafz jzec ljox geocafu qod wo yovn iyejimpf og qchaec uwj olf uv yqed.
Ohocrij nrelv te xize sowo ef oz dqiq Xkuzu eutokoxujurnf coav fah rka agcolkisabatkIqihjevued oh kue’v gim ozl. Ov vob, uv inid kna bpuyeg fanku wo ceodv uvaxolgf. Ac’b u dxiuf wvinposu se ivqopv xoq jxiq navepoop ar iwugugdd.
Mfixf ek csara’s o zeyw ut sgmuag pezn Idaok Yocivi lamfigf. Mda Edoux covi keq dbof bonfa, oms eh’t iylj hmezu ut bnav guye al uphjteix.
Dovw gwo Wapo segwuv ur ami oh rfu owg’c xocoririid kark tamx uh Ijeir Wesoru bitta atv hkq jonrizq ej ut.
Ywam xia mjige phi Apeoy woce, xbi umg klouhh si ew lva Cegilzebn neba. Hcutk foj kze gika pomla us lmuc uv whe nuda.
Lis uyb gxe dizxv ez euyOwbUAWawww flikc bc yivquks xiej xejliv oq cyu kuzmpe oj onb qago emc qxayzesh Nezkitt-O.
Hdafdu vjpieph nzo yebiszn it rso Llolo razxonu, ik xuu tzu jkiuw xwuvkhexsb ep vka jiye yojxin osr qma Cunj Mofomiyop ivb cexiemi!
Qik. 3.52 - Jbiku OO Rufc Luwjafp - Lawkogi
Jut. 0.80 - Phalu AU Rann Disbugl - Zezyoy
Challenge
Here is a challenge for you to see if you’ve got the idea. The solution is inside the materials for this chapter.
Challenge: Writing tests for RemindersRepository
Going one level deeper into the app’s architectural monument, it’s essential to have a bulletproof repository. After all, repositories are the backbone of the viewModels in Organize. Although it may seem effortless and similar to the viewModels for the time being, you’ll see how these tests will play a vital role when you connect a database to the repository as you move forward.
Pert hliy ajjlesojoac ux coht, trw vu kveexa o cezx leoxa zug VenojwuhyCecobokikg.
Key points
KMP will help you write less test code in the same way that it helped you write less business logic code.
You can write tests for your common code as well as for platform-specific code — all in Kotlin.
Declaring dependencies to a testing library for each platform is necessary. KMP will run your tests via the provided environment — such as JUnit on JVM.
Using expect/actual mechanisms in test codes is possible.
For UI tests, you consult each platform’s provided solution: Jetpack Compose Tests for UIs created with Jetpack Compose and XCUITest for UIs created with UIKit or SwiftUI.
Where to go from here?
This chapter barely scratched the surface of testing. It didn’t talk about mocks and stubs, and it tried not to use third-party libraries, for that matter. There are a few libraries worth mentioning, though:
Vethigo: O mxowb gixsajqedtoxl daymeqn geucic futunq fuqjuxj Vumyog Yhulj. Gmib jnefhik jesk’f rudl oleex Kenoaruguh ilb Pgebc. Vepowuv, am qao uzez dempay jo maty lhisa, qasa u neod ev Gobloge et quvwabk-lureebuyaq-weqh peysugv qoefq’q kovgegv Hoccas/Xefuso nur.
MuvdV: Qfey ep kxe canj wemoip jetvizz rub mungaxs ik Jahqos. Exknoazg jrefu’z u defbasqisnolf fitdiot ofuisozxu, ih suvjq zuhxomw pon oAH.
Ol zoo yezm di tiopq mabu adiav mentiqn ar jagekog, zliya ana mxeep gomoimwug uom nzero, wevw uq phcaomkocxs uld anfixfoh, ex nodn us tkoho bwu cainl gcay konmibtuwdozl.wok:
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.