Chapters

Hide chapters

Advanced Android App Architecture

First Edition · Android 9 · Kotlin 1.3 · Android Studio 3.2

Before You Begin

Section 0: 3 chapters
Show chapters Hide chapters

3. Testing MVC
Written by Yun Cheng

Testing software is important to ensure your software’s behavior and to catch your bugs before your user catches them. In this chapter, you will learn about testing on Android, understand why this book will focus on unit testing and examine why it is difficult to write unit tests for an MVC app.

Android testing

Testing in software development is often underestimated and pushed to the later stages of a project, if even at all. Unfortunately, there are some developers, clients and managers who don’t believe that serious testing is important. The truth is, mobile apps are becoming larger and more complex. They’re used to chat with our friends, play video games, socialize, take pictures, schedule appointments and much more. As our apps and games grow in complexity, so does the code base behind them, making testing even more important.

Android Testing is typically divided into three different types: Small, medium and large. In this section, you’ll get an overview of each.

Small tests

Small tests are unit tests that run independently of an emulator or physical device. Small tests generally focus on a single component as all of its dependencies are tested beforehand and are mocked or stubbed with the desired behavior.

When compared against the other two test types, small tests are the fastest because they don’t require an emulator or physical device to run. That said, they’re also low-fidelity, which makes it difficult to have confidence that your app will function properly in the real world.

On Android, the most commonly used tools for these tests are JUnit and Mockito.

Medium tests

Medium tests are integration tests that help you check how your code interacts with other parts of the Android Framework. Medium tests are typically run after you complete unit tests on your components. That’s when you need to verify that things will behave properly together.

On Android, one of the most common tools to perform integration tests is Roboelectric. Unlike traditional emulator-based Android tests, Robolectric tests run inside a sandbox and don’t need a device or emulator. However, it’s best to test your app on an emulated device, a real device or using a service like Firebase Test Lab. Device farm services are great because you’ll be able to test against different combinations of screen sizes and hardware configurations. This can help you catch bugs specific to some device categories.

Large tests

Large tests are integration and UI tests that emulate user behavior and assert UI results. These tests are the slowest and most expensive because they require the emulator or a physical device.

On Android, the most commonly used tools for UI testing are Espresso and UI Automator.

Overall, Google recommends that you create tests of each category based on your app’s use cases according to the following rule: 70 percent small, 20 percent medium, and 10 percent large. The Testing Pyramid (https://developer.android.com/training/testing/fundamentals) illustrates this.

Focusing on unit tests

Although all three types of testing play a role in ensuring a quality app, this book concerns itself primarily with unit tests, for the following reasons:

  1. It is much faster to run unit tests than it is to run UI or integration tests. UI and integration tests typically run on an emulator, and starting up your app on an emulator and simulating user input takes up significantly more time than just running unit tests on the local JVM.

  2. Unit testing does not require any Android testing libraries. The idea, here, is that you are testing regular Java/Kotlin code, not code that is Android framework-specific. As a result, using JUnit, the testing framework for Java, is all that is needed to write unit tests. If you wish to test code that uses Android-specific classes, you’ll need a testing library specifically for Android. Libraries like Robolectric (robolectric.org) and Espresso (https://developer.android.com/training/testing/espresso/) are popular options.

  3. Unit tests make up the foundation of an app’s test suite. You can imagine that individual units of code work behind the scenes to display what the user ultimately sees in the app. The idea behind unit tests is that, if you can test that all the individual units of code work as expected, you can have high confidence that, added up together, the app as a whole also works as expected.

Unit testing the Movie class

First, you will write your first unit tests for the WeWatch app. In the starter project, open Movie.kt and take a look at the getReleaseYearFromDate() method:

fun getReleaseYearFromDate(): String? {  
  return releaseDate?.split("-")?.get(0)
}

This method exists because the format of dates returned by The Movie Database API is YYYY-MM-DD, but the app is only interested in displaying the year. It’s easy to make errors when parsing dates, so it is worthwhile to test that this method returns expected values.

In the starter project, open the file MovieTests.kt and add the following test code:

class MovieTest {

  @Test
  fun testGetReleaseYearFromStringFormmattedDate() {
    //1
    val movie = Movie(title = "Finding Nemo", releaseDate = "2003-05-30")
    assertEquals("2003", movie.getReleaseYearFromDate())
  }

  @Test
  fun testGetReleaseYearFromYear() {
    //2
    val movie = Movie(title = "FindingNemo", releaseDate = "2003")
    assertEquals("2003", movie.getReleaseYearFromDate())
  }

  @Test
  fun testGetReleaseYearFromDateEdgeCaseEmpty() {
    //3
    val movie = Movie(title = "FindingNemo", releaseDate = "")
    assertEquals("", movie.getReleaseYearFromDate())
  }

  @Test
  fun testGetReleaseYearFromDateEdgeCaseNull() {
    //4
    val movie = Movie(title = "FindingNemo")
    assertEquals("", movie.getReleaseYearFromDate())
  }
}

Here are the four test cases tested in this suite:

  1. Pass in a releaseDate of 2003-05-30. The expected output would be the year extracted from the date: 2003.
  2. Pass in a releaseDate of 2003. Even though the input is not in the format YYYY-MM-DD, the expected output would still be 2003.
  3. Edge case: pass in an empty releaseDate of "". In this case, the most appropriate output for this input would be just an empty string.
  4. Edge case: pass in a null releaseDate. In this case, the best output for this would also just be an empty string.

Run the tests. You should see that only three out of four tests pass! Test case #4, which tests a null input, fails. This is because the code for getReleaseYearFromDate() does not properly handle a null input. Instead of returning an optional String?, it is better to return a String, even if the String returned is just an empty one. Make the following fix in the Movie.kt file:

fun getReleaseYearFromDate(): String {  
  return releaseDate?.split("-")?.get(0) ?: ""  
}

The addition of the Elvis operator ensures that if the releaseDate is null, an empty string is returned. Rerun the test with these changes, and you should see that it now passes.

Unit testing an Android Activity

As you just saw, unit testing the logic in data classes — like Movie.kt — that make up the Model is pretty straightforward using JUnit. You simply instantiate the object using the class’ constructor, like with val movie = Movie(title = "Finding Nemo", releaseDate = "2003-05-30") . Then, you call a method on the object, like movie.getReleaseYearFromDate(), and assert that the result is what you expect.

But there is an entire rest of the app beyond data models still left to test! What about unit testing all the Android Activities in app?

Unfortunately, testing an Activity is tricky for two reasons:

  1. Android lifecycle: An Activity is an Android framework-specific class that is bound by the Android lifecycle, which is controlled by the operating system. As such, you cannot instantiate an Activity: It has no constructor. Thus, you cannot just create an object out of that Activity and call methods on it. For example, if you wanted to test the behavior of displayMovies(movieList: List<Movie>?) in MainActivity.kt given different inputs, you cannot just create an instance of that Activity like val mainActivity = MainActivity(), nor can you call methods on that instance like mainActivity.displayMovies(ArrayList<Movie>()).
  2. Android dependencies. A typical Android Activity will contain other Android framework-specific classes in it, such as Context, RecyclerView, Toast, etc. As you will learn in Chapter 5: “Dependency Injection”, these dependencies will need to be mocked if you want to test code in isolation. It is easy enough to mock your own classes, but not so easy to mock classes that belong to the Android framework.

Again, there are libraries like Robolectric that can address the above two challenges, but these will run much slower than regular JUnit tests.

Why MVC makes unit testing hard

Recall from the previous chapter that, while the Model View Controller pattern proved useful for other software systems, it didn’t quite work for Android apps. Yes, you managed to separate the Movie model in a way that allowed it to be tested, however, the Android Activity ended up serving as both the Controller and the View. The downside with that, as you just learned, is that Activities are not easily tested with unit tests. The result is that all the Controller logic is trapped inside the Activity, and it becomes untestable.

Clearly, the MVC pattern in Android is inadequate for not just separation of concerns but also for unit testing.

Key points

  • There are three categories of testing in Android: unit tests, integration tests and UI tests.

  • Unit tests are fast to run and don’t require external Android test frameworks.

  • If you can achieve high unit test coverage of your app, you can be confident that the app as a whole works as expected, because ultimately the app is made up of all those small units all working correctly.

  • Unit testing a regular data class using JUnit is easy: Simply create an instance of that class, call a method on it and assert that the output is expected.

  • Unit testing an Android Activity is difficult due to the Android lifecycle and the many Android dependencies in an Activity.

  • There are external libraries like Robolectric and Espresso that can test classes containing Android framework-specific components, but these are significantly slower to run than unit tests.

  • The MVC pattern is not conducive to unit testing because all the Controller logic is stuck inside an Android Activity unable to be tested. (The Model, however, can be tested.)

Where to go from here?

It’s impossible to cover all of the aspects of Android Testing in a single chapter. If you want to learn the ins and outs of Android Testing, check out the following links:

In this chapter, you gained an overview of unit testing and saw how a major drawback of the MVC pattern is the lack of unit testability due to the nature of the Android Activity. In future chapters, you will learn about other patterns that address the problems of unit testing logic in Activities — by moving that logic out of the Activities.

Have a technical question? Want to report a bug? You can ask questions and report bugs to the book authors in our official book forum here.
© 2024 Kodeco Inc.