Testing Coroutines on Android (Android Dev Summit '19)

Video Statistics and Information

Video
Captions Word Cloud
Reddit Comments
Captions
[MUSIC PLAYING] SEAN MCQUILLAN: I am Sean McQuillan, Developer Advocate for Android. MANUEL VICENTE VIVO: And Manuel Vivo, I work in the Android Developer Relations Team. SEAN MCQUILLAN: And we're going to be talking to you about testing coroutines. But before we talk about testing coroutines, let's talk a little bit about coroutines. So at I/O, we talked about how we're going to make Android UI coroutines-first. And what does that actually mean? What, practically, does that change about what we're doing while we're building the Android UI toolkit? So fundamentally what that means is, as we're building new APIs for Android, we're going to take a look at whether we can fit coroutines into them and whether that makes sense and provide a good coroutines support for the APIs we're building. As we build Jetpack libraries, we're going to use coroutines to build those libraries. We're already doing that with some of the Jetpack libraries we're working on now. So you're going to start seeing coroutines shipped in Kotlin-first jet Jetpack libraries. Additionally, we're going to write documentation. I have put documentation up on developer.android.com to explain how to use coroutines and how to use coroutines with architecture components and 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 you can use WorkManager, Retrofit, Room, viewModelScope. These all support coroutines out of the box in a stable version. The liveData builder, lifecycleScope, and whenStarted are all still in a release candidate state on the train to stable. And Kotlinx coroutines test-- the library we are going to be talking about today-- it's still experimental. And it's on the trend to stable as well. So to zoom in on testing coroutines, we need to define an application that we can actually test. So I'm going to go ahead and walk through how to add coroutines to an application that uses only room just to keep the app simple, so we can focus in on the testing. It's going to be just a simple to-do app that store strings and you can mark them as done. And I am going store this in a Room database. And to do that, I'm going to need to define an entity like I would with any Room database. And there's no coroutines in your entity. It's still just an object that holds a row for your database. But then we're going to add coroutines over in the Dao of our Room. So here we can see where we're going to start integrating coroutines into this Room flow. We're going to go ahead and add a suspend modifier on one of our Room queries. This time we're going to insert with the addItem. And that makes this function Main-safe. It's now a suspend function. And Room is going to run that query on a background thread. And it's going to run that on a Custom dispatcher. It's actually going to be the same executor that Room uses if you use live data. And it gets this other really cool superpower of, its cancel-able. So if the coroutine that calls it cancels, it's now cancel-able all the way down. We're also going to do the same thing. We're going to make a suspend function for fetching all the items here as well. You could use Flow for something like this as well. But I want to keep this example simple so we can focus in on the testing part. 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 now technically correct, which, as I like to say, is maybe the worst kind of correct. So we're going to move on. We're going to make a repository that uses our Dao. So we basically have to now make our first API decision. We have to actually figure out how to use coroutines here. And one option that we have is we can make the 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 that's super cool, and awesome, and magical coroutines Kotlin stuff. The deferred version does not. Both of are Main-safe. There's absolutely no difference in the threading behavior between these two implementations. But the threading behavior does get quite different when we look at how we get the results out. So deferred's have this callback called invoke on completion, which just gets called on any old thread. And 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 suspend function. So generally, in Kotlin, you should refer to exposed suspend functions just as many times as you can. That's just the place where you should default. And then we're going to go over to our viewModel and finish out this flow for adding an item to our to-do list. So the viewModel is the natural owner of this work because it's the thing that owns the work that's happening. So that's why I'm going to actually launch the coroutine. This launch call right here actually creates a new coroutine, which then allows me to call the suspend functions that I just created. And then I can use the result right away right here without having to define a callback, which is kind of like the superpower of coroutines right there. So there's three basic rules that we walk through as we were going through that code. So as you're designing code with coroutines, prefer to expose suspend functions as your primitive API choice. Try not to return a bunch of deferred's 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 kind of contains the lifecycle of that work-- be the thing that launches it. And just learn to trust that main-safety is going to work. I've seen a lot of code as people come into coroutines. And they'll start with contexting five times on the way to actually calling a database call. Just trust that it's going to work, and it does. So that's all I really have to talk about for this basic coroutines talk. And I'm going to pass it over to Manuel, who's going to talk about testing. MANUEL VICENTE VIVO: Thank you, Sean. Testing is an integral part of the app development process, but I don't want to spend that much time about it. So TLDR-- test your code. We are going to focus on unit testing in this talk. So how can we define a good unit test? A good unit test should be fast. You shouldn't have to wait for it to fail or pass. And it should be reliable-- always give you the same results. And it should be isolated as well. So execution of unit tests should be independent from each other. And obviously, after the test finishes, no other work should be running. So as we said, we're going to see how to test coroutines and the code that we showed before. So when testing coroutines, I would like to ask yourself, is the test that I am creating now through getting the execution of a new coroutine? If that's the case, it's because you are likely calling launch or async in code under test. If that's not the case, it's because you are probably testing a suspend function that doesn't create a new coroutine. And if nothing of this happens, you are not testing coroutines. So we are living without. So a test is broadly going to fall into one of these two categories. So FYI-- as Sean said before, we're going to be using the Kotlinx coroutine test library. And it's an experimental. Keeping up to date should be relatively straightforward on the road to stable. So I wouldn't worry that much about it. So we're going to see how to test suspend functions now, and specifically, the repo layer. The repo layer as we said, it's supposed to expose suspend functions. Now we're going to see this insertTodo, which is basically adding an item to the Dao. How can we test this? This is a suspend function. And it's run inside a coroutine. And for that, we can use the method runBlockingTest, which is the method from the test library that we just mentioned. runBlockingTest is going to create a new coroutine, and it's going to allow you to execute suspend functions immediately. And you may have heard of runBlocking in the past. What is the difference with runBlockingTest? Well, runBlockingTest is going escape delays, so you don't have to waste extra time in your tests. How can we use runBlockingTest in our tests? Basically, just have to grab your test body inside this method-- runBlockingTest. And that would be it. So if we go through the test, we are creating our subject-- our repo. Then we're calling the suspend function, insertTodo. We can do that because we are inside a coroutine. And now that insertTodo function is going to be executed immediately. So therefore now we can assert that the item was have it. That was easy, right? Things get complicated now with tests that are going to trigger new coroutines. So we're going to focus on how to test viewModel later. So now here we have addItem, which is a regular function-- it's not a suspend function-- that is going to create a new coroutine. This is executed with viewModelScope, which actually uses the dispatchers.main as a default dispatcher. Here, just to simplify everything, calling the repo with a text-- something we want to add. Can we use the technique that we just saw before? So just grabbing our test body is a runBlockingTest. So if you do this, it is going to fail. Why? We are going to see this in a second. So if we had to visualize what's happening here in the different threads, you will see that the test body is going to be executed in the test thread. But as soon as you call viewModelScope does launch, that code is going to execute in a different thread. And so the test thread is going to keep on running. And the test assertions are likely going to fail. Because still, all their code might be running in other threads. So this is not an option. We cannot use runBlockingTest as it is. What if we take runBlockingTest out of the equation? We want to make it simple. And we just wait for something to be there? For example, you can use Mockito wait if you're using Mockito or any kind of other testing framework. But here, what we really are doing is that the coroutine code is still running on a different thread. But we're blocking the test thread, just to wait for something to be there. So this might be OK in some [INAUDIBLE],, don't get me wrong. But this test doesn't fail fast. And so even if it passes, it is going to add an extra overhead time for every single test that you run. So your test rate is going to be overall, slower. So this is OK, but we can do better. So actually, what we want-- it's a test that are going to pass or fail fast. That's clear. And we want them to be deterministic when running coroutines. 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 TestCoroutineDispatcher. This is just a rule of dispatcher, but it's a fake one. And it's going to allow you to control the execution of the coroutines when you are doing tests. And so how can we use TestCoroutineDispatcher? We said before that viewModelScope is using Dispatchers.Main. So somehow we have to replace it. But to be honest, Dispatchers.Main is not even available in unit test. So you couldn't use it anyway if you wanted to. And this is because Dispatchers.Main uses the Android main looper to access some code. And so that's available in instrumentation tests, but not in unit tests. So we need to replace it by a TestCoroutineDispatcher. How can we do that? So for every test class, you would have to add some code like this, whereas you are going to declare a variable-- that's dispatcher. And then before running every single test, you are going write Dispatchers.Main with this method-- Dispatchers.setMain. And then after the test, you are going to reset everything that you did and then clean up the TestCoroutineDispatcher. This just makes sure that no other work is running after the test finishes. So this is another boilerplate code to add to every single test class. So you can strike this out and put it in a JUnit rule. And then we can have something like this. So now we are in our TodoViewModelTest. We defined our main coroutine rule with the code that we just saw before. And how can we use it? So we said before we have the runBlockingTest. TestCoroutineDispatcher also allows us to call runBlockingTest. But with the difference that we had before is that now, every single coroutine that gets 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 to-- if you don't want that boilerplate code. So you can say coroutineRule.runBlockingTest create an action and function. Be imaginative-- Kotlin power. SEAN MCQUILLAN: You can go crazy with Kotlin and make that an apply function right there. MANUEL VICENTE VIVO: Definitely. So if we see what's happening now-- if we utilize that with the threading that we saw before, you will see that now runBlockingTest-- in reality, it's going to create a new coroutine. And everything is going to be executed there. So this is going to create a new coroutine that this body is going to get executed. viewModelScope.launch is going to be executed immediately there. And then by the time the test assertion is gone, all the work that the coroutine has started, is finished. So you are good to go. But what if you don't use Dispatchers.Main? Now imagine that in our viewModel, we want to do some formatting in Dispatchers.default just before adding that to the repo. So can we use just the rule runBlockingTest? You're going to have the same problem. That coroutine is going to be executed in a different thread. And so the test assertions are likely going to fail. And this is because we hard-coded Dispatchers.default in the code. And that's not a pretty good practice for testing. So what we recommend is that you should always inject Dispatchers. So how can we do that? For example, go and watch the DI talk. I'm pretty good now. And so in our viewModel, what we can do is pass the default dispatcher as a parameter. And the default dispatcher later on will be used to execute the viewModelLaunch. And in this case, obviously in production, you will still be using Dispatchers. default. But now in tests, what we can do is pass in the testDispatcher from the coroutine rule in our viewModel. And if we do this, we'll get the expected results. And the coroutine that we started is going to be executed immediately. This is not the only thing you can do with TestCoroutineDispatcher. Sean is going to tell you more about it. SEAN MCQUILLAN: 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 these three bullet points that Manuel had earlier. And let's dive into these and talk about some of the features of TestCoroutineDispatcher and the other parts of the library. So we started with, we want to make tests that run fast. Who loves waiting for long test suites that take half an hour to run? nobody? Whoa. Oh, there's a person up there. Yes, one person-- we should chat. So we all want our test suite to run fast-- milliseconds is awesome, seconds is good, minutes, OK. That's what we're aiming for here. And so the big thing that TestCoroutineDispatcher helps you with here-- and runBlockingTest kind of work together on this-- is, it gives you this delay and time out behavior. So this lets you basically auto-progress time from your test in the coroutines context. So if there's 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 time out to happen. In practical test code, this is typically used for testing time outs. 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 things that it helps you do as a programmer. Because the other cost to writing tests is that you have to write the tests. So one thing that it does is, it returns units. So you can just apply it-- say my test equals runBlockingTest. Which is nice because if you use runBlocking, it returns the last value of the lambda. And then JUnit4 won't run it. So that's a pretty nice thing. Every single part of the library is injectable. So there is TestCoroutineDispatcher, there is testCoroutineScope. 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 designed from the beginning to be test framework agnostic. So it doesn't have a dependency on JUnit4.12. So whatever dependency of JUnit you have in your builds is going to be fine. It's going to work with JUnit5 and it's going to work with your custom test runner. 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 them to fail one out of 10 times. And you know you've hit this when you're like, oh, the build failed. Let me run that again. And that's the first thing you do. And so when you get in that situation, you want to spend some time on code quality and test quality. And this library helps you when you're doing coroutines in a couple ways. So the big one-- really these two kind of work together-- is, it gives you explicit control over your coroutine execution. And it does that by transforming what was fundamentally 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 visualize that a little bit. So let's imagine this is the order of coroutine execution in one of my tests. So I've written a test using runBlockingTest. And the coroutines execute A, then B, then C, then D, then E. Then 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 coroutine in the same order. And because it's deterministic, I know that until I either change the code or I change that test, it's going to keep passing. Which is lovely, because I'm going to only get signals when it fails. If I didn't have deterministic behavior here, I could end up in a situation 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. Because you know it's test code. I just put it together. It's not production code. And so it's going to fail, and I'm going to get a flaky build, and I'm just going to hit that rebuild button. And so that's why I prefer determinism when I'm testing concurrency, especially down in that specific level. And then the other big thing for this dispatcher is, it's pause-able. So this is like a huge thing. So it does this immediate execution, which is very much the exact same thing that dispatchers on confine does. However, since it's pause-able, it allows you to basically undo that and actually execute coroutines in a much more realistic fashion than either one. So immediate execution is awesome for 90%-95% 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 coroutine leaks at the end of the runBlockingTest lambda. And if you call that cleanup test coroutine, it's going to go ahead and make sure that you didn't leak a coroutine into your next test suite, which writes to the database. And then your test fails one out of 10 times. So it helps you with this situation right there. And it also tries really hard-- and not always successfully, but it tries-- to put the uncaught exceptions into the test that caused the uncaught exception and caused that test to fail instead of the test suite 1,000 tests later. So go check out kotlinx-coroutines-test. Coroutines are awesome. We love them at 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. [LAUGHING] Thanks for coming to Android Dev Summit. [MUSIC PLAYING]
Info
Channel: Android Developers
Views: 26,333
Rating: 4.9317074 out of 5
Keywords: type: Conference Talk (Full production);, pr_pr: Android, purpose: Educate
Id: KMb0Fs8rCRs
Channel Id: undefined
Length: 20min 50sec (1250 seconds)
Published: Thu Oct 24 2019
Related Videos
Note
Please note that this website is currently a work in progress! Lots of interesting data and statistics to come.