Testing Android Applications

//Testing Android Applications

Testing Android Applications by Younes Charfaoui

Testing is an important part of software development. Whether you’re making a small app to learn new skills or building a multiplatform app that will have millions of readers, making sure that it is tested robustly will ensure that your app has the highest possible quality at all points in it’s life.

There are many types of and approaches to testing. You may have heard of terms like Automated Tests and Software Tests, and you may have also tried your hand at it before but want to learn more. In this article, we will see why testing is so effective, and explore its benefits while focusing on testing in Android.

Table of Contents

  • How I learned the value of testing
  • Why you should test
  • Types of tests in Android
  • Level of tests
  • Exploring testing levels
  • Test-driven development
  • Keys to success: learn more

How I learned the value of testing

When I began my Android development journey, I didn’t really delve into testing. I used to see two folders named androidTest and test that I didn’t use at all. Even as I went deeper into the field as an Android developer, I assumed that the testing that a lot of books and speakers talked about, Testing and Automated Tests, were advanced topics. My focus remained on learning to create apps and finding clients (as I am a Freelancer).

Eventually, I came to incorporate testing into a project for a client, as I believed it was important for a professional Software Developer to test apps for clients. All software developers know that we should test, and I found myself writing test code to verify my production code. For example, I wrote code to verify if users were shown an error message if they tried to login to the app with an unverified email. At the time, it seemed tedious, since I knew it worked based on my manual tests. I felt like it was a waste of time, so I abandoned it and completed the app, got my money, and started searching for my next client.

After some time, I found a new client that needed an android app built. This app development process invovled many changes through the development lifecycle. I would develop and test everything manually: the login, adding orders to the database, displaying error messages correctly when there is no internet, and so on. My manual testing ensured that everything was working correctly, but I didn’t like the workflow: every time the client sent me an update or a new feature request, I had to retest everything every time I made a change!

I was also afraid of forgetting to test something with each iteration. So I came up with this brilliant idea; I created a checklist in my notebook for that project to hold all the features I had to test so that I could give my client a good quality app. Here are some of the checkpoints from that checklist:

  • Test if the login screen shows an error when we leave the email or password field empty.
  • Test if the login screen shows an error when we enter an invalid email.
  • Test if the login screen shows an error when we enter a password that’s smaller than 8 characters.
  • Test if the login screen shows an error when we enter a password containing just numbers or letters (it should be both).
  • Test if the app transitions from the login screen to the main screen when the user enters valid credentials that are in the database.
  • Test if the login screen shows an alert that there’s no internet connection detected when the user’s device is offline when they enter the required credentials.

The complete list is too extensive to replicate here, but you can get an idea of how much manual testing I had to do, not just once, but with every change that had to be deployed to the client. I called each testing cycle testing week: I would grab my phone and my notebook, start testing each checkpoint to see if the app was working correctly, mark it as a failure when it wasn’t, and then move to the next point. Once I went through the checklist, I would check the code for every failed test separately and try to fix it, then test all over again. This process would take a week or so to complete.

This is when I remembered that androidTest folder. I remembered that had I written a test before and realized that if I wrote a test once, I could run it many times, without the frustration of manual testing. So when I completed the app and began searching for my next client, I promised myself that I would incorporate automated tests into the next project.

Though that project took so much time and energy from me, it taught me that manual testing is hard to do efficiently and accurately at scale, and it eats into time I could spend on other productive tasks.

Now, I have a great green button in Android Studio that I can push to run all my tests, and guess how much time this takes? 6 seconds! It really takes 6 seconds to check that the latest system build works correctly. If a test runs into a problem, I would then correct it and run that test alone (20 ms) before running the entire suite of tests again for another six lovely seconds. That’s a total of 12 seconds and 20ms compared to a week running a manual testing workflow. I can ship that code with confidence because I have the power of Automated Testing.

My software development worldview changed dramatically after applying software testing, especially with Test Driven Development (which we will explore shortly). Now I design and apply high quality architecture, implement clean code, refactor, and create decoupled modules easily, and use all the best practices of Software Development thanks to testing.

Why You Should Test

Perhaps by now, you can see why it’s useful to write tests: It provides a great way to validate a system quickly and efficiently and frees up your time to give attention to other aspects of the development process.

Here are some other advantages that automated tests provide:

  • When writing tests, you are writing the documentation of how to use the software/application: think about it, if we write a test about the function add() that takes two numbers and adds them together, here’s what the test will look like:

