How to Build Effective Tests for Your Android App

For starters, there are many approaches to classifying types of software testing. Let’s take a look at the most common ones. According to the level of automation, there’s manual testing (performed without testing software) and automated testing (performed with testing software). This article focuses on automated testing.

Testing can be done at different levels and with varying degrees of detail:

  • Modular testing (with unit tests) is applied to check whether separate software modules perform correctly.
  • Integration testing checks the connections between modules.
  • System testing checks if the whole software system is performing correctly.

Within the Android framework, we can conduct all three types of testing. Modular testing is the most detailed. With its help, we can test standalone modules (classes and methods). Integration testing is more high-level: while testing how several modules interact, we can conduct tests on a certain screen of the application. System testing is carried out at a higher level, testing several screens at once or the application as a whole.

android testing
Running tests on mobile devices is more reliable because emulators can’t imitate all the processes that happen on a real device

There are also two other types of testing in the Android framework: local testing and instrumental testing. The main difference between these two testing environments is where the test is actually performed. Local tests are always performed at the level of the JVM (Java virtual machine), while instrumental tests are run on an emulator or physical device. Modular and occasionally integration tests can be performed locally. Within the scope of instrumental testing, end-to-end testing (system tests) and integration testing are usually performed.

In this article, we describe modular and integration testing based on the JVM. Let’s start with modular testing.

Modular testing

The purpose of modular testing is to isolate individual components of the application and verify their functionality.
For modular testing, Android developers use the JUnit library. JUnit is a simple open-source framework for writing repetitive tests. These tests are run on your local computer and compiled to be launched on the JVM to minimize the run time.

Let’s look at a basic example to get familiar with the main features of JUnit:

class TestExample {

	private var testObject: TestObject? = null

	@Before
	fun setUp(){
    	testObject = TestObject("testName")
	}

	@Test
	fun testSomeBehavior(){
    	assertNotNull(testObject)
    	assertEquals("testName", testObject?.name)
	}

