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!