Engineering Maintainable Android Apps, which is a 4 week MOOC that shows by example various methods for engineering maintainable Android apps, including test-driven development methods and how to develop/run unit tests using JUnit and Robotium (or equivalent automated testing frameworks for Android), as well as how to successfully apply common Java/Android software patterns to improve the extensibility and clarity of Android apps. Students will work on the appropriate automated unit quizzes, based on the material covered in the lecture videos. These lessons will demonstrate the benefits of good software engineering practices that are targeted at creating maintainable code for mobile apps.
There will be roughly 3-4 hours of student engagement time per week, including video lectures, and quizzes. The ordering of the modules within the course is designed to be flexible. In particular, students can watch the videos in whatever order suits their experience and needs, e.g., they may want to watch the unit testing videos prior to the software pattern videos if they prefer to learn about unit testing first.

教学方

Dr. Douglas C. Schmidt

Professor of Computer Science and Associate Chair of the Computer Science and Engineering Program

Michael Walker

Instructor - Graduate Student pursuing PhD in Computer Science

Dr. Jules White

Associate Professor of Computer Science

脚本

[MUSIC] We saw when we were looking at instrumentation testing in Android, that sometimes it can take a long time to build our instrumentation test, and then load it into our emulator so that it runs the test and then get the result back. And we spend quite a bit of time waiting on things to happen. The other thing is that it might be hard to create the state that we want, for example, if we wanted to fake that the device is in a particular location, other things, we have to do some work if we're doing integration testing to try to make that happen. So, what I'm going to show you now is how we can use a library called Mockito that can help us test by creating mock objects. And what mock objects are, they are stand-ins for the real object, but they have a specific behavior that we define. So, let's talk about how we go and use Mockito to make our life easier. Now, if you can look at this test you'll probably all ready start to see that we're going to be using Mockito and doing something similar to what we did with instrumentation tests. We're going to be using this at run with Mockito J-runner, JUnit runner. And basically what this is doing is, when we're going to be running our tests, we're going to be injecting mocks, these are fake versions of objects, into our tests. So, one of the things that we ran into when we wanted to test the geocoder was that it depended on certain Android classes that might not be available to us. And so, what we can do is we can mock those objects so that we have them available to us, and make it so that we can run our test, but run them as JUnit tests. So, to give you an example, geocoder is something that are geoutils are used and can be constructed with. But normally we wouldn't have a way to get a geocoder without [COUGH] running an actual Android environment. So, what we're going to do is we're going to create a geocoder, but we're going to do something interesting. We're going to mock the geocoders. That means we're going to build a fake version of the geocoder. Or even better, it's Mockito's going to build a fake version of the geocoder. And then we're going to specify what we want that fake geocoder's behavior to be. So, if you'll notice, I've created a member variable for the geocoder that we want to create and use. But I'm not instantiating it or doing anything else, I'm just putting the member variable there. And then I put at mock in front of the member variable. And if you want to, if you prefer to do your annotation like that, that's fine too. And what this is going to tell my Mockito is, when you run this test, the Mockito J-runner is going to look for all of these mocks, and it's going to create the mock that should be injected into this member variable. So, then what we can do is we can have our geoutils. I'll just go ahead and do that. And then we'll have our setup method. And we can go ahead and create a geoutils with the mock that's going to be created. So, what we know is that when this test gets run, we're going to end up with some mock object. Some fake version of a geocoder that behaves in a way that we can control, and then we can go an add a test. And let's say, let's add a test that coordinatesWithNoZipCodeReturnNull. So, we want our geocoder to return null if we give it a zip code that's invalid. So, what we're going to do is we want something like this, zipCode, and we're going to say all right, geoUtils.getCurrentZipCode and let's give it 0, 0, which we saw in an earlier screen cast that 0, 0 does not have a zip code. So, if we do that, we're going to assertNull(zipCode). So, we expect that if we call getCurrentZipCode with 0, 0 that we return null. So, let's go ahead and run this geoutils test and see what happens. The first thing is, we're all ready doing something interesting, which is we are creating mocks and what we see is our test actually ran. And if we look at what happened, we see that our code ran, we got the getCurrentZipCode [COUGH], that actually made it into the geoutils class. If we look at where it actually failed, what we're seeing is that, well it tried to get something that didn't exist. So our mock object actually worked, it actually ran, returned a list and on this line right here, it attempted to access the list and list was empty. So, our test actually ran with our mock object. So, we had a fake object that returnedjust an empty list. So if we wanted to go back, first let's go ahead and fix our test, so it does the right thing. So let's say, one way to do this is we can say, String zipCode = and we'll just check. And I apologize, I like doing one liners sometimes. It's > 0, so if we have at least one location, then we'll just return the first one, otherwise we'll return null. And then we return zipCode. [COUGH] And so, what we're going to do is now check that our test is going to passed. So, let's rerun this. We'll wait a second while Android updates, and now our test is passing. So, we've fixed the behavior and gotten what we expect. And we're using a mock object to get it, so we're not waiting for the Android instrumentation test to be loaded, we're actually running much faster. So, this can be a boon if we want to be able to test faster, as well as if we want to be able to put a certain state in play. So, let's take a look. Let's go ahead and look at, what if we wanted to replicate our test of the Nashville zip code? Okay, so let's create that test for the Nashville zip code. CheckThatTheLatLong, or let's say, nashvilleLatLongreturns37212. [COUGH] We're going to check this with a test. So, we'll go through, we'll get the zip code. And we'll pass in the coordinates for the Nashville 37212 zip code. And then we will assertEquals. 37212 is what we expect, and we are going to make sure that matches the zip code. So now if we run this, let's see what happens. And just as you know I'm clicking and running things that way in order to help have some visuals here. There's lots of keyboard shortcuts that you can use to do this thing, and I encourage you to learn the keyboard shortcuts to make your life just a little faster in the development environment. So what we see is the test failed, and it says the reason it failed is because it expected 37212 but it got a null back. And knowing our code the reason I probably got null back is that means that when our geocoder, if we go and look at the geoutils class, when we called get from location our mock object doesn't know how to get a location for a Latin long. So it's just returning an empty list. And then here we're checking the size of the list and if there's a least one entry in the list, which there's not. In which case it's just returning null to us. So what we need to do is we need to be able to tell the geocoder a list of addresses to return to check so that we can see if this code does the right thing when we return the address. So I'm going to create this fictitious mock scenario with our unit test. So what we have to do is we have to tell the mock what we want it to return when that method is called. So the way we're going to do that is with this statement called when. So we're telling it when this condition occurs, and we're going to say when the geocoders.get from location method is called. So this what is at the method that's that's being called by the geoutils class. And we'll just go ahead and we'll start off just being really specific we'll say, when it's called with these arguments. We want to return a list that we control of addresses. Now we're just going to start off with a empty array list. We run this to show that we can run it and it still works. And then what we're going to do is we're going to modify that array list. So we still get the same behavior that we did before. This is also compiling. So what we're going to do is, we're going to create a fake address for Nashville. Now I'm just going to do it up here, just to show you how to do it up here. And we're going to say private address, address, were Nashville. There's other ways to do this but we're just going to keep going with the current approach. What we're going to do is say when address for Nashville.get.getPostalCode.thenReturn37- 212. So what we're doing is we're saying whenever the address is getPostalCode method is called we want it to return 37212. And then what we're going to do is we're going to tell the geocoder that whenever its get from location with the Nashville address or the Nashville lat long is called to return that address for Nashville. So the way we're going to do that is we're just going to say arrays as a lists make our lives a little easier here, address for Nashville. So what we've said now is the behavior we've defined is up here we've said whenever we ask this address for the postal code, it is going to return 37212. And then down here, we've said whenever we ask this geocoder for the address, the addresses associated with the Nashville. Latitude and longitude, we want to return the Nashville mock address. So what we're doing is we're mocking an address that we know has the correct postal code for Nashville. And then we're mocking and saying, whenever we get a call for the addresses at the location associated with Nashville's latitude and longitude, we're going to return that address for Nashville. And so what should happen then is when we call this, get current zip code. If we dive into that it's going to call our mock geocoders get from location, with the natural address, or with the natural latitude and longitude which we are expecting to return a list with the natural address. And then the result at this expression should be that we get the natural zip code back. So we're basically mocking all this behavior for the geocoder in our test so we don't have to run an instrumentation test but also we can, if we wanted to set up a very particular state very quickly and easily we could do that. So let's run our test and see what happens now. So now we're getting the test running, and our test passes. So what that means is, we have created a fake geocoder, and we've explained how it should behave when it gets certain method calls. Now let's do something interesting. Let's change this so it's not exactly the national address anymore and see what happens? So we'll just rerun this part of the test. And let's see what happens. We failed now. And the reason is because we very precisely defined the behavior to be when you get this latitude, longitude and number of results that are wanted. But we're actually giving it a different wind down here. See you can very precisely define the behavior. But let say that we don't really want to get tightly coupled. We just want to return one address always for any of our test for example. And then what we can do is use something that's called an argument matcher. So the idea behind this is maybe we will want to create a geocoder where it doesn't matter what latitude and longitude you give it it's always going to return the same address for you. because you can imagine, if you had to specify in this format of every possible latitude and longitude combination, well, they're both doubles. It's just a huge number of possible combinations. You're not going to be able to do that in a test, so we have to have some way that's more flexible. So way that we use argument matchers is I'm just going to break this down a little bit on some separate lines so it'll be a little easier to see. Is we want to replace these specific numbers with something else. So what I'm going to do is I'm going to replace the first one with any double. I don't care what number you pass me as long as it's a double I don't care what the latitude or the longitude is, I just expect it to be a double or how many results that you need. And these are methods that are provided by Mockito. And just to show you where I've gotten these, I've actually statically imported Mockito at the top of my test. And you'll need to do this, too, in order to have when and any available to you in the same fashion. Just like we do with the JUnit asserts, and we statically import them, I'm doing this for Mockito. And so, now, what my mock says is it doesn't matter what you pass in for the latitude, the longitude or the integer, I'm going to give you back Nashville. So we'll go back, we'll rerun our test. Now this isn't a particularly helpful way defining this for our test, in this case, well, possibly it is. But let's say we go down here and we change and we say zip code two. And this is actually testing the mock, not really testing anything valuable but I just want to show you what this is going to do. We could say we're expecting the second zip code to also be 37212. Now before this would have failed, but we're going to run it again and show that now that we've mocked this behavior with the matchers, we still can get this to pass. So we're building, as you can see, complex behavior for this geo-coder object. We're able to precisely set up it's state and we're able to very quickly run the test. You can compare that with the instrumentation test that took much longer to run. And you can see that mocking is a very powerful way to build your test. And get them to run quickly, and you can go and mock just about anything. Sometimes you'll take some time to define the behavior of that mock. Just like we saw when we were specifying this Nashville behavior, but you can go and create a complex set of behaviors that allow you to isolate your code. Since we know exactly what this geocoder is going to return in different situations, it allows us to test and isolate our code more effectively. And set up the context or environment, the scaffolding for our code more precisely. There is one more thing that I need to mention on mocking and that's how you set up and use Mockito in your Android project. So, like most things in Android, we're using the Gradle build system so whenever we need to bring something new in we have to go and edit our build.gradle file. So you're going to need to add the Mockito library as a test compilation dependency. So I've opened up my build.gradle file. And I'm showing you the line right here that you would need to add. You would add the appropriate version of Mockito. And very importantly, you want this to be a test compilation dependency. You don't want this to be a compiled dependency like these up here. And the reason for that is anything you add as a test compile dependency, that means that it's only present when you're running your unit tests or your other tests. For example, Android test compiles for these instrumentation tests. So you don't want the Mockito Library loaded into your app, and compiled into it for distribution or for loading a device. This is really only something that you want available for when you're running unit tests. So that's why it's here. And so by adding this test compile dependency on Mockito, that's what gives us the ability to import the MockitoJUnitRunner. It gives us the ability to statically import the Mockito when and other methods that we're using to define behavior down here. And then it's also giving us the @Mocks that we see up here.