Testing Coroutines on Android (Android Dev Summit '19)
I am Sam McClelland developer advocate for Android I'm gonna be working the Android developer relations team and we're gonna be talking to you about testing
co-routines but before we talk about testing carotenes let's talk a little bit about co-routines so at i/o we talked about how we're gonna make Android UI cover teens first and what does that actually mean like what practically does that change about what we're doing well we're building the Android UI okay so fundamentally what that means is as we're building new api's for Android we're gonna take a look at whether we can fit every teens into them and whether that makes sense and provide a good curry teens support for the api's we're building we're also as we build jetpack libraries we're going to use co-routines to build those libraries we're already doing that with some of the jetpack libraries we're working on now so you're gonna start seeing every teens shipped in Colin first jetpack libraries additionally we're gonna write documentation and have the documentation up on developer.android.
com to explain how to use care routines and how to use coverage changes architecture components and other other parts of android so at i/o we talked about a bunch of different libraries that we're working on and since then four of them have made it to stable so that's awesome so we can you can use work manager retrofit room view model scope these all support co-routines out-of-the-box in a stable version the live data builder lifecycle scope and when started are all still in a release Canada state on the train dis table and Collin X every tease test the library we're gonna be talking about today is still experimental and it's on the train to stable as well so to zoom in on testing every teens we need to define an application that we can actually test so I'm gonna go ahead and walk through how to add co-routines to an application that uses only room just to kind of keep the app simple so we can focus in on the testing it's gonna be just a simple to do app that stores strings and you can mark them as done and I'm gonna store this in a room database and to do that I'm gonna need to define an entity like I would with any room database and there's no co-routines in your entity it's still just an object you know that holds a row for your database the we're gonna add curry teens over in the DAO of our room so here we can see where we're gonna start integrating care routines into kind of this room flow we're gonna go ahead and add a suspend modifier on one of our room queries this time we're gonna insert it with the add item and that makes this function main safe it's now a suspend function and reims gonna run that query on a background thread and it's gonna run that on a custom dispatcher it's actually gonna be the same executor that room uses if you use live data and it's gets this other really cool super power of its cancelable so if the co routine that calls it cancels it's now canceled all the way down we're also gonna do the same thing we're gonna make this suspend function for fetching all the items here as well you could use flow for something like this as well but I want to kind of keep this example simple so we can focus in on the testing part then going over to oh I have a little bit of code up here that's kind of questionable but I wanted to show real quick it is in fact main safe you can make questionable architecture choices and this is this is now technically correct which as I like to say is maybe the worst kind of correct so we can go ahead and move on we're gonna make a repository that uses our Dao so we basically have to now make our first API decision we have to like actually figure out how to use go routines here and one option that we have is we can make it a suspend function like this and that's very similar to what we did before or we could return a deferred right here this is kind of like a promise or a future if you're familiar with that but basically it's an object that lets you say I would like to get the result of this computation later and these are two different ways I could write this API and I have to make a choice and decide which one I'm going to do here and if I compare these two they're similar in a lot of ways and there's actually some big differences so both of them basically require a suspend function to call them when you call the deferred version you don't technically need to be in a suspend context to call it but to get a result from it you later will the suspend version does support that auto cancellation feature because it's like super cool and awesome and like magical carotenes Kotlin stuff the deferred version does not both of them are main safe like there's absolutely no difference in the thready behavior between these two implementations but the threatening behavior does get quite different when we look at how we get the results out so deferreds have this callback called invoke on completion which just gets called on any old thread you have no control over that and it becomes really difficult to actually use that to get a result out if you're not in a suspension so generally in Kotlin you should prefer to expose suspend functions just as many times as you can like that's just the place where you should kind of default and then we're gonna go over to our view model and finish out this flow for adding an item to our to-do list so the view model is kind of a natural owner of this work because it's the thing that kind of owns the work that's happening so that's why I'm gonna actually launch the co-routine this launch call right here actually creates a new KO routine which then allows me to call this as Ben functions that I just created and then I can use the result right away right right here without having to define a callback which is kind of like the super power of two routines right there so there's kind of three basic rules that we like walk through as we were going through that code so as you're designing code with care routines like prefer to expose to spend functions as your primitive API choice like try not to return a bunch of deferreds or or build complicated interfaces that are harder than that unless you have a really good reason to go for other interfaces have whatever the natural owner of the work is that like kind of contains the the lifecycle of that work be the thing that launches it and I'm just kind of like learn to trust that main safety is gonna work this is something I've seen a lot of code is people like come into co-routines and they'll like start like with context team like five times on the way to actually calling a database call it's just like trust that it's gonna work and you know it does so that's that's all I really have to talk about for this basic courtesy has to determine well who's gonna talk about testing thank you Shawn testing is an integral part of the app development process but I don't want to spend that much time about it so TL DR test your code webinar focus on unit testing in this talk so how can we define a good unit test good unit test should be fast you shouldn't have to wait for it for you to fail or pass and it should be reliable always give you the same result there should be isolated as well so execution of JUnit test it should be independent from each other and obviously after the test finish is no that work should be run so as we said we are going to see how to test coroutines and the code that we show before so when testing quarantines I would like to ask yourself is the test that I am creating now through reading the execution of a new culture if that's the case it's because you are likely calling launch or racing in code under test that's not the case is because you are prolly testing a suspending function that doesn't wait on you according and if nothing of this happens is drawn a testing code in so we are living without so they broadly full a test is probably going to fall into one of these two categories so fYI as shown said before we're gonna be using that test the code next go routine test library and it's an experimental keeping up to they should be a relatively straightforward on the road to stables wouldn't worry that much about it cool so we're gonna see how to test to span function now specifically the repo layer triple layers we said is supposed to expose to spend functions now we're going to say this insert to do which is basically adding an item into the doll how can we test this with the suspend function needs to run inside recruiting and for that we can use the method run blocking test which is it a method from the test library that we just mentioned from making tests gonna create a new color team and it's gonna allow you to execute to span functions immediately and you may have heard of run blocking in the past what is the difference with run blocking test well run blocking test is going to escapes delays so you don't have to waste extra time in your tests how can we use from blocking tests in our tests basically you just have to grab your test body inside this method from blocking test and that would be it so if we go through the test we will see we are within our subject our repo then we are calling the suspend function insert we can do that because we are inside a corrodium and now that inserts to the functions will not be executed immediately so therefore now we can assert that the item it that was easy right things get complicated now with tests that are going to trigger any incursions so we're going to focus on how to test the B model layer so now here we have add item which is a regular function it's not a suspend function that is going to create a new corrodium this is executed with blue modulus code which actually uses the dispatchers that main as default dispatcher here we are just calling just to simplify everything calling the repo with a text something we want what can we use the technique that we just showed before so just wrapping our test body inside run blocking tests so if you do this it is going to fail why we're going to see this in a second so if we had to be realized what's happening in the different threads they will say that the test body is going to be executed in the test thread but as soon as you call B model scope does launch that code it's going to execute in a different thread and so that the tester is going to keep on running and the test of assertions are gonna likely going to fail because still a little code might be running in another thread so this is not an option we cannot use run blocking test a series what if we take from blocking test out of the question we want to make it simple and we just wait for something to be there for example you can use mockito wait if you are using market or any kind of other testing framework but here what we really are doing is that they code this is still running the the corroding course is running on a different thread but we are blocking the test read just to wait for something to be there so this might be okay in some scenarios don't get me wrong but this test doesn't fail fast and so even if it passes it is gonna add an extra overhead time for every single test that you ran so your test suite is going to be overall slower so that this is okay but we can do better so actually what we want it's a test you're gonna pay pass or fail fast that's clear and we want them to be deterministic when running audience so we want to remove that randomness from the test to make it reliable what can we use we have a class in the library which is called Tesco routine dispatcher this is just a regular dispatcher but it's a fake one and it's gonna allow you to control the execution of the current beans when you are doing tests and so how can we use discovered in this pattern we said before that the model scope is using dispatchers don't make so somehow we have to replace it to be honest dispatches domain is not even available in unit tests so you shouldn't you couldn't use it anyway if you wanted to and this is because this patch is done man uses the hundred main looper to access some code and so that's available instrumentation tests but not in unit test so we need to replace it by actors go route in this pattern how can we do that so for every test class you will have to add some code like this whereas you are gonna declare a variable test dispatcher and then before running every single test you're gonna will write the dispatchers don't make with this method dispatcher that's a man and then after the test you are gonna reset everything that you did and then clean up the desk routine dispatcher this does make sure that no other work is running after the test finishes so this is a lot of boilerplate code to add to every single test class so you can extract this out and put it in the unit rule and then we can have something like this so now we are in our to do we model test we define our Minko routine with the code that we just so before and cottonwood you said so we said before we have run blocking test test dispatcher that the screw dispatcher also allows us to call run blocking test but with the difference that we have before is done now every single code in that get started with this test dispatcher is going to execute immediately so that's pretty handy for us so now you can make it shorter if you want if you don't want that boilerplate code so you can say quarantine ruled from dropping tests great actions and function be imaginative you could be in power you can yeah go crazy with towel and make that an apply function right there definitely so yeah so if we see what's happening now if we whistle eyes that with the threading that we saw before we will say that now run blocking in tests in reality it when I create a new routine and everything is going to be executed there so this is my create a new column that this body is gonna get executed boom another scope Lawrence is gonna be executed immediately there and then by the time that testosterone is calm all the work that the the Cortina started is finished so but would it you don't use these patches don't main now in my Linden in our blue model we want to do some formatting in these patches of default and just before adding that to the repo so can we use just run blocking test you're gonna have the same problem that Corden is gonna be executed in a different thread and so the test our solutions are likely going to fail and this is because we have coded these patches dot default in the code and that's not a pretty good practice for testing so what we recommend is that you should always inject these patches so how can we do that for example you'll watch the VI talk pretty good and so in our B model what we can do is that pass the before dispatcher as a parameter and the default dispatcher later on will be excused to execute the B model let's go launch and in this case like obviously in production you will still be using dispatch as the default but now in test what we can do is passing the test dispatcher from the coroutine rule in our B model and if the if we do this we'll get perspective result and the cool thing that we have started will not be executed immediately this is not the only thing you can do with test scores in this pattern Shawn is gonna tell you more about it so I know we're all awaiting the end of this conference so let's kind of get through this section but let's see let's see kind of these three bullet points that Manuel had earlier and let's dive into these and talk about some of the features of test current uses dispatcher and the other parts of the library so we started with we want to make tests that run fast I mean who loves waiting for long test Suites to take half an hour to run is that a note nobody oh whoa oh there's a person up there yes one person we should chat so yes we all want our tests we to run fast like milliseconds is awesome seconds is good it's okay like that's what we're aiming for here and so the big thing that tusk every teen dispatcher kind of helps you with here and run blocking tests kind of work together on this is it gives you this delay and timeout behavior so this lets you basically Auto progress time as from your test in the co-routines context so if there's like a delay or a timeout in your test you can trigger that immediately or instantly in your test execution instead of having to actually wait a second or five seconds for that timeout to happen in practical test code this is typically used for testing timeouts like that's the reason that you would actually end up calling these functions explicitly but it's nice to have that feature available it's also kind of fast from like things that it helps you do as a programmer because the other cost to writing tests is you have to write the tests so one thing that it does is it returns a unit so you can just apply it say my test equals run blocking test which is nice because you use run blocking it returns the last value of the lambda and then J unit four won't run it so that's a pretty like nice thing every single part of the library is injectable so there's test care routine dispatcher there's test care routine scope and you can inject either of those depending on your architecture and how you need it to work with your application and it's also extensible the whole library is like designed from the beginning to be test framer agnostic so it doesn't have a dependency on j-unit 4.
12 so whatever dependency of j unit you have in your builds is going to be fine it also you know it's gonna work a chain of five and it's gonna work with your custom test runner all right the other thing we want from our test Suites is we want them to be reliable we either want them to either always fail because things are broken or we want them to always pass because things are not broken we typically don't want to fail one out of ten times and like you know you've hit this one like you're like oh the build failed let me run that again and that's like the first thing you do and so like when you get in that situation you want to like spend some time on code quality and test quality and this library helps you when you're doing co-routines in a couple ways so the big one really these two kind of work together is it gives you explicit control over your co-routine execution and it does that by like kind of transforming what was fun a very asynchronous activity running a bunch of things on different threads and joining them all over the place and doing a bunch of concurrency and turning it into a deterministic process that should execute the same way every single time and so we can kind of visualize that a little bit right so let's imagine this is the order of co-routine execution in one of my tests so I've written a test using run blocking tests and the cover teams execute a then B then C then D than E and I check this in I put it into my continuous integration and the test runs again and again and each time it's running this cover team in the same order and because it's deterministic I know that until I either changed the code or I changed that test it's gonna keep passing which is lovely because I'm gonna only get signals when it fails if I didn't have deterministic behavior here I could end up in a situation where where I ended up getting a different order and this different order may not work with the assertions I made or the fakes I have or some part of my test code you know it's test code it's kind of like put it together it's not production code and so it's gonna you know it's gonna fail and I'm gonna get a flaky build and I'm just gonna hit that rebuild button and so that's why I prefer determinism what I'm testing here currency especially done in that specific level and then the other big thing for this dispatcher is it's possible so this is like a huge thing so it does this immediate execution but which is very much the exact same thing that dispatchers on confine does however since it's possible it allows you to basically undo that and like actually execute cover teens in a much more realistic fashion than either one so immediate execution is awesome for 90 95 percent of tests that you write but it's actually an order that can never happen in production so sometimes you need it to not happen and that's when you need to pause the dispatcher to write that last 5 or 10% of tests and then it helps you write isolated tests and the big thing there is it looks for correcting leaks at the end of the run blocking tests lambda and if you call that cleanup test care routine it's gonna go ahead and make sure that you didn't leak of routine into your next test suite which writes to the database and then your test fails 1 out of 10 times so it helps you with this situation right there I mean it also tries really hard and not always successfully but it tries to put the uncaught exceptions into the test that caused the uncut exception and cause that test to fail instead of the test suite a thousand tests later so go check out call lennox care routines test co-routines are awesome we love them on android and that's because this graph as you can clearly see on this graph as you go into more complex code the axis goes up thanks for coming to Android dev summit [Music] you.
Comments
Post a Comment