	@After
	fun tearDown(){
    	testObject = null

Let’s take a look at what’s going on here. With the @Test annotation, we mark methods responsible for the test logic. In our example, we’re checking whether an object was actually created and whether the attribute of the created object corresponds to the specified condition. Two other annotations – @Before and @After – indicate the methods that will be called before and after each test, respectively.

In order to cover an actual Android-based app with tests, JUnit is not enough

They’re convenient to use for initial setup and for cleaning data to prepare it for the test. You can also add a @BeforeClass annotation to the method that will be executed before all tests in the current test class and an @AfterClass annotation to the method that will be executed after all tests in the current test class.

Note that in order to cover an actual Android-based app with tests, JUnit is not enough.

Let’s imagine we’re testing a class. Most likely, this class will have some dependencies that need to be replaced or imitated in the test environment. There are several ways to provide dependencies in the test environment (creating real test objects, creating mockups with the help of additional libraries, via dependency injection libraries), but we won’t talk about all of them in this article. In most cases, libraries are used, as building new test objects takes longer.

The types of dependencies associated with your tests typically determine which tool you use:

  • If your tests have minimal dependencies on the Android framework, or if they depend only on your own objects, it’s fine to include mock dependencies using a mocking framework like Mockito.
  • If you have dependencies on the Android framework, particularly those that create complex interactions with the framework, it’s better to include framework dependencies using Robolectric.

For instance, say we have an application that displays a list of popular Reddit API publications.
The Model–View–Presenter (MVP) architecture is used to build the user interface. The app consists of a single Activity and a single Presenter. The latter is responsible for business logic not related to the UI and additional classes (model classes, APIs, etc.).

Here’s what the Presenter class looks like:

class SubrreditPresenter(val redditRepository: RedditRepository) : BasePresenterImpl() {

	val disposables = CompositeDisposable()

	fun loadFirstDataSubreddit(limit: Int){
    	disposables.add(redditRepository.getGeneralData(limit)
            	.doOnSubscribe { view?.showLoading() }
            	.doAfterTerminate { view?.hideLoading() }
            	.subscribe({data: List -> view?.loadSubreddit(data) },
                    	{ throwable: Throwable -> view?.showErrorMessage()}))
	}

	fun unsubscribe(){
	    disposables.dispose()
	}
}

Our Presenter doesn’t have any Android dependencies and, therefore, can be tested within the Mockito framework. At this point, we can test whether the app successfully receives the list of publications and passes them for display.

This means we need to check that after we request the View to publish, the getGeneralData, loadSubreddit, and showErrorMessage methods are called. We can conduct a check with Mockito. We just need to mock the View object and use the Verify method to check if a certain method of a mock object has been called.

To test the Presenter, we need to replace the test response from the repository. For this purpose, we create a simple repository test class that returns an empty list when requesting any number of publications except for 0 (in which case it shows an error message to test the error case).

import org.junit.After
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.Matchers
import org.mockito.Mock
import org.mockito.Mockito.*
import org.mockito.runners.MockitoJUnitRunner

const val PAGE_COUNT_TEST = 5
const val PAGE_COUNT_TEST_FOR_ERROR_SIMULATE = 0

@RunWith(MockitoJUnitRunner::class)
class SubRedditTest {

	private lateinit var presenter: SubrreditPresenter

	@Mock
	private lateinit var view: SubredditView

	@Before
	fun serUp(){
    	presenter = SubrreditPresenter(TestRedditRepositoryIml())
  	  presenter.attachView(view)
	}

	@Test
	fun obtainListSubreddit(){
    	presenter.loadFirstDataSubreddit(PAGE_COUNT_TEST)
    	verify(view).showLoading()
    	verify(view).hideLoading()
    	verify(view).loadSubreddit(Matchers.anyListOf(SubredditData::class.java))
	}

	@Test
	fun failObtainingListSubreddit(){
    	presenter.loadFirstDataSubreddit(PAGE_COUNT_TEST_FOR_ERROR_SIMULATE)
    	verify(view).showLoading()
    	verify(view).hideLoading()
    	verify(view).showErrorMessage()
	}

	@After
	fun termDown(){
    	presenter.detachView()
	}
}

Integration testing

Now let’s learn a thing or two about integration testing. Integration tests can be run locally or on a physical device. However, since the latter method requires more time than local modular tests, developers tend to use it to evaluate the performance of the application on specific device hardware.

Integration testing checks the interaction between several modules. In our case, it will be a separate screen (Activity). If, while testing the Presenter, we’re checking that certain methods were called in the View, now we need to check that the UI is being correctly rebuilt after these calls (if the list of publications is loaded and displayed, for example). Therefore, we will conduct a basic UI check within the scope of modular testing. Deeper and wider testing of the UI (like a system test carried out on the black box principle) is better performed using instrumental testing (on a physical device or emulator).

Let’s take a look at our Activity class:

class SubredditActivity : BaseActivity(), SubredditView, SubredditAdapter.OnItemClickListener  {

	override var presenter: SubrreditPresenter = SubrreditPresenter(RedditRepositoryImpl())

	private val subredditAdapter: SubredditAdapter? = SubredditAdapter(this)


	override fun onCreate(savedInstanceState: Bundle?) {
    	super.onCreate(savedInstanceState)
    	setContentView(R.layout.activity_subreddit)

    	recyclerView.layoutManager = LinearLayoutManager(this)
    	recyclerView.adapter = subredditAdapter

    	presenter.loadFirstDataSubreddit(PAGE_COUNT)
	}

	override fun showErrorMessage() {
    	super.showError(R.string.txt_toast_net_connection)
    	recyclerView.gone()
    	emptyView.visible()
	}

	override fun loadSubreddit(list: List) {
    	recyclerView.visible()
    	emptyView.gone()
    	subredditAdapter?.addAll(list)
	}

	override fun onItemClick(view: View, subredditData: SubredditData) {
    	createCustomTabs().apply { launchUrl(this@SubredditActivity, Uri.parse(subredditData.url)) }
	}

	override fun onDestroy() {
    	presenter.unsubscribe()
    	super.onDestroy()
	}
}

Since this class has a strong connection with the Android SDK, we can’t test it with the Mockito library alone. Therefore, we need additional tools for testing Android-based apps.

One of the most popular tools for working with Android classes within JUnit is Robolectric. Robolectric is a framework that provides fast and reliable Android unit tests. All of these tests are performed inside the JVM, simulating a runtime environment for Android 4.1 (API level 16) or higher.

This allows you to test code that depends on the platform without needing to use an emulator or fictitious objects. Thanks to that, you can skip such stages as dexing, packaging, and installation on the emulator, reducing testing cycles from minutes to seconds. There’s also the AndroidX Test library, released as part of Jetpack. It provides common testing APIs for test environments and includes Robolectric testing tools. Note that the Robolectric team recommends using the new AndroidX API interface instead of APIs unsuited for Robolectric, which will soon be deprecated and deleted.

Robolectric has a very limited set of APIs for communicating with the View. Usually, Android SDK APIs (such as Activity.findViewById) are used for[this purpose. These APIs are safe, as Robolectric tests don’t have to be in charge of the synchronization between the test threads and the user interface. An additional tool, Espresso, is usually used to test the UI. This library helps to choose matching and interaction for instrumental testing. Starting with Robolectric 4.0, Espresso APIs are now supported within Robolectric tests; thus, they are also available for local JVM tests.

testing tools
You can pair Robolectric with Espresso to test your View

Espresso is the main component for interacting with the View (via onView and onData). Let’s look at the main components of Espresso:

  • ViewMatchers is a collection of elements implementing the Matcher interface. They are passed to the onView method to search for the View in the current hierarchy of views.
  • ViewActions is a collection of ViewAction objects that set some action – for example, click, longClick, typeText, and others.
  • ViewAssertions is a collection of ViewAssertion objects that are passed to the ViewInteraction.check method for comparing the state of the View – for example, matches.

Now let’s look at a short example. We will test our Activity, which includes two cases: successfully loading and displaying a list of publications on the screen and opening a screen with additional information about the publication selected from the list:

@RunWith(AndroidJUnit4::class)
class SubRedditTestRobolectric {

	@Test
	fun successLoadAndDisplayReddit() {
    	launch(SubredditActivity::class.java).use { scenario →
        	// Moves the activity state to State.CREATED.
        	scenario.moveToState(Lifecycle.State.CREATED)
        	// Moves the activity state to State.STARTED.
        	scenario.moveToState(Lifecycle.State.STARTED) 
        	// Moves the activity state to State.RESUMED. 
        	scenario.moveToState(Lifecycle.State.RESUMED)	

        	scenario.onActivity {
            	onView(withId(R.id.emptyView)).check(matches(not(isDisplayed())))
            	onView(withId(R.id.recyclerView)).check(matches(isDisplayed()))
        	}
        }
	}

	@Test
	fun openItemDitailsCorrect() {
    	Intents.init()
    	launch(SubredditActivity::class.java).use { scenario →
        	// Moves the activity state to State.CREATED.
        	scenario.moveToState(Lifecycle.State.CREATED)
        	// Moves the activity state to State.STARTED.
        	scenario.moveToState(Lifecycle.State.STARTED)
        	// Moves the activity state to State.RESUMED.  
        	scenario.moveToState(Lifecycle.State.RESUMED)	

        	scenario.onActivity {
            	onView(withId(R.id.recyclerView))
                        .perform(actionOnItemAtPosition(1, click()))
       	     Intents.intended(hasComponent(SubredditDetailsActivity::class.java.name))
        	}
    	}
   	 Intents.release()
	}
}

There’s a common block in both test methods: start and movement through certain states in the lifecycle of an Activity. We perform the latter using ActivityScenario, which is part of the AndroidX API. ActivityScenario provides an API to start and manage the Activity lifecycle status for testing purposes. ActivityScenario works with arbitrary actions and works consistently on different versions of the Android platform. This class is a replacement for ActivityController in Robolectric and ActivityTestRule in ATSL.

Now we launch action data in the main thread of the current activity using the ActivityScenario.onActivity method. In our first test case, we use Espresso to find the necessary View by its identifier and check its state. We need to know that the list with items is displayed and that the View doesn’t show an error.

In the second case, we need to check that a screen with a description of the publication opens when we click on the first item in the list. How exactly can we check that? For these cases, Espresso provides an additional espresso-intents package, which is used to check and plug intents for sealed testing. Intents are noted from the moment we call Intents.init. After that, we just compare intents aimed at one class using Intents.intended(hasComponent (“short class name”)), and then we clear the Intents state with Intents.release.

This test is to be performed on a local JVM using Robolectric. It can also be run on an Android device or emulator, but to do so, you have to place it in the androidTest folder and add additional dependencies to the build.gradle file.

Wrapping up

In this article, you learned the basics of testing and testing tools for Android. Covering an application with tests is quite a time-consuming process. However, by constantly running tests in your application, you can check the correctness, functionality, and usability of the app. This way you’ll receive a stable product.

This article describes only the most basic features of popular frameworks for testing Android apps. Each of them possesses a large list of features that are necessary for testing actual projects. In addition to the mentioned libraries, there are other useful tools for testing (UI Automator, App Crawler, etc.). They greatly simplify the testing process and make it more convenient.

Software Quality Assurance Services
Are you planning to power your business with a smart software solution? We’ll help you build reliable and high-performing software for your business needs