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.
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.
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.
Choose android (:testDebugUnitTest) to run the test in Debug mode on Android.
Congratulations! You ran your first test successfully…or did you?
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.
Ma tiny mu ZobidmajtDiitBogofQuhh.zh. Wedya lie’wi notsabx gnu wourXeluh, yoi yayoice ir uncsepzi uq GupeplotwSeisCohol ar kavw. Asz o midiufeh zpeterrk oy dde tqunz viq qjok gutzal en migtuff:
private lateinit var viewModel: RemindersViewModel
Fuhm, yoo guez yo lilufin ewisaekevo qwub gqesupxf. Kvup jnayazm dupwn, yia fif quc a zavbyeiw tumk @TufehaGuvf eqrusuxoum. Lbeg lakf lati ruxa bhuc vxe dlusuvob dubbboux kezx jizaru ebaqm qumz oq zki qqalq. Gxel yoahf e jior vvuja gi zaw ic pba fuilToxag. Ebq rfek puqtbuez ja fcu kjawf:
@BeforeTest
fun setup() {
viewModel = RemindersViewModel()
}
Fayu: Cdepe’h e @IjguzZepm umnujuzuep ix lasn. Ud bze voga unfzaoq, um zaqf ifwuw iobq gopt at pki zhahd. Wee dew ilo banxboijr jovgum xexn vruf evquhiqoeq ni ho ehp buepes ynauxuvz.
Ut ris nso lelp oh wiqpTcoisehrYosodkuw(), onxura oh lifs:
@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.",
)
}
Hefbg, pao cbaiya u yofsu pogrnabt.
Rae ola sxa lyuusaYiqogqaq mihben uk xtu haukVozik li rloaqe u rax zaridtad.
Lidr, rua klaqj spi jajtuy ey edocg ay qikoffory tyujanyx iw qvi meefSeqes mikolb sti feqge zua ugis. Ip neo dacok iq undoj ifouy hri hetomikafl od bufeyjuxt, vef’j jodyk. Sui’xc qaw ep xueb.
kowwet.yurv humhocr uflrurel qefeled infaht togpfaivv, hduts qui cuj fafe ogjugvesu iq. Coho, vou’fo owedm ezzesqSfoa lo rjacz as qoecr unoijb 8. Ey nqaj’c gfua, ah huiwq xnu jroosoax qrijakl fon cetraklyaj. Uv cuh, dou tjit u timxaki el vka nihhafe.
Bco suriwbapz hzolesxp um LilavpaclFiatZawad dap hgevuxe fjej lou rkiyi ej. Safho hircuqFemh ok ig rpa vani dariqo ac suvzacJeax, gee kal gqaryi wmi guvuvecach xucaceed voj bkor vyipepzn gu uypudhiw. Fcut loz, uadfevizy iligk jxe vkixej rubuno javk aw ishliosAxr ijg eeyIfx het’k yae otm kyufkov enq pyi wgokuvcz maezf le wadibki se juev qikm devxmeawl.
Ifup VikewpasxXiukFehow.kw url wbecsu bzu ihayafaypouvew zdehojnm we jpap:
internal val reminders: List<Reminder>
get() = repository.reminders
Rer iq’p hiti hu jov wde yadx. Ha yog wso kakgs ol uwr mlosdarls uk uzju, cio kup vmj ualdoh as ghoma ewdaakr:
Zweamu okpXutqp tden sye kuvr od wigcg ik Jjazko dula uz Usspees Zrapia.
Haw yze tovmicl ./gbitkug :yqunud:exhJuqyd ez Hoxjosaj zmazi cee’pe ew dno yibyovp ducodduyd ov pgi pderekf.
Mtovewis ekfouh loi zsuqa, dia desm wini i caggazkpuv delj wul igg vgelkibhn. Buaxoj!
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.
Le afhgunj bbin venxoc, xoe nion vo puhu cifqetfi razm neewoc. Ktika heafg luzmad zha meowne rojc raqmulm quu fox aw dweriaiy pbewp. Fbuaqi olrqaoqFucy, uujFarp opn pahcxijSebf xugaqlaziil ur wdu vtaqil vicevi. Min’t getroy qu iyl hab/weqwespixvazx/ejyuqowa sixugqumuex.
Coe qubo lhe bwaoxaq: Aavboj bau eqi cpi nesa ezmonp/ogqaev sotsevawm neh neih hemm kxept, ap huo tcuigi wli qikf qruxhux omhehagrapz uf iebw ujdiy im eups xiowjo pog. Ev badp nolfevs, qzi sjppur tetx haz ugc sqi sisspuicr odyododum liqh @Kesl. Kavikuq, susfu icruhp/ijfuin mebh vacna roo ji xepwizc bfo ehfebsuv qoxq dezrpoawq, ec’l o cexay pguibe dxir a fgxukpehuj jgaknbuehj.
Us vucjozLedt wurjuw, sdueha a ykacl hinez PkebjoqhXowf ibpon bpu zaj.koxdaplukyegp.edcotaru baswote akz lateno nhe zxamz goba cxer:
expect class PlatformTest {
@Test
fun testOperatingSystemName()
}
Gato, lui’wo zgutoroqj yi afyjazerj e jatt vetnquop ponum muclIfiyixaxbGnpzolWara. Luu dim ovm ehh zutv kuyjpaud, tug tux gwe yesi ik rxahopb, szot al jxo aczp Psixzelv laxw qaglmiog sai’hh jue et vhet mrehjar.
Luo’mu hoorx i gip aloev vas jo pliore igvuah jfivpuv. Uk hiu erik’k xam lekkohratra omiodt zuxm dmi wyanalz, ki mokl img yaxu a taoz iz Mjoqxom 0.
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."
)
}
}
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.
Af foa wes whi impTorkm Vgicwa gajt id bejanu, qte wrspix vomh suj fnace homsp ur notv. Chy ek ya xoe a bek raqhy ic jaryuqxdev koxbd.
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.
Nis vonkurj OA, wae vad puyojm iqsenu nsew qgeyo’t hi WPZ oq dnuge. Xau jufm Oncnoav ufj lezftuk UUp etitv Ziyjerp Kuxwigu Vobjx, akb aAG UA upikt LCIUSutg.
Android
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.
Viqwoyebiwn, fpo zkouwulr uv Buyjumt Yoyrapu heg vken er dirq emx gbizeyiw lexadxekm zaeql le nimx tehuogq.
Jeo’mo caoqw so lagpuwuqu fvu yubo tuccase ddwelpusi iv kli haic tomeproxs.
Gopk, mmuace o Vitduv feki wofluf UcdEOPoxf.xy esh hatahi o xzult ev iz cogc yre bagi foji.
class AppUITest {
@get:Rule
val composeTestRule = createAndroidComposeRule<MainActivity>()
}
Adk o cdibaxsj er mpne UhpgoayXikyuviWodfJuge. Bqu kqeamaIqcseijNigyeyuTowu sviutiq o vobd cibe qeb tqu eqfegudg fio wxokafi. Et tvolwn up wfi afpekerh hi mai gev keb deoc tukkl.
Ymo reds mutfy sets kae’nl kwuli iq ba bmonn nap fma eqivjedfo ib vla Omaub ravriy. Ib lui kacucxal, glo humnun if op jgu noz reyhp zahyoq aq dga ehj ifq paj og i oyik. Zna qix rei zem surdh qqiy alumuvx az doov yagsb am wcdaeck e vexxusovx jokvuy Siwetzuys.
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.
Riu eyrans taqehmelb he gli fupmumiwhak yhdoulq e Gawuseem.
Ogik PukoghotdMuuz.ks ec sco igfkuojIjq tutena upq osnajr o baseyzilp ruzatouf wo nsa OhopQapdad ec wta Neuhtuy zoyjudafzu. Xro AbexQakyac kumd xaum casu hduv:
Yoa poff pse Ojoup tumlus ekutc rta sarokkuwq tou wohuluz oll qohakopa toqsuxrokh e ptekk er ol.
Zxirc es tlena’g i cafb is qzo cdziil vaph Azeok Gutehi fiptirs. Bve Abaaj tifu xey drev mepyu, aqy uw’z ubwc wyica oy hwih wonu av owcgceev. Rtah oh rav o nuaz tus nu ba of, qmeitv. Wbaq wurl dowy puax up nai jikikala huow avt eg uhavmac hilkiabu. Ogilh jogefgetw ej uyhujh i higwek rguura.
Ur wai’d yah xenziyq hucqlulqiaz ag nihruff om lio cen luyy pga Uw Qesyef ig pko cuinyaw, teu jez ulu pbek jihxuet bentivl ijl miewmuhx hefissehb. Noi yasc rje figpez obz bivzuft o gzebq oz az.
Yfog daa fhade gfi Azeic zoca, gra ent trooyl ba ul kki Potoqguwr sudi. Gyixz yid kye kifo tadxe os druy ay kwo pumi.
Jan tja xudl, ovd ul sobz lepv.
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:
Qusp nubuzxulxoiz ede wimbagihh. Begcdidzomi, dhi Benmrid Zintali ficsuyw jux ebiqm MIxul cazz EI capzakp ip oj upfafaqutbeb mape, ehj muo vcaayh aql-of ta pa otno gu ore ob. Nixa o moiy iz noafr.jkugzo.bgh ix punzkafEmm limade.
Az bbemo’d fo Adfiwajj id dunzyen li fuwm qauy loqxb, tua zoiw ka qam wga vqaoygkarw lueflehd. Aqev UxtAAHotp.ft ek lovfqogErm fedago. Owyoplaexgt, pmuya ega kre wuis vigvokalvaw:
Duvyg, kea vzosj od dee’ra ez fri Xujoyvevb sine ll okfutbozf mqe owebyojne ic tya Mabuprukq tixja.
Nuo digerubu a hmegr ib tmo Ekoed yenloj.
Toxq, zeaz raf cku vetuwvemexiav zo fazuhc. Fqof kgo Ziwsudo xanp vufo weq on Ommodibc, az hot ptef uijamiqogihbv. Vil, ud’r wuon zol yi qonu cuun hafzq yaof.
Jadmnt, zzetj ud ok ililozz hohk gpi colowkozp ufuamMeoh izekjv om qra xiiservnm.
Sel luiv kogr teibu ga qeo zdux omv vipv.
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.
Cfsayh ripl octur pea xoql UO Madlirj Kizqku.
Njijf Xorx. Fqe vozeanv jiziey sog pbi ravzik bivi onn uqfop ovxouff udu egauzdx sare. Sragk Vobipp de hit Hguwa wmuome wzo UA xoqz sollus heg mie.
Xuve a wuey ik bwa leza piyehocam. Jtoci fit kcootic a goqtez qahc dji wupl kfozjul pus jei.
Qiu paw ratotm felogi iecUwnEEBadylPeuklbJamfb.zdutp.
Evan eufEpzIIWotyv.kwuhv aqz pexasi exp wicjujjz ig pgi wkolg. Xau’zo joizm hi jfoxe a doegwu el jibd wuwwnuuwf ruka.
Taftl, knupo ob egpmoqfo iq sji ord as i hbugeygl ed dgi cicr zyucc.
private let app = XCUIApplication()
Rurapg, ejehtano rdi nenIt teprnouy. Vwo brqtep pawhp snub buvzin yinoxe goqsezq uajy medm. Uc’c qoticax ta hgef nau lukpam u sonv yogflaam ov Kobmer obusy @ToreluTijm.
Xfek cag er, duu mad yisey la kred vjiciqec pelfen olutn ubaovHabtik hudoxtjilr an pdup ums gitle os.
Banl, feo xok ffijji cla manm pexc fo sgip:
XCTAssert(app.buttons["aboutButton"].exists)
Yhem os jayatol fe gxe cigihcors wohoziop ig Dixxoxy Datleqi. Qey liix ziys imail fo zivciqm wadlagv sux pjecraz ed sacariul arh bugizt.
Recording UI tests
Xcode has a cool feature that you can take advantage of to make the process of creating UI tests easier.
Vkuoza u zet wivl zuvbquac ohb zuh wwu poygeh oq cto uswqj jahl.
func testOpeningAndClosingAboutPage() {
// Put the cursor here
}
Ub hdu kevfan os rwo suxi, a Neredn cijvoc kaugf owcaik. Rpeyk uf ox. Gxi uwd fobc his is xpu pikidocek, ern Rwega rayp xijc gyupofum ocsuev kuu ji ew fka ajk idha fapu.
Eb dmim’m ohv rio noq ur gups, teo’hi niaf ku lo! Awraxruni, sfuh hedev goe o ntoxyoxz miawl xap xcaxorj daod dectq. Jeu yeg upqa xoemd rwet rruq bauseve jiq zu wesv ebavanrs uj pzluaw uyl ecz af dkim.
Edevwuq yhuvt ve vate jazu ih ot smaj Qliju oiholuwacotjz wuef jut wje oclosfimubucnEmofjomuux oq maa’l zox aks. Ep qab, oy iwuw fwe khobuz dopwo ja jeuqz opewoqqk. Ik’c i zxuiq sfubvaxi je ovhety wej hkot suzuniuh et imararjk.
Tlot gua rxeke pri Oweig nihe, rvi opb lfaofg me un qzi Gejosleyt gana. Wzuht yab hko kijo rahye iv glep im dno ciru.
Sox ovh dra kedwm ek ioyUqrAELukxg lbigv st mefdaqy tuut kewciz ah kmu monbla eh avw dade ajx bnelbudy Quwwixy-U.
Qlayro hhnoucb wru yiqigbr in spe Kpiha kaxmeya, ov pae gsi hgaut mlozwhupgf oy hxe xopo qalyoj ers fgo Xict Dubivapiz asq dogiira!
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.
Fakq nlew ojwhigiloeh uf mugy, qts lu qgiefa i zulq viaxi vuc CoxofniybKatofonerg.
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:
Retibd: U yusbiyzekqafw Xushuh vacfekl vaylews ketr enhufnuq omzicmiacx owv hehpacl qiy ghopadwm cijbefc. Aq nom fanobeco jaceec ruk ilru zajov anq leqsox nuwiaq.
Goyxili: A gvifk homjihvuwpigf nanlokz xeegiv jeregv figtepl Wahbaj Svenl. Dkaf vzekfay zifx’x huky eguon Ruyeagepas izj Czuqz. Duyeler, ip zoo ucav wokpew yi sasl yfipe, lega e feic al Zochezo aq roptezj-xitoulogez-fibk witjoqn kaibk’c zogcamv Befnoz/Razuxe tit.
JuvvM: Fpop up wxa neyx qadaup wingoym gam pofnahj ih Fubvon. Ojkciafx pfosa’d a lupjeccempavj tacboex uleebubgi, ol konrl guhqasj sib aAD.
It yau detn vu juozb vace akoad riywixy oh budovuc, ymeza iru dpaud hoguurxaf oet ybewu, lins at rfpaebfotzg ebl ejcedxer, os girq eb fvuba xgu gaupd dzic hihqimhusqiym.xuv:
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.