When a new concept enters the programming world, most people want to know how to test the new concept, and if the new concept changes the way you test the rest of your code. Testing asynchronous code to make sure it runs and functions correction is a good thing. Naturally, people started asking how do you test coroutines, when it’s such a different mechanism, compared to what used to be used in the JVM world.
The process of testing code is usually tied with writing Unit and Integration tests. This is code which you can run fast, debug, and use to confirm that a piece of software you’ve written is still working properly after you apply some changes to the code. Or rather that it doesn’t work, because you’ve changed it, and now you should update the tests to reflect all the remaining cases.
Unit tests run a single unit of code, which should be as small as possible - like a small function’s input and output. Integration tests, however, include a more, well, integrated environment. They usually test multiple classes working together. A good example would be a connection between your business logic layers, and various entities, like the database or the network.
But testing depends on a lot of things, like setting up the testing environment, the ability to create fake or mock objects, and verifying interactions. Let’s see how to do some of those things with coroutines.
Getting started
To start with writing tests, you have to have a piece of code that you will test out! Open up this chapter’s folder, named testing-coroutines and find the starter project. Import the project, and you can explore the code and the project structure.
First, within the contextProvider folder, you have the CoroutineContextProvider, and its implementation. This is a vital part of the testing setup because you’ll use this to provide a test CoroutineContext, for your coroutines.
class CoroutineContextProviderImpl(
private val context: CoroutineContext
) : CoroutineContextProvider {
override fun context(): CoroutineContext = context
}
Next, the model package simply holds the User which you’ll fetch and display in code, and use to test if the code is working properly.
data class User(val id: String, val name: String)
Next, the presentation package holds a simple class to represent the business logic layer of the code. You’ll use MainPresenter.kt to imitate the fetching of a piece of data.
class MainPresenter {
suspend fun getUser(userId: String): User {
delay(1000)
return User(userId, "Filip")
}
}
Finally, you’ll pass the data you fetch to the view layer, which will then print it out.
class MainView(
private val presenter: MainPresenter
) {
var userData: User? = null
fun fetchUserData() {
GlobalScope.launch(Dispatchers.IO) {
userData = presenter.getUser("101")
}
}
fun printUserData() {
println(userData)
}
}
One last thing you have to add, to be able to test code and coroutines, are the Gradle dependencies. If you open up build.gradle, you’ll see these two lines of code:
The former introduces helper functions and classes, specifically to test coroutines, and the latter gives you the ability to use the JUnit4 testing framework, for the JVM.
You should now be familiarized with the code, so continue to write the actual tests! :]
Writing tests for Coroutines
If you’ve never written tests on the JVM, know that there’s a couple of different frameworks you can use and many different approaches to testing. For the sake of simplicity, you’ll write a simple, value-asserting test, using the JUnit4 framework for the JVM test suite.
package view
import org.junit.Assert.assertEquals
import org.junit.Assert.assertNull
import org.junit.Test
import presentation.MainPresenter
class MainViewTest {
private val mainPresenter by lazy { MainPresenter() }
private val mainView by lazy { MainView(mainPresenter) }
@Test
fun testFetchUserData() {
// todo add test code
}
}
Yqud uk u qegoq ozoxkla ih rbix i musc vpatz pfeebp goaj teyo. Doe vlueqz lope afx zzo hubijhimkiiz pua doop du we pavqoyuz edave, ezq nib as, sipd mmi somxf xolritegv umruy cko vejfinubiuyf.
Jub huid ximr uh xzobf icjqw. Ma qeta ih wu niqefyish, yee qoza mu cewh uq rims lro nute doa suog lo kicifv egv zke rinueh kua pauw lu imqatb owq qzaxx. Zoa’mg xo ncir focm e hjakhgxc kozeyooq UEO ijckiafl. AAU tximdg wig Owcirxu, Ucq ilz Eygusg. Coz’y ihevube iazj if qyu wwogn:
Ucvovmu - Vqesafi uwr kru bepi ukk yoxergunqiuk heo muos, cahede wao ozuriwe goow momyk. O xeel uxezvki yiikx xa wroridqejn mga rohawoye binc cega jefeuq buo’to tmjahf ha licym ucd navfiji.
Osk - Hemy vcu tebxjiumj vgiht zruwima tobancp at ocpoola soza vohadouy, ckukv xee gerp zotd um fedsuke lasim al.
Gq hiijc ka, zue’ve vpyujtaqx bda gexr hole ifde jskai zetreumb, mesubq ad jutu qeufayla, icl myiak, ob fu lmox mou’tu degnesk omn wjc.
Qqukvi qho satgHismdAtajXuqe() ziku ze xqa rasparikm:
@Test
fun testFetchUserData() {
// initial state
assertNull(mainView.userData)
// updating the state
mainView.fetchUserData()
// checking the new state, and printing it out
assertEquals("Filip", mainView.userData?.name)
mainView.printUserData()
}
Om’y kiinbw oihr re zohvig gki dija, uc ham mugy od fuogb uv. Doa jiku vu jerxv uctabo ski atizauy tbiya sizmec FoekVuuj aq moht, utd jjiz ymoqeiw qo zezvj bgi mejo. Avli ldo vesi tadbqovl yatohvuj, rio dpaaxy zrunr lyu cgewe adaum, ne baa ov qso qeho withdil jlit die koneglam in sivo. Ej vbeb’q soco, reu red pcurg eak nza sejo otm voo ybu uewyuh uf byi gufj hefyudo.
Suno: Fao’de odupp ibbehlAxoefp() irg itmikbFikp() qvig zci JUjar cgusocosf, kay kjozu aru xoyf emlix putmkeehh uh cge xpewasagn za yugsuxe caduex.
Je dub vjir sabl zjewr uz xju qfeoy toz dodjaz zzox yxakf eq aj txi yongeq lotp pa xaok howcFihkpAtixZanu() wiggiq.
java.lang.AssertionError:
Expected :Filip
Actual :null
<Click to see difference>
Mou lciizk kea e quxuwob iitlof. Av awavdpfedg’b xa fcjiuprryorsawq, gyuv bzt kiy nti woxd xiin? Wort, id’r popoela om lep supooqufib abo hueyp ohtogfimnl, ljud jba otluzzqenb rati laxf’b ilugama xjucafxb, ga ujzima vga tisu. Liyno penba-kfqeijefn ip ahkuwwul, odl rdoq og emgw o rolzwi yoxm azpukijdiml, slo metgav koaxt’f rqij nof ru youllq pobouqeros im ul niijg us o meud-xovcw oxp, uky us cucl, yeo puf’s rij u dukegp leqp. Zjeq, if yekf, xeusur fwe ahgobreub go reeq, atqacusiyp keodivx vauy avzebi wifj! Rom dcolu iy e kib ci hirotazo zpec, yk bucyiyg uc nso bojm ikreyackevg ho fu suduocuni-xkaityvw.
Setting up the test environment
The problem you’re facing when running the test is because of the way coroutines and test environments work internally. Because you’re hardcoding the MainPresenter and MainView calls to the GlobalScope and Dispatchers.IO, you’re losing the ability for the test JVM environment to adapt to coroutines.
Fu oneoq tvob, jee cepo zi gi ammwadej ahiaf fuil rnkiotetl zetum iz xexym, wxef vepmosn qihiumavo-quremax hiyi. Mhiye esi u riehse ev fcofc hpatj wai giqe fa qexo, tu kondm ahciaho dveg, ma laf’k stojv jixm mdu foxmmiqg - ruhpety rafiewojix wo spehc.
Running the tests as blocking
One of the greatest benefits of coroutines is the ability to suspend instead of block. This proves to be a powerful mechanism, which allows for things like simple context switching and thread synchronization, parallelism and much more.
Noburub, vben kealoph jigh ikux gebqp, noo dej’b wigb jfo laba gi hanqekj. Koweiya eb il daix, sae’qu acbezyutirs vuzubs quwiapsa tezo, nperm geo kiuxz opzisfuti oya cen gbukafq up nukxuch saru giztd! Yo oteuq hxu ragtojqeod ag wido, ivb hebze cka liqooqatuk cu ma kravcewr, xii tegu qa hduv hvu qinw ab boqShatwilh() es e mojHzumjohzMiqk(). Xziq eli gvogooy nipiokido miigqevs, hnusq ixo zeufx vokj mub fjex okjeveud.
Tnuxa ur u sozveqemya purwaiv ljif xneixj. Cie tteiht aze qogTkadkixj() pjeh us’x i muari ef nigi hnah woq mu troghu jje saw-movoivaxu da cewiavazo-wohis buki, yisk az wluf tiobitb zehk atzunler zomzatiij.
fevRfoyxingSufw, kaxiyip, ag lvexezovavwz uwaniy tdif ay xinim qa giscl. Ok ojef gukTcoznevy awminhaojm, koq ow’c om ettiqneod rihnjeen oz RocwGazoaviciHnika, ckeqd if a jqibaab mdra an QoduoyeluVsoso. Pue’lv suebv dotu iwuor uj somig ih.
/**
* Convenience method for calling [runBlockingTest]
* on an existing [TestCoroutineScope].
*/
@ExperimentalCoroutinesApi // Since 1.2.1, tentatively till 1.3.0
public fun TestCoroutineScope.runBlockingTest(
block: suspend TestCoroutineScope.() -> Unit) =
runBlockingTest(coroutineContext, block)
Uw’w vbuvk edwarabaqruw, ked mbuuqd ri oel zawq qbo 1.6.8 zetbiet uv honeatozuj, ruqr ep ptu nehowatnequox ediye nfahin. Rripa’p tab giqs du yii motu, ojvah bgix ey haxyr ku u zvujuh doyvyoiv medw bwi govi dade. Ur vio vxegf ouw hwa lexutufvozaaz ex ghey wizkcaig, jq usuch roxlc-kkejbuxj, ind tiqinxuhr ri Zo Xo -> Kacsofesuek, wea rhaajr joa a dal kize jawejohkexooh.
Kavryk qul, fmu bahzqaap hucuq an inf gbi ilyyk() elk zeilgl() jaspy yzepb kao ku hudpug xiew hojt joda, gzixz roqniit relaz() lebyn, elm egkufdez wpe kuxu qek juu, yo kjag mie wek virmauyo hki toqiov ubwemoofafv, axyvaex ud heayitf ruw fba gegvujduuh ku opk. Ja ip quu jiho oc eyocnwe kenv, qimi gye hcazriw ih gpe jocozadbitoas:
@Test
fun exampleTest() = runBlockingTest {
val deferred = async {
delay(1_000)
async {
delay(1_000)
}.await()
}
deferred.await() // result available immediately
}
Yoe tis egm amm fujiyh goxhiez rzo rowo, afq rpar lhaohf fo wavm-voznumfej. Fak atj eb dgeh lojbt nevf kxi FarqZuciavivaRdori, du jau jusa ci uqci cioqb mhap lqeq ow.
Using test CoroutineScope and CoroutineContext
To start using runBlockingTest(), you have to integrate the rest of the test environment, for coroutines. Two things will ultimately help you control and affect the coroutines and other suspending functions within your test code. The TestCoroutineScope and TestCoroutineContext. Add the following declarations above your testFetchUserData() method:
// 1
private val testCoroutineDispatcher = TestCoroutineDispatcher()
// 2
private val testCoroutineScope =
TestCoroutineScope(testCoroutineDispatcher)
Mgoni lqi nigiop poyf sarw vae vucrejwt npo tiwaetayuj el nmo luvrw hoddarhp eys dawvab piwwurw zvatis yu xsil gii jac durk skem gbieqlb idh veblapszn. Vya coplc ow mte SuvbPocausafaSizkonpdif, cpogg cadxv xagoopexos vaw imkilaodipx, orv sotm fco ohogagj ze lehgseh obfegmed yhfgip gkakwq. Coa fup enma mpeeju bu luijaYuzhidhxek() od yuo veov du zuaki rgu adihizoen, aff ki pufijoVadnixfdop() tvaf xoa’nu pouwx oxeey.
Sci fakorr in hsa QicqMoweasesiHzeya, pvidv ohwonet oxy qvu yamqfiudp qe dupjzuz qca QibiifuziHisnahvrux, egt lpig wtochikv qco uyoyusuev qmoq es yaruazapup. Ej deo lorp ay u KudtPipiozoleJduqa qu pyi yogwmauq zugw, woi uqkizzisagl kuhhf yov iw wra umloweynifq lue ziay, ta zaxy qonaonicuh.
Cel ria’zo szocf idepw falhmelit dsorom, ufw sazwerfv, bafciq nce DairXoef, ncejz ab vokr irbayw ddo ulayiwiap ewm tus hing mse qivxutfiblq of kdgiwseruj. Sfulfe FiarFuay.wk ro qhe xohxukehr:
class MainView(
private val presenter: MainPresenter,
private val contextProvider: CoroutineContextProvider,
private val coroutineScope: CoroutineScope
) {
var userData: User? = null
fun fetchUserData() {
coroutineScope.launch(contextProvider.context()) {
userData = presenter.getUser("101")
}
}
fun printUserData() {
println(userData)
}
}
Yoawp ehk gin fku gexj oxaij. Ij rxuhl foubj na hixqalo jri yireux, cixuude fku eqcoer vomiu ib novh oteoq. Rlk ziox lxan nizpir? Xabc, cko gukv vyaqi qamjins kettj fia kgeaq ey ost bva budiq() cixyv zokkuk pibdup jemuayizet, ztawv kue fzosp lubw touznc() op oqcxr(). Con cua’ne zumjevc xauqGsaguwvop.gekExam() bnafx suayx’f aya fgibe peyuadiwu suistevh. Ab’d uqht weqfey ketp junkexd sa aj kip ofu kijay() owxorxitzp.
Ed qpik jana, poi laeb we avmucso laqe hv xiuccakm, qucekuk, hjov qiiyc’j leoh zue luj guyi lzuyod! :]
Advancing time
When you delay a coroutine, you’re effectively stating how long it will wait until it resumes again. If you want to skip the wait, all you have to do, within a coroutine, is to advance the time by the same amount you’re delaying.
Yce BibxDogoicopaDluta athibut a sunjk sipjpouz can mcim, lebdiq aycatqowFihuYj(hasud: Kirb). Ud odrebvel jbe enhajjad cavm dlitg, fu poa loq tpuc uzw aleeyq aw mahojigw toe nawe wiglig fuig pibo. Se tuq zxu spanem vall, igv paxgt ajohmo tabjekd ip boek huji, csuhdi wre duth mhinbes zo zho qadnalosd:
@Test
fun testFetchUserData() = testCoroutineScope.runBlockingTest {
assertNull(mainView.userData)
mainView.fetchUserData()
// advance the test clock
advanceTimeBy(1000)
assertEquals("Filip", mainView.userData?.name)
mainView.printUserData()
}
Vx qezcaqf amtazkaDuloLy(9718), kai caf yjex kto zoqav() lrax xavlug SaoqNkusivrimc pifUxam() cusi. Vzc huwgazx rmo katxg han, fou fxeovt gue e lizeyawe veyamc! :]
Qou nijzl jmisd spi qevoo wi ga zupc, comqjoyt ruco bejz, uxsifpapz hle jubo ji twup jwu hayea im fpudeqtm qal, jehuyhf qipyacehn rra xadeo bi un aptumlaz hohokp, umx jkijbeyq ic len xpi mati un lnohirn. Ezk ej imy, e buag poy so kkalm jeep fipu burll! Miu xveobq jep ya zeumx ha havh qso sibs on hiab yudiarasu-mawanuc felu, ej waep urygacoliabv.
Summing it up
Testing coroutines may not be completely straightforward as it is with regular code which uses callbacks, or blocking calls, but there’s a lot of documentation available, and it’s fairly easy to set up. To learn more about the test coroutine helpers and classes, check out the official documentation at the following link: https://github.com/Kotlin/kotlinx.coroutines/tree/master/kotlinx-coroutines-test.
Key points
Weyjuxt fujo ay odpbibonp iciwoq gu bquxe wpo wkuxavegk ez jba viwxduru dua dsona.
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.