Site icon Mobile App Development Services

Testing Android Applications With Perfect Coverage

Code testing cartoon banner. Functional test, methodology of programming, search errors and bugs, website platform development, dashboard usability optimization for computer pc vector illustration

Android apps could be tested in different ways. For example: functional, performance,accessibility and compatibility testing. And also scope wise depending on size, or degree of isolation of the test. For example, Unit tests (small test) are only used to test small parts of an application. End to end testing (big tests) are to test a large part of an application like a flow or whole screen. Medium tests check integration between two or more units.

There are several types of tests used in mobile app development. UI tests, which are also known as instrumental tests, verify the complete flow and integration among modules on a physical device. Local tests or host-side tests, on the other hand, are small tests that run quickly and in isolation on a development machine or server.

Sometimes, local tests need to be run on simulators, such as SQLite databases, which must be tested on multiple devices. Alternatively, big tests, also known as end-to-end tests, can be run on a machine by using an Android emulator like Robolectric.

Test coverage 

Ideally you want to test each and every line of your code which is a slow and costly process. You have to find out which test needs to run on a real or emulated device or which should run on a local workstation. High fidelity tests run on emulators which are slow and require more resources, so you should select them carefully.  While lower fidelity could be run on the workstation’s JVM. 

You must write the application architecture testable else you will not be able to test it perfectly and may be unable to test complete code. 

Write decoupled code to make it more testable, like your code must not be dependent on the other code that has higher dependencies.

Let’s see how our testing code looks like both local most likely unit and instrumental tests like UI testing.

 Local test 

UI test

Libraries 

After 1969 since software testing became a billion dollar market there are many methodologies, tools and libraries being used to ensure software quality. Before these power tools and libraries debugging and manual testing was a widely adaptive way to ensure the software releases without bugs. 

In Android, for local tests, we usually use JUnit and Mockito (to mock the objects). And for instrumentation testing, Espresso is mostly used. Robolectric is another famous library in Android testing that lets you test Android framework specific tests on local machines (using JVM) which robust the test run time much more than running on real devices. 

In this blog, we just have an introductory view of the libraries. 

In the example, you might notice how the assertEquals method (by JUnit) helps us to test our focus code in all cases. 

And also Expresso provides different helpful methods like onView to get the reference of the UI element and perform different actions on them with the help of different methods like perform and check.

Example

Let’s take an example of a ride hailing use case, I just mock the code to make it simple. All the parts are divided into units as much as possible so we could have good test coverage and easily find out which part of the code is not working. 

Here is our class responsible for finding the ride logic. 

package com.example.testingandroid

import kotlin.random.Random

class FindRides {

    fun getNearByRides(inMiles : Int? = null, inTime : Int? = null,bySeats : Int? = null,carType : String? = null,rides : ArrayList<Ride>): ArrayList<Ride> {
        var availableRide = rides
        availableRide = getRidesByMiles(inMiles,availableRide)
        availableRide = getRidesByTime(inTime,availableRide)
        availableRide = getRidesBySeatAvailable(bySeats,availableRide)
        availableRide = getRidesByCarType(carType,availableRide)
        return availableRide
    }

    fun getRidesByMiles(inMiles : Int? = null, rides : ArrayList<Ride>): ArrayList<Ride> {
        return if (inMiles != null) {
            ArrayList<Ride>( rides.filter {
                it.distance!! <= inMiles
            })
        } else{
            rides
        }
    }

    fun getRidesByTime(inTime : Int? = null, rides : ArrayList<Ride>): ArrayList<Ride> {
        return if (inTime != null) {
            ArrayList<Ride>(
                rides.filter {
                    it.time <= inTime
                })
        } else{
            rides
        }
    }

    fun getRidesBySeatAvailable(bySeats : Int? = null, rides : ArrayList<Ride>): ArrayList<Ride> {
        return if (bySeats != null) {
            ArrayList<Ride>(rides.filter {
                (it.availableSeats?:0) >= bySeats
            })
        } else{
            rides
        }
    }

    fun getRidesByCarType(carType : String? = null, rides : ArrayList<Ride>): ArrayList<Ride> {
        return if (carType != null) {
            ArrayList<Ride>( rides.filter {
                it.carType.toString() == carType
            })
        }else{
            rides
        }
    }

    fun fakeAvailableRide(): ArrayList<Ride> {
        val availableRide = arrayListOf<Ride>()

        val types = arrayListOf<String>()
        types.add("Luxury")
        types.add("Economy")
        types.add("Saver")
        types.add("Business")

        repeat(Random.nextInt(10,20)) {
            val ride = Ride(
                types[Random.nextInt(0,3)],
                "Car $it",
                (Random.nextInt(1,30)),
                Random.nextInt(1,30),
                "Driver " + Random.nextInt(10,99),
                Random.nextInt(1,4)
            )
            availableRide.add(ride)
        }
        return availableRide
    }

    fun findNearestRider(rides : ArrayList<Ride>): Ride? {
        return  rides.minByOrNull { (it.time)   }
    }

}

Here are unit and integration tests of the case

package com.example.testingandroid

import org.junit.Assert.*
import org.junit.Rule
import org.junit.Test
import kotlin.random.Random

/**
 * Example local unit test, which will execute on the development machine (host).
 *
 * See [testing documentation](http://d.android.com/tools/testing).
 */
class ExampleUnitTest {

    @Test
    fun getRidesByMiles() {
        val findRides = FindRides()
        val allRides = findRides.fakeAvailableRide()
        val filterByMiles = findRides.getRidesByMiles(20,allRides)
        filterByMiles.forEach{ride->
            assertTrue((ride.distance!! <= 20))
        }

    }

    @Test
    fun getRidesByTime() {
        val findRides = FindRides()
        val allRides = findRides.fakeAvailableRide()
        val filterByTime = findRides.getRidesByTime(15,allRides)
        filterByTime.forEach{ride->
            assertTrue((ride.time <= 15))
        }
    }

    @Test
    fun getRidesBySeatAvailable() {
        val findRides = FindRides()
        val allRides = findRides.fakeAvailableRide()
        val filterByTime = findRides.getRidesBySeatAvailable(3,allRides)
        filterByTime.forEach{ride->
            assertTrue((ride.availableSeats!! <= 3))
        }
    }

    //Integration test
    @Test
    fun getRidesByCarType() {
        val findRides = FindRides()
        val allRides = findRides.fakeAvailableRide()
        val filterByTime = findRides.getRidesByCarType("Saver",allRides)
        filterByTime.forEach{ride->
            assertTrue((ride.carType == "Saver"))
        }
    }

    @Test
    fun findNearByRides(){
        val findRides = FindRides()
        var allRides = findRides.fakeAvailableRide()
        allRides = findRides.getRidesByMiles(15,allRides)
        allRides =  findRides.getRidesByTime(10,allRides)
        allRides =  findRides.getRidesBySeatAvailable(2,allRides)
        allRides =  findRides.getRidesByCarType("Saver",allRides)
        allRides.forEach{ride->
            assertTrue((ride.distance!! <= 15))
            assertTrue((ride.time <= 10))
            assertTrue((ride.availableSeats!! >= 2))
            assertTrue((ride.carType == "Saver"))
        }
    }
}

findNearByRides is an integration test, where we are testing all the units together in their flow. All other cases are unit tests. 

Here is the end to end and UI testing example. Below is the Activity code.

package com.example.testingandroid

import android.os.Bundle
import android.text.method.LinkMovementMethod
import android.text.util.Linkify
import android.util.Log
import android.view.View
import android.widget.TextView
import androidx.appcompat.app.AppCompatActivity

class MainActivity : AppCompatActivity() {

    var MILES : Int?= 0

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        val findRides = FindRides()
        val availableRide = findRides.fakeAvailableRide()
        val nearByRides = findRides.getNearByRides(5,20,1,"Saver",availableRide)
        val nearestRide = findRides.findNearestRider(nearByRides)
        val rideSummary = findViewById<TextView>(R.id.rideSummary)
        val tvTimeToReach = findViewById<TextView>(R.id.tvTimeToReach)
        val tvDistance = findViewById<TextView>(R.id.tvDistance)
        val tvAvailableSeats = findViewById<TextView>(R.id.tvAvailableSeats)