result = add(5, 6)

assertEquals(11, result)

The test clearly demonstrates that add() takes two number and returns a result. This shows me how this function can be used.

  • You will have the power to refactor your code without fearing it: Sometimes, when you already have code that is working great, and you may fear having to make changes to its design and refactor it to look more pretty and efficient, as it might lead to the code breaking. So we may leave it as it is instead of making changes to improve the app.

With automated tests, you simply click that green button and see if the changes you introduced have affected that piece of code. If it has broken the code, you can simply roll back and see what caused the error. Because of the marginal time required to test, you can concentrate on fixing the code because testing is no longer a big task that eats into your time.

The list of advantages is endless, we can write a whole article about why you should write a test, but the most important thing to remember is that testable software is high-quality software. So, removing the time barrier for testing makes it easier for you to keep testing your apps and thus ensure high quality software.

Types of Tests in Android

Testing in Android is a bit different from other platforms. There are two types of tests that you can carry out when developing apps for this platform. Let’s take a quick look:

Instrumented Tests

When testing the entire app, you will have to push your app to a Device to run it rather than test on a workstation. We call this type of test Instrumented Tests. Any test that requires the Android framework needs to be run on the device and requires some android specific components, like Context.

Local Tests

The second type of test is Local tests. These tests run locally on the JVM and don’t have dependencies on Android Specific dependencies. For example, imagine we’ve created a function to convert temperature from Celsius to Fahrenheit. This function will take a number and return a number. There’s no dependency on any part of the Android Framework to run this task. You can test this behavior easily with a local test.

fun convertCelsiusToFahrenheit(fahrenheit : Double) : Double {
     return ((fahrenheit  -  32) * 5)/9
}

This function can be incorporated into an Android app that does the conversion. Let’s imagine that the app has a button and a text view. It makes sense then to test this portion of code separately from testing it with the button click.

Local tests run much faster than instrumented tests, and you should know how to separate the code that requires the android framework from the code that doesn’t need it. For example, the previous function can be written in a separate utility class. A bad implementation will be if we write that function into an Activity directly, and when the button clicks we execute the following:

button.setOnClickListener {

     val fahrenheit = editText.text.toString()

  val celsius = ((fahrenheit  -  32) * 5)/9

  resultTextView.text = "celsius C"

}

This code is a bad implementation because we can’t test the function aspect of the conversion algorithm. We always need to run an Instrumented test to see whether the function is working correctly or not. This is a powerful concept to learn: always try to separate

You can create local tests even if you need android dependencies, but you should mock or create fakes with custom responses for those dependencies; for example, let’s take a function that computes the size of a Bitmap, where the bitmap is an Android Specific Class, but this function only uses the public function getRowBytes() and getHeight():

fun calculateBitmapSize(bitmap: Bitmap) : Int {

    return bitmap.rowBytes * bitmap.height

}

As you can see we can create a mock or a fake with custom responses for getRowBytes() and getHeight() and test this function separately. Here’s what the tests will look like:

import org.junit.Test

import org.junit.runner.RunWith

import org.mockito.Mock

import org.mockito.Mockito

import org.mockito.junit.MockitoJUnitRunner

@RunWith(MockitoJUnitRunner::class)

class BitmapTest {

    @Mock

    lateinit var bitmap: Bitmap

    @Test

    fun testCalculatingBitmapSize(){

        Mockito.`when`(bitmap.rowBytes).thenReturn(550)

        Mockito.`when`(bitmap.height).thenReturn(120)

           Truth.assertThat(BitmapUtils.calculateBitmapSize(bitmap)).isEqualTo(66_000)

    }

}

This is how you can mock android specify class, but don’t overload this type of test, you shouldn’t mock everything you see. There is a rule: don’t mock what you don’t own. For example, if we mock the Button class of Android, we are testing our implementation of the mocked button, and not the actual Android Button class. Something like bitmap.height is simple, so consider the simplicity of the behavior as well.

Exploring testing levels

For software testing, there are a variety of testing levels that may be used to verify behavior of a section of code. These levels help us focus in to a specific area to make sure if functions as planned, bring various elements together to see if they work well together, and then test system wide to make sure the entire app works as planned. Let’s dive into these levels.

Unit Tests

This is the most basic type of test: you test a unit of your code. These types of tests aim to verify each part of the software independently and see if they fulfill the requirements of the app.

To start unit testing, add a dependency for JUnit and mockito:

dependencies {

    // JUnit 4 framework

    testImplementation 'junit:junit:4.13'

    // Mockito framework

    testImplementation 'org.mockito:mockito-core:1.10.19'

}

Then, under the test folder, you can create classes and packages to structure your tests, here is an example of a test in the Android Documentation (https://developer.android.com/training/testing/unit-testing/local-unit-tests):

import com.google.common.truth.Truth.assertThat

import org.junit.Test

class EmailValidatorTest {

    @Test

    fun emailValidator_CorrectEmailSimple_ReturnsTrue() {

        assertThat(EmailValidator.isValidEmail("[email protected]")).isTrue()

           assertThat(EmailValidator.isValidEmail("nameemail.com")).isFalse()

                     assertThat(EmailValidator.isValidEmail("[email protected]")).isFalse()

    }

}

In this test, we are testing the function of isValidEmail() to see if it works well via multiple assertions and different emails combinations.

Integration Tests

After testing each component separately with the help of Unit Tests, it is time to put them together to test their integration. These are called Integration tests because they verify that the interactions between classes or modules are correct. This is where you bring multiple classes that collaborate together and see whether they are interacting correctly.

Here is an example from one of my recent projects:

lateinit var messagesRepository: MessagesRepository

// 1

@Mock

lateinit var remoteMessageDataSource: RemoteMessageDataSource

@Mock

lateinit var cacheMessageDataSource: CacheMessageDataSource

// 2

@Before

fun setUp() {

    messagesRepository = MessagesDataRepository(remoteMessageDataSource, cacheMessageDataSource)

}

@Test

fun `when repository get all messages is invoked, the remote and data source should be invoked`() {

        val order = orderTest

                     // 3

                     //mocking

        Mockito.`when`(remoteMessageDataSource.getAllMessages(order)).thenReturn(dummyMessageList)

                     // 4

                     //launching the action

        messagesRepository.getAllMessages(order)

                     // 5

                     // verifying

        Mockito.verify(remoteMessageDataSource).getAllMessages(order)

        Mockito.verify(cacheMessageDataSource).insertMessages(dummyMessageList)

        Mockito.verify(cacheMessageDataSource).getAllMessages(order)

        Mockito.verifyNoMoreInteractions(remoteMessageDataSource)

        Mockito.verifyNoMoreInteractions(cacheMessageDataSource)

}

Although it has a lot going on, I will try to explain everything. First, this method is testing a message repository class. This class is responsible for getting messages from an API (with remoteMessageDataSource), cache them into the database (with cacheMessageDataSource), then return the messages list from the database. Pretty easy, right?

This is clearly an integration test that runs in the local machine under the test folder because it is testing what happens when we integrate these components.

Let us understand what I am trying to achieve with this test.

  • The first thing to notice here is that I mocked the remote and the cache data sources, so I don’t have to do real database calls and real network calls. This is because the real database calls and real network calls will slow down the tests. What I did was simply mock these interfaces (I own these classes, so I am mocking what I own).
  • The setUp() method is simply initializing the messageRepository class, we do it in the setUp() method to not do it in every test method because this method will run before each test, for that we used @Before
  • In the test method, we start by mocking the call of the getAllMessages() In the real-world, this method will return a list of messages, but for the purpose of testing, we can return a list of our own that contains several dummy messages. The content doesn’t matter, we just need a list with random messages.
  • After that, we launched and invoked the main action we want to test. getAllMessages() from the repository. Don’t be confused by the other getAllMessages() from the remoteMessageDataSource. I will soon introduce another one from cacheMessageDataSource.

After invoking the method, it is time to verify that our implementation is correct. I will show you the implementation in a second, but bear with me for a moment. Let’s first understand what we are verifying? First, we are verifying that some methods in the cache and remote data sources were called. Then, we are verifying that no other methods have been invoked from these data sources. That’s it!

  • We are verifying that getAllMessages() from the remote were called.
  • Then we are verifying that the cache data source has inserted those messages into the database with the insertMessages()
  • Then we are verifying that we got that result from getAllMessages() of the cache data source.
  • Finally, we are verifying that no other calls have been made from the remote and message data source.

Here is the implementation of the getAllMessages() of the repository we were testing:

override suspend fun getAllMessages(order: Order): List<Message> {

    val messageResult = remoteMessageDataSource.getAllMessages(order, lastUpdateDate)

    if (messageResult.isNotEmpty())

        cacheMessageDataSource.insertMessages(messageResult)

    return cacheMessageDataSource.getAllMessages(order)

}

You may ask, don’t we need to test the getAllMessages() for remote Data Source, insert, and the cache data source? Of course, we must, but these should be unit tests instead. We have to write them first, then we write the integration tests. Here is an example of a unit test for the room implementation of the cache data source:

@Test

fun testInsertingNewMessages() =runBlocking{

           roomMessageDataSource.insertMessages(dummyMessageList)

    val messages = roomMessageDataSource.getAllMessages(orderTest)

    Truth.assertThat(messages).containsExactlyElementsIn(dummyMessageList).inOrder()

}

End to End Tests

With End to End Tests, you test the system to see whether it works correctly and to ensure that the overall system meets the requirements of the app and that it is very close to what the user will experience.

In the next section, we will talk about espresso, which is one way to perform simple end to end tests in our application. When the end to end tests involve complex, multi app testing, there are tools specially designed for that, such as UiAutomator.

Espresso

Espresso is a framework made by the Google Android Team to test app UIs. Using Espresso, you can write concise, beautiful, and trustworthy Android UI tests. These tests are very quick! It allows you to leave your waits, syncs, sleep, and polls behind while manipulating and asserting the application UI.

When a user performs a certain action or inputs something specific into the target app’s activity, this sort of test checks that the app behaves as intended. It lets you verify that the target app responds to user inputs in the app’s activities with the right UI output.

Setup

To start using espresso in your development machine, you need to do two things: add the dependencies that Android Studio requires and set the test device environment.

The test device environment needs to have no view animation, that’s a constraint imposed by Espresso to not wait until the animation is done to perform tasks on the device.

  1. Go to Settings, then choose the Developer option, and disable the three main settings: Window Animation Scale, Transition Animation Scale, and Animator Duration Scale.
  2. Add the following dependencies in the gradle file:

androidTestImplementation "androidx.test.espresso:espresso-core:3.3.0"

androidTestImplementation "androidx.test.ext:junit-ktx:1.1.3"

androidTestImplementation "androidx.test:rules:1.3.0"

androidTestImplementation "androidx.test:runner:1.3.0"

androidTestImplementation "junit:junit:4.13.2"

  1. After adding the required setup, go into the androidTest folder, and start writing tests, here is an example of a real UI test from the Android documentation (https://developer.android.com/training/testing/espresso):

@Test

fun helloWorldTest() {

onView(withId(R.id.name_field)).perform(typeText("Steve"))

onView(withId(R.id.greet_button)).perform(click())

onView(withText("Hello Steve!")).check(matches(isDisplayed()))

}

A lot is going here, so let me explain how it works:

  • First, we have to locate the view we want to perform an action on with the method onView. This view can be a Button, EditText, or any other view, and the action can be clicking, long click, typing a text, or any other action.
  • To find the view, you can either locate it by its ID with the method withId(), by its text with withText(), and other methods provided by the Android SDK. These methods are called ViewMatchers, and you can also create your own matcher if your layout is complex and it is hard to locate a view.
  • After locating the view, you can perform an action using the perform() You can use click() to perform a click on the selection view, typeText() to type a text, longClick(), closeKeyboard(), and many more action, and you can also create your custom actions.
  • After locating and performing the action, it’s time to check the expected behavior. You can do that by chaining the check() method, this method can be chained to a ViewMatcher, or ViewAction. You can check whether after clicking a button that the button is not displayed any longer, or after typing some text, a prompt or error text is shown, and so on, and guess what, you can also write your own ViewAssertion class to perform custom checking.

There is a lot to learn about Espresso, there are Intents, Recycler View Actions, Idling resources, and many more, I advise you to check the following packages online:

  • espresso-web – Contains resources for WebView support.
  • espresso-idling-resource – Espresso’s mechanism for synchronization with background jobs.
  • espresso-contrib – External contributions that contain DatePicker, RecyclerView, and Drawer actions, accessibility checks, and CountingIdlingResource.
  • espresso-intents – Extension to validate and stub intents for hermetic testing.

Here is another test from a project that I am working on. I’m sure you’ll be able to understand what it’s doing from what we’ve learned so far:

@Test

fun whenClickingOnForgetPassword_shouldGoToResetPasswordActivity() {

Espresso.closeSoftKeyboard()

onView(withId(R.id.loginForgetPassword)).perform(click())

Intents.intended(hasComponent(ResetPasswordActivity::class.java.name))

}

Test-Driven Development

I’m sure that you’ve heard about Test Driven Development (TDD). You may have even tried it out and become frustrated after writing the test first and then the code for it. Believe me, I can’t express how frustrated I was by the idea at first. I tried it, but my frustration kept increasing because I thought it was a complete waste of time (yeah, I was still uninformed back then). I kept wondering what sense it made to write a test for something that does not exist? How can I waste time by watching the test fail? Really, TDD learning was a bad experience for me the first time I tried it because I thought I knew better. But ultimately, after trying it out and testing it repeatedly, TDD gave me much more than I thought it would.

What is TDD?

TDD is a practice where a developer writes a functional test before building the code. TDD means that you should write your tests first, watch the test fail (obvious because you don’t have any code for it), then write the code to make the test pass, then refactor the code to refine it in multiple aspects: code design, smaller functions, and good names; basically, clean code.

The current understanding of TDD was described by Kent Beck based on an idea he discovered (or rediscovered as he prefers to label it) in an old programming book:

  1. You should write new business code only when an automated test has failed.
  2. You should eliminate any duplication that you find.

Kent Beck described the discovery as follows:

The original description of TDD was in an ancient book about programming. It said you take the input tape, manually type in the output tape you expect, then program until the actual output tape matches the expected output. After I’d written the first xUnit framework in Smalltalk, I remembered reading this and tried it out. That was the origin of TDD for me. When describing TDD to older programmers, I often hear, “Of course. How else could you program?” Therefore, I refer to my role as “rediscovering” TDD.

You can find his explanation on Quora.

There are multiple approaches to achieve TDD, I personally use the Uncle Bob way:

  1. You are not allowed to write any production code unless it is to make a failing unit test pass.
  2. You are not allowed to write any more of a unit test than is sufficient to fail, and compilation failures are failures.
  3. You are not allowed to write any more production code than is sufficient to pass the one failing unit test.

Benefits of TDD

TDD has many benefits for your code design and for your productivity as a developer because you will get a lot of things done. Here’s what I love about TDD:

  • It creates documentation on how to use the code.
  • TDD Improves the code architecture and the code quality.
  • TDD reduces the time required for software development because you have less time dealing with the debugger and bug chasing.
  • TDD helps you achieve good code quality and high code coverage
  • With TDD, you are creating a testable code, so consequently, you will use all the best practices of good software development such as dependency injection.

Importantly, when using TDD, for each failed test you are writing production code, which means each line written was to pass a failed test, and with that, all your code is of high quality.

When should you start TDD

You shouldn’t start TDD immediately because it takes a lot of time to grasp the ideas behind it, and there’s a high probability that you will do it wrong in the beginning.

You should work on an experimental project instead of trying it directly in your live projects. I advise you to start by solving simple project or code exercises using TDD. For example, try to solve the TDD katas here. These exercises are simple but very effective for learning TDD before you bring it to your project, and trust me, at first you will find it slow, then you will adapt to it to make it much faster. I recommend watching TDD demos to see how TDD is performed in real-world scenarios. Here is a good place to start: TDD Demo with Kotlin by Uncle Bob.

You can also watch this video from the First International TDD Conference, held in July 2021.

Keys to Success: Learn More

To be good at anything, you should be a constant learner, especially in the world of testing in Android because it is not always trivial. I advise you to read the following documentation and articles but also check Stack Overflow for how to test things. for example, I had an issue when testing if a UI Element is not present in the User Interface, so I searched for it and learned the solution. You won’t find this information directly, but it’s a matter of searching and learning. also, always try to read other people code and their testing strategies.

Summary

To wrap things up, we can certainly assume that code testing is the best thing you can do to improve your code quality and your software project. It may look time-consuming, especially if you are using TDD; but trust me, these processes will give you a lot of value as your projects move along the software development lifecycle.

I hope that you will start testing if you are not doing that already, and I hoped you enjoyed this article. I know testing in Android takes extra time, but it’s something we can’t escape and should plan for in our project roadmaps.

About the author

Younes Charfaoui is a Google Certified Android Developer and Github Campus Expert. Besides his work as a software engineer, he’s also an author and speaker, with interests in Android and Machine Learning. You can find him on Github.

Give us your feedback

We’d like to know what you thought about the article and if there’s any other tech related content you’d like to see from us: https://www.surveymonkey.com/r/CQW52JK?uuid=[uuid_value]&n=[n_value]

By | 2021-08-31T13:23:29+00:00 August 31st, 2021|Uncategorized|0 Comments

About the Author:

Leave A Comment