Tech Showcases,
Developer Resources &
Partners
droidcon News
Unit Testing Kotlin Channels & Flows
By
Mohit Sarveiya
droidcon Americas 2020
Unit testing Channels and Flows can be a challenge as they are fairly new. In this talk, I will share with you how to implement and test practical examples from my experience. These examples are testing delays, retries, and errors. I'll also share testing more complex examples such as polling. For each use case, we'll look at how to use features in the coroutines library such as runBlockingTest and TestCoroutineDispatcher. From my journey of using and testing Flows in production, I'll share the challenges I experienced.
Transcript
English
00:10
hi my name is mohit cervea i'm a kotlin gde and my presentation is on how to unit test curritins so i'll go over in this presentation different use cases for testing flows i'll go over how to test errors delays and all in utilities that the la the library care routines library provides i'll also go over some common testing problems that you may encounter as you start to unit test so let's start with a very simple use case suppose you were building an app and it was communicating with some api service and you made a request to it and you got back a stream you got data back as a flow and suppose you had a use case where it was a flow of locations so how would you go about consuming this and testing it so you may have you have modeled this as a flow locations and you'll probably have some business logic in which you're taking the stream of data and manipulating it processing it so you may have some business logic in these operators filter map flat map etc so how do we test this how do we test the stream uh these stream operators so the core routines library has a lot of modules and one of them is a testing module which provides utilities for android needs and jvm needs etc so this is how you could include your test implementation this library and your in your app so first thing you need to do in order to test your current team is to in order to test your flow you need to create a core routine and the way i'm going to do that is by using a method that they provide called run blocking test and what run blocking test does is that internally it basically creates the occur routine on some specific scope so as you know you always need a scope that you want you need to create your career routine on launch on and it creates a scope for you internally and it also you the second thing that you need when you create a call routine is a dispatcher so it will create a test dispatcher for this career routine so what gets executed inside of it is basically your logic that you your test gets executed in that core routine so this is this is what it does what run blocking test does at a higher level overview so in my flow first thing that i need to do is to mark out basically my flow of locations right in some way and in order to mock out the data i could use makito or i could create a fake could do you could use either one of those options so the next part is about you know you want to collect from the flow in your tests and have it go through all of the operators that you have defined in your business logic so what i'm doing here is that i'm using a terminal method called to list two lists the way that works is that it takes the flow that you're testing and it takes each emission and goes through your operators that you are using and it stores the result into a list one by one by one and now i have a record of everything that my flow has submitted and i could do verifications on this list so i could do a verification like this do i get the locations that that i expect you know from from this flow after he has gone through all of the operators that i have applied to it so this is a very simple test over here of collecting from a flow and testing that it emits what you expect it to emit so let's look more deep into how run blocking test works it helps you and it will help you to understand as you write more tests more advanced tests so how does this run blocking test work internally so what it takes is the test that you're defining it takes it in and what it's going to do is that you could look at what run blocking test does into three different sections first it sets up your test and it does so by creating a scope and you need a scope to launch occur routine then it runs your tests and it does so asynchronously as you can see they're using something called a deferred so you could see there your test it is wrapped around scope.async and then what it does is that it advance it calls the medical advance until idle which i'll go over later in my presentation which is used for going past any delays that you may have in your test it also captures any exceptions and surfaces of them to you so lastly it performs a cleanup stage in which it cleans up any core routines that you may have created and if there is an area it will tell you that was there was a core routine that could not be you know cancelled or cleaned up and i'll show you different use cases where this may ha may happen so right there we have three different stages three different steps you know what run blocking tests what works how what it does so the next thing is that so this is how this test works in a simple manner but we can make a lot improvements on this simple test so the way in my test i'm collecting from the flow and reading from it is by calling this terminal method called to list but is there a better way to do that so in the community there there is a library called turbine by this cache app team by square and i want to acknowledge the you know the engineers who open source this this is a really great library it's a very small library and it makes testing flows really fluent this library is based on a pattern that was in the sql delight library which is another great library so let's look at how to use turbine to improve our tests so the way turbine works is that it takes a flow that you're testing and you which has which has a stream of data that that's going to emit and it creates a channel in which it's going to store the stream of data so a channel what is a channel a channel is a way for one core routine to communicate to another it's a if you're familiar with rx java it's similar to a subject so you could send something to it and you could receive something from it you could also cache data in it if you want so there are different types of channels that you could use they have different types of behaviors turbine library creates an unlimited channel and what an unlimited channel is that you store it as you send data to it it could cache it and the cache could increase dynamically as you send more things to it so what is stores inside of this channel are events and when it goes and reads from your flow that you're testing it stores these three different types of events a completion an error or there was an item that was emitted so the first thing it reads the first item from the flow and it stores it into the channel and it does so using the item event as you can see here it encapsulates whatever was emitted could be a string some data that you want and it does so like one by one one by one and then lastly it will store a completion event which marks the end of your flow that everything was emitted from your flow but what if there was an exception while you were reading from your flow and your test it will also store that you know as an event here as an error event and it encapsulates the exception throwable that that that occurred so you know i now have you know in my tests when i read from the flow and everything is stored in this channel what you have here is a record of all the events that that happen well from your flow and now i could do verifications on my tests against this so the way you could do that is that this library provides you with an interface to be able to read from this channel in your test and it's done in this interface called flow turbine and it take and gives you the ability to know like how long did something take to emit it also gives you the ability to to determine whether the flow didn't really emit anything it was empty it also gives you the ability to fetch you know something that was emitted by the flow from the channel and you could do an assertion on it it also gives you the ability to determine if there was an error from reading from the flow or the flow actually just completed you know and it there's nothing else you know that that's inside of the flow that the flow needs to emit so in my test i'll show you how to use these methods you know to query this channel so internally what this library provides is this test extension a test extension on the flow that you're going to use to read it and what it does internally is that it creates a car routine and the car routine that it creates inside of it it has a channel that's going to store events and so it launches occur routine and it starts to collect from the flow read from the flow one by one and it stores it as an event in this channel after it has done that when it completes it will store the completion event so the way in your test you could access this channel over here is by utilizing this interface this interface has methods that give you the ability to do assertions on everything that was emitted by the flow the events that occurred so rather than using a two list what i'm going to use is turbine and with with turbine my location data that gets that's in this flow one by one is going to read it and store it in this channel then i'm going to query it so first thing that i'm going to do is call the test method which in which turbine begins to read from the flow that i'm testing one by one each item and then i'm going to get the first item do an insertion on it so expect item graphs the first thing that was emitted from the flow and i would do an assertion it was the first location that i expect then i'm going to call expect item again it's going to give me the second location and then the third location so one by one by one i could pull things from my flow and do assertions like this and lastly i need to verify that my flow actually completed so i'm going to call the expect complete method and what this now you have this is the this is how it looks with turbine the benefit of this is that it's much more fluent way to test your flows provides a really nice api and i really enjoy using the turbine library every day so so this library also provides really good safeguards and the safeguards that it provides is that suppose there was something emitted by the flow but i didn't do a assertion on it so it will give me an error suppose in this example i did an assertion on the first location the second location but not the third location and it will give me an error but you missed something you didn't read it from the flow and do an assertion on it so what if also i missed verifying that my flow actually completed emitting everything it will give me an error that you had an unconsumed event that you didn't verify against so that's this library provides these safeguards that really help you in your testing and this is how my test now looks using turbine so this is a really great library to use so the next thing i want to talk about is errors so let's add on to that knowledge and how to look how do you test errors so in your there's a possibility like where in these filter operator any in this stream there's a possibility that an exception occurs so what happens when an exception occurs in these operators this flow will basically complete if an exception occurs and it will complete and you'll see this on completion get called so how do you go about testing that what are different ways so if an exception occurs like in your test or turbine you're reading it what's going to it's going to intercept that exception it ha it and it does so with this event it will store it as an event whatever exception that happens and the reason why it does that the way it does that is by when it reads from the flow you can see in the collect here it wraps it in a try and catch and in the catch it will basically store in the events channel that you got the error and then it will store that event that you could fetch so i could determine whatever error that occurred using expector which will give me a particular type of throwable so this is how you could handle errors with the turbine but there is a better way to do this is there a better way to handle exceptions what you one thing that could happen is that you know you it could cause causing your app to crash so there is a better way to to handle this and the way a better way to handle exceptions is to use a catch block so in the catch block if there was an exception that occurred upstream anywhere in these operators the catch method will get called so when it gets called what the catch operator has is the ability to admit something when an exception happens from your flow it has something called a flow collector so it also gives you whatever exception that occurs so you could log it you could track it some way and in order to better handle exceptions one pattern you could use is this result type in the kotlin library this result type is very much like either from arrow it encapsulates some some result whether it was a success or a failure it provides methods to determine like if it was a success or a failure and i could use this i could use this in my catch operator like this i could say whenever an exception happens from my flow emit basically a result of failure in the success case i can redo result.success so i have some wrapper type that i'm using in my flow so this is a this is one way to handle exceptions and flows so now when an exception occurs it goes into the catch block and will give me a result of failure so how do we go about testing this with turbine what what changes so now you know when the catch block gets called it gets executed turbine will store in this channel some type of event but it won't be an error event it will this time will be an actual item event it will store basically whatever the result which is was the result type that i have specified so now i could go into my test and i could do expect item which will give me the result and i could verify that it was a failure or if it was if i expect a success it will i could pull out the value from that result type so this is one way to handle exceptions uh this is one way to test exceptions so the next thing that i want to talk about are delays so testing delays is the most tricky part about testing care routines and uh we'll start off with a very basic simple example here what i have a car routine using run blocking tests on creatine current and i'm calling it method process data process data is a suspending method and then internally it delays for one second and then before that after that it basically performs some calculation so how is this delay handle like when you were if you were to test the suspend function what happens how is this delay handled so in order to understand that we have to go back and look at how run blocking test works when we look at run blocking tests you can see these three different sections so look at the middle section when you're of run test you'll see that there is this method called on the dispatcher called advance until idle what that does internally is that it handles you know going through you know the delays that you have specified in your test that's in the in what you're testing so it will advance virtual time forward uh on all on all of your of your delays so what happens when that gets called is that then at the end of your test it will basically proceed forward to you know do to perform the calculation so that's how this is handled in this scenario but what if you had this delay not in just the suspending method but inside of a curve routine i'm launching current inside of this method process data and it has what i had previously the delay and the calculation how is this handle so when you look at this i have a method called process data and after that i'm performing some assertions so this is the most trickiest part to understand that that really cut that really gets you know and as you're testing it will basically uh something that will come up so the way that how is this delay handled now on this when when it's inside of a curve routine so what will happen is that when it hits that delay this career routine will suspend itself and what happens is that it will basically not go forward after the delay and do the calculation but the perform assertions logic will basically execute and but this is not the intended behavior that you may want and i'll show you how to control delays but and the reason why that happens is that it goes to this line advance until idle at the end of your test then it will basically go forward past that delay and do the calculation so this is something that that is tricky uh so you want basically better control of your delay you want to go past the delay perform the calculation before you do the assertion so how do you control delays so in order to do that the car the core routines library provides you with basically this utility and this is an interface and it has the ability to control virtual time you can advance a delay by some number of milliseconds on a dispatcher you could also pause the dispatcher etc this is really useful for testing delays so the way you could control delays i want to go past that delay that i have in this process data method and do the calculation before i do my assertion the way i could do that i could call it advanced time by right after it by one second so when that gets called it will go past the delay and then perform the calculation and i'll get the expected behavior that i want it'll go and perform the calculation then i could perform any assertions that i want so if you have any type of delays you know in your flow and your care routine using advanced time by is really useful so another use case where you may have delays like in your flow and your stream is when you do retries suppose when an exception happens and you want to do a retry there is a method that allows you to do that there's an extension it allows you to specify how many retries that you want to do one two or three and inside of this logic what you could do is that before you retry anything upstream i want you to delay by some number of sec milliseconds i got two seconds here and then you could also specify when do you want to retry the you know you're upstream well you may want to recharge only on a particular type exception you could also specify that here so when you when this delay hap after the delay will do retry everything upstream all everything in your operators that you have specified so one thing to help you to better you know test delays etc is to use a dispatcher using flow on so what flow on is very similar to subscribe on like in rx java so everything above stream runs on that dispatcher that you specify with flow on on the delay will also run on that dispatcher etc so when i go about testing this like i may i will be able to inject a test dispatcher in so how do you go about testing this so i'm going to create a core routine that i did before using run blocking test and then what i'm going to do is basically create a custom dispatcher above my test and i'm going to inject it in using di into my flow so that's as i'm doing over here so the next thing that i'm going to do is that i'm going to say this run blocking test to not create a dispatcher by itself i want you to use the one that i created here and i could do this by using an extension so there is an extension on run blocking tests there's an extension on test curating dispatcher that allows you to specify that hey run blocking test use my dispatcher don't create your own that you could use so next step is that i'm going to start to collect from the flow that i'm testing and suppose that using makito or fakes you have specified it to throw an exception so when then so these call operators happen an exception happens and it goes into the delay it will delay for you know this amount of time and then to go past that delay i could say advance time by go forward and then when it go when you when it goes forward it basically retries the flow again and and if that fails it will ultimately go into the catch block so then i could perform any type of assertions that i want so this is a way to handle like delays in your in your flows so uh something useful so that's how you test delays and that's the most tricky part about you know testing carotenes so next thing i want to talk about are some more utilities this is will be helpful for testing care routines in the view model so suppose you had basically a use case where you're you know reading state from your view model and your view is rendering it and this could be modeled in different way in shared flow state flow etc so i may implement my view model using jetpack compose is japs are not jetpack jetpack is v model class and what what's useful about it is that it provides you with a custom scope a scope that you could use and it sets the job on its supervisor job it sets the main dispatcher on it so this is what i could use to launch care routines in my view model so how do you so the way you could you know model that stream one way to do it is to use immutable state flow and which i'm going to push like states state updates to that my view could render view or some other component so i may have an update state method in which i'm reading a flow of data could be from repo database etc and i'm mapping whatever it emits into some state change as i'm doing here so every time you know the emission happens i'm going to update that state update the mutable state flow just like this i'm doing it states that value equals something some state and i'm going to do this inside of a curriculum i need to launch occur routine to collect from my flow of data and then to update my immutable state flow this is a very simple implementation so you could also do this by using collect so inside of the collect i'm updating my mutable state flow so now the reason why i don't i use mostly launch in because collect is a suspending method any logic that you have after it will block until like until the flow of data emissions complete so that's why i like to use in my view models you know launch in and anything outside of that in my view is where i would use a collect so that's a good practice to to follow i found so how do we test this in order to test this you could create a curtain like this i'm going to use makita or fake to mark out the data that i'm reading in my view model i'm going to launch the code routine by calling update state to read from to update my state and then i'm going to read from this the states flow and do verification of what i expect the state to be so when i run this test i'll get an error i'll get an error because you know it uses this the view model uses dispatchers.main and i need to be able to set a custom dispatcher for it in order to do that they provide you with so i need to set a custom dispatcher for this main and they provide you with the utility to do this called uh dispatchers.set main method and i could use this method to basically specify some custom dispatcher that i want to use and uh so in my setup method that's what i'm doing here and then when i release when my test gets teared down i'm going to reset it this is something you would want to do for all of your view model tests and when you look in the community you'll see that you know one way to extract this out into a reusable rule is to create a test coroutine test rule in which i'm giving my test dispatcher my custom one it has two methods you'll rise starting and finished in which i'm setting the main dispatcher so i could reuse this rule for all my view model test now so the way you use that reuse that is by using a rule using this annotation at get colon rule it allows you and you specify your coroutine test rule so and then when i run this test now you'll see that it passes so that's something that's really useful for view model tests so there are a couple other things the advice that i want to give for remodel testing that suppose i in your view model you're ingesting some class and it has a channel in which things are being posted to it's made want to make sure and you're on destroy like you close that clean up that channel you close that channel otherwise you know you may have a memory leak or you may have other types of errors that come up so it's very important to write a test for that also that you're cleaning up by closing any channels that you created so i want to go over now some common testing problems that i that i encountered that you may encounter so one thing that come may come up as you start to run you know start to test things is that when you look at how run blocking tests work right at the end when it goes to clean up you know any core routines you have in your test you may get this exception where you have a job you have a care routine whose job is still active why is it active it could happen any number of reasons and the figure you have to figure out which curve routine that you were like running in your test that was that's still active so number of reasons why it could happen is that it might be because of a delay it might be because of you're using a particular type of channel that has a behavior where it will basically keep your keeper routine still active so let me give you an example suppose you had a channel like this and it has a channel in which you're posting events to and i'm using a rendezvous channel which has a behavior that i send something to it and it will wait until someone reads it reads the data so i could have a current team like this in which i'm sending an event to age this channel and i have another suspending method in which i'm reading from this channel so when i go how there's an error that could happen when i go to test this so when send event happens when i call in my test it creates a current team and it has a rendezvous channel and i send something to the channel some location event some event and when i send something to it it basically because it's a rendezvous channel this if this car routine will get suspended it will suspend until something actually or reads from this car routine so you have a car routine that's suspended and what's going to what it's going to say is that you have a core routine that's still active that you need to clean up and uh it will give you this exception so this is a standalone car routine so you can see in the error message it says curoutine number two protein number one is the run blocking tesco routine so core routine number two you could also like give names to your curtin which will make it really useful in debugging like these exceptions in determining which one is still uh hanging so in order to fix it fix this you could you know call the other method to process to read from that channel so this is you know one one case where a simple case where where it could happen problem could happen another problem that where this could happen is that when you go about testing delays i'm at a delay i have a delay here in this care routine and send event and when i go run this test when i call the send event method in my test you can see google say that you have your core routine made by a send event method is still active and the reason why is active is that you have been like advanced time forward virtual time forward on that dispatcher you could fix this by doing test car routine dispatcher dot advanced time buy and so this now now your tests will work so this is something common that also you know happens one thing to make sure here one thing to keep in mind i didn't specify you know like i did before like routine dispatcher.run blocking tests in this case so those are common problems that you may run into so lastly i want to talk about how does testing work inside of the core routines library itself it's very different and the reason i want to talk about that is that if you want to learn about core routines you can read the docs uh also you know listen to many great presentations and also the third method third approach is that to look at you know the test itself so if you were like going into a new code base you might look at the test to understand the functionality so testing is also useful for learning and it helps it's helpful to learn about the behavior care routine it's also a multi-platform library and shows you they have a set of multi-platform setup to be able to test in these different platforms so here's a very simple test that i'm going to show how you how it works and the way the what is this is testing is that i have a child job that i created i cancel on it and i call join on it but i want my parent to still keep executing that's what i'm trying to verify here so the way this works is that every test using the library implements this test based class or inherits it and the way what this class works is that it has a method called run test so inside the library run blocking test is not used it uses run tests to create core routines and this test launches a parent routine i create a child job and i call cancel on it and the way order of verification is done like assertions are basically using this expect finish pattern in which i'm using a counter to just verify the order of execution and this is this is commonly used in the library so what you'll see is that this test-based class because it's multi-platform it's it uses the expect keyword and there are implementations for each of these platforms jvm js and native and uh and you'll see that different ways in which like those these different methods are implemented for these different platforms so the way for jvm when you call run test it creates a car routine actually using run blocking for js this will create a curtain using globalscope.promise so you see that different implementation for those different platforms in this framework so the way the expect method works is that it's used for verifying order of execution it has a counter that you supply to one two three as you as you as you go about implementing your test and it has internally an atomic integer that gets incremented every time you call it and when you call it it basically verifies that you didn't do like expect one like multiple times so you make sure you're verifying order of execution like sequentially and the finish method works very similarly where it has an atomic boolean set that gets set and it verifies that you only call that once at the end of your test so that right there is like the framework the framework that is that is used to implement testing inside of the core routines library so this test right here is so i wanted to share that to be able to understand how these tests are set up this is useful if you want to learn about behavioral proteins by looking at the tests themselves in the library i found it personally useful so lastly i want to say that um a lot of the things that i shared in this presentation i wrote a blog post about it i have a website called codingwithmohit.com you can see the link there and be sure to check out check it out might be helpful to you i also wrote a blog post about how the testing pattern works and i share tests how test works how actor test works and different types of tests internally how they work and this blog post here um here are some links that i'll share out in my slides on presentations i gave before and blog posts are written on testing from from my experience and and my knowledge of it and uh i hope this presentation was useful to you testing is it's important uh so is equally important as implementing something and using flows so thank you it's time to take some questions from slido are there any other any are there any projects that showcase or more in-depth career teens testing there are many libraries i mean sql delight has you know so so many tests you know you could look at that i know in the community there are people who have example apps that have testing in it there's you know there's an app actually i was just looking at an app called duckduckgo which is uh open source it's on github which uses carotene and has curoutine testing in there so those are there is there's a library called grpc kotlin which is uses curritins and it has testing in there you could look at same thing recorder plus library grpc uses cruising curitin that has you know tests in there that you could look at so any more questions anyone
01:22
um
02:34
next question that we have how do career teens enable flows channels so the way that they enable it is that so you know when i set up when you set up a flow and and you want to be able to to read from it or cancel it and you have to call collect on some curve routine that you created on scope flow dot cancel is deprecated so the way you know you could cancel you know reading from a flow by canceling the curosine and what you're running it running in so it has so when you look at you know the channels internally how they work is that they use a uh a double a linked list to be able there's actually a paper written on on that and this is based on actually this is a good example about academia and industry work together to enable that that functionality and uh so those are all the things that you could look at internally in the current teens library when you look at the tests so i hope i answer that question to the best of my knowledge
03:46
well thank you for coming to my talk please you know follow me on hey it's mohit on twitter and please reach out if you have any questions thank you
droidcon News
Tech Showcases, Developer Resources & Partners
EmployerBrandingHeader
jobs.droidcon.com
![]() Latest Android Jobs
Kotlin Weekly
![]() Your weekly dose of Kotlin
ProAndroidDev
![]() Android Tech Blogs, Case Studies and Step-by-Step Coding
Zalando
![]() Meet one of Berlin's top employers
Academy for App Success
![]() Google Play resources tailored for the global droidcon community |
Droidcon is a registered trademark of Mobile Seasons GmbH Copyright © 2020. All rights reserved.