        if(nearestRide!=null) {
            rideSummary.text =
                "You rider ${nearestRide?.riderName}. is on the way on your ${nearestRide?.carType} car ${nearestRide.carName}"

            tvTimeToReach.text = "${nearestRide.time} min(s) to reach"

            tvDistance.text = "${nearestRide.distance} miles away"
            tvAvailableSeats.text = "${nearestRide.availableSeats} seat(s) are available"
        }else {
            rideSummary.text = "No rider found"
            tvTimeToReach.visibility = View.GONE
            tvDistance.visibility = View.GONE
            tvAvailableSeats.visibility = View.GONE
        }

        Log.d("Ride near by","Total ${nearByRides.size}")
    }
}

And here is the test case 

package com.example.testingandroid

import android.view.View
import android.widget.TextView
import androidx.test.core.app.ActivityScenario
import androidx.test.espresso.Espresso.onView
import androidx.test.espresso.UiController
import androidx.test.espresso.ViewAction
import androidx.test.espresso.ViewInteraction
import androidx.test.espresso.assertion.ViewAssertions.matches
import androidx.test.espresso.matcher.ViewMatchers.*
import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.ext.junit.runners.AndroidJUnit4
import org.hamcrest.CoreMatchers.*
import org.hamcrest.Matcher
import org.hamcrest.core.StringStartsWith.*

import org.junit.Test
import org.junit.runner.RunWith

import org.junit.Assert.*

/**
 * Instrumented test, which will execute on an Android device.
 *
 * See [testing documentation](http://d.android.com/tools/testing).
 */
@RunWith(AndroidJUnit4::class)
class ExampleInstrumentedTest {
    @Test
    fun useAppContext() {
        // Context of the app under test.
        val appContext = InstrumentationRegistry.getInstrumentation().targetContext
        assertEquals("com.example.testingandroid", appContext.packageName)
    }

    @Test
    fun findRide(){
        ActivityScenario.launch(MainActivity::class.java)
        onView(withId(R.id.rideSummary)).check(matches(isDisplayed()))
        onView(withText(containsString("Saver"))).check(matches(isDisplayed()))

        val numberResult: ViewInteraction = onView(withId(R.id.tvTimeToReach))
        val textTimeToReach = getText(numberResult).split(" ");
        val time = textTimeToReach[0].toInt()
        assertTrue(time<=20)

        val tvDistance: ViewInteraction = onView(withId(R.id.tvDistance))
        val textDistance = getText(tvDistance).split(" ");
        val distance = textDistance[0].toInt()
        assertTrue(distance<=5)

        val tvAvailableSeats: ViewInteraction = onView(withId(R.id.tvAvailableSeats))
        val textAvailableSeats = getText(tvAvailableSeats).split(" ");
        val availableSeats = textAvailableSeats[0].toInt()
        assertTrue(availableSeats>=1)
    }

    private fun getText(matcher: ViewInteraction): String {
        var text = String()
        matcher.perform(object : ViewAction {
            override fun getConstraints(): Matcher<View> {
                return isAssignableFrom(TextView::class.java)
            }

            override fun getDescription(): String {
                return "Text of the view"
            }

            override fun perform(uiController: UiController, view: View) {
                val tv = view as TextView
                text = tv.text.toString()
            }
        })

        return text
    }
}

The test cases that run on the device are placed in app/src/androidTest/java/com/example/testingandroid/ExampleInstrumentedTest.kt

While test cases that are run on JVM/local machine are placed in app/src/test/java/com/example/testingandroid/ExampleUnitTest.kt

Here is a full code of example. Try it out to explore more. 

https://github.com/BilalCode/AndroidTestExample

In conclusion, testing is an essential part of the software development process that helps to ensure code quality, prevent bugs, and reduce maintenance costs. We hope that this tutorial has been helpful in introducing you to the world of testing using Kotlin, and we encourage you to continue your learning journey by exploring more advanced topics and techniques. Happy testing!