Testing in Android with Espresso - Part 1 (Setup and Basics)
Jul 23, 2018 • 15 Minute Read
Setup and Basics
One of the integral phases of application development life cycle is testing but the question is why?
The answer is to eliminate the occurrence of ANR dialog or unexpected behavior in application logic. For Android, all UI updation is handled by the main thread and if the main thread is unable to respond to user events within 5 seconds then it causes the application to crash and gives the user an option to close the application instead of keeping the user in an infinite waiting state, resulting in poor user experience.
Application Not Responding dialog is the least expected event that can occur when the main thread (aka UI thread) is blocked for too long. Some instances that may cause this are:
-
Rendering a larger image
-
Performing REST calls on main thread
-
Or performing any long calculation
Other reasons are Exceptions, infinite recursive method calls, missing class definitions, deadlocks ,or implementation of unsupported features on older versions like vector drawable etc.
Test Driven Development
TDD is a recursive cycle of writing code, testing, and refactoring the code to resolve bugs the moment they appear. TDD lays out the approach to test the working of the application after every modification in code base. The objectives of TDD are:
-
To meet the requirements that guided the application design and development
-
To determine the performance of an application under different environments considering
-
Network connectivity
-
User input
-
Server stability
-
Battery, CPU, and memory optimization (memory leaks)
-
-
To validate the application security against SQL injections and data security vulnerabilities
-
To verify the working of application specific APIs
"Testing leads to failure and failure leads to understanding :- Burt Rutan"
Unit and Instrumentation Testing
Unit Testing : Unit tests run on the local machine, i.e. Java Virtual Machine (JVM), which means it requires no device or emulator. Unit tests tend to be fast because units test don't test any Android specific dependency (Activity,SharedPreferenc) or mock objects are used to mimic the behavior of android framework dependencies.
Unit tests are located under package-name/src/test/java/
Instrumentation Testing : Instrumentation tests are specifically designed to test the UI of an application and requires an emulator or physical device to perform tests. Android UI components depend upon Context to access the resources in an application like xml, images, string, and other resources. Context is provided by the Android operating system, so for instrumentation testing the context is provided by the Instrumentation API to track the interaction between android OS and to application i.e. to give commands to activity life cycle, environment details etc.
Instrumentation tests are located under package-name/src/androidTest/java/
Testing Terminologies
- AndroidJUnitRunner : AndroidJUnitRunner is a JUnit test runner that lets you run JUnit tests on Android devices while providing annotations to ease-up the testing procedure. For instrumentation tests, the test class must be prefixed with @RunWith(AndroidJUnit4.class).
- Annotations : @Test Annotation is used to mark a method for testing and methods with @Before annotation are executed first to setup the environment for testing and methods with the @After annotation executed at the end. @LargeTest annotation is used to indicate that the test duration can be greater than 1 second.
- Espresso : It is a testing framework for Android to write tests for user interface interaction.
- Mockito : mockito is often used for unit testing to create dummy objects to mimic the behavior of objects.
Instrumentation Testing with Espresso
Espresso is a testing framework by Google for UI (user-interface) testing which includes every View component like buttons, List, Fragments etc. Espresso is collection of testing APIs, specifically designed for instrumentation/UI testing. UI tests ensure that users don’t have poor interactions or encounter unexpected behaviors.
Espresso has various options to find 'View' on screen, perform actions, and verify visibility and state/content of 'View'.
onView(withId(R.id.my_view)) // onView() is a ViewMatcher</strong>
.perform(click()) // click() is a ViewAction</strong>
.check(matches(isDisplayed()));// matches(isDisplayed()) is a ViewAssertion
-
ViewMatcher : used to locate the view in the UI hierarchy(tree structure of xml layout components) using withId(R.id.id_of_view), withText("find by text on view").
-
ViewActions : used to perform a specific action or group of actions in the UI views using ViewInteraction.perform(click(),doubleClick()) or click(), longClick(), doubleClick(), swipeDown(), swipeLeft(), swipeRight(), swipeUp(), typeText(), pressKey(), clearText(), etc.
-
ViewAssertion : used to assert view’s state using ViewInteraction.check(assertion method) where assertion methods can be isDisplayed(), isEnabled(), isRoot().
Setup
- Add espresso dependencies in build.gradle app module:
dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar'])
// AndroidJUnitRunner and JUnit Rules
androidTestCompile 'com.android.support.test:runner:1.0.1'
androidTestCompile 'com.android.support.test:rules:1.0.1'
// espresso support
androidTestImplementation('com.android.support.test.espresso:espresso-core:3.0.1', {
exclude group: 'com.android.support', module: 'support-annotations'
})
androidTestCompile 'com.android.support.test.espresso:espresso-contrib:3.0.1'
// for intent mocking
androidTestCompile 'com.android.support.test.espresso:espresso-intents:3.0.1'
// for network testing to track idle state
androidTestCompile 'com.android.support.test.espresso.idling:idling-concurrent:3.0.1'
androidTestCompile 'com.android.support.test.espresso:espresso-idling-resource:3.0.1'
// other dependencies
}
- To enable instrumentation testing, add the following dependency:
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
Complete build.gradle(app) file:
apply plugin: 'com.android.application'
android {
compileSdkVersion 26
buildToolsVersion "26.0.1"
defaultConfig {
applicationId "com.pavneet_singh.espressotestingdemo"
minSdkVersion 15
targetSdkVersion 26
versionCode 1
versionName "1.0"
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
}
}
dependencies {
//as mentioned above
}
The values for library version can differ in your project due to future updates and the rest of the configuration values are project dependent.
Turning-off Default Device Animation
The default animations can cause issues during the Espresso testing process, so it is recommended to turn-off the animation on the device and emulator before testing.
-
Open Settings
-
Select Developer Options (if not activated, then click several times on build version/number in About Device Settings)
-
Disable:
-
Window animation scale
-
Transition animation scale
-
Animator duration scale
-
First Espresso Test with Buttons and EditText
MainActivity which contains Button and EdiTtext, on which we will perform the testing to verify visibility, entered values, and click operation.
Let's Setup MainActivity with the Following Code
activity_main.xml layout source code for MainActivity
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context="com.pavneet_singh.MainActivity">
<EditText
android:layout_marginTop="30dp"
android:id="@+id/editTextName"
android:hint="Enter Name"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:ems="10"
android:inputType="textPersonName" />
<Button
android:onClick="setDefaultText"
android:layout_marginTop="60dp"
android:id="@+id/button"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Clear" />
<android.support.design.widget.FloatingActionButton
android:id="@+id/fab"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="bottom|end"
android:layout_margin="@dimen/fab_margin"
app:srcCompat="@android:drawable/ic_dialog_email" />
</LinearLayout>
The onClick attribute in xml points to a public method to handle click events in MainActivity as shown below:
package com.pavneet_singh;
import android.content.Intent;
import android.os.Bundle;
import android.support.design.widget.FloatingActionButton;
import android.support.design.widget.Snackbar;
import android.support.v7.app.AppCompatActivity;
import android.support.v7.widget.Toolbar;
import android.view.View;
import android.widget.EditText;
import com.pavneet_singh.espressotestingdemo.FruitListActivity;
import com.pavneet_singh.espressotestingdemo.R;
public class MainActivity extends AppCompatActivity {
private EditText editTextName;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
editTextName = (EditText)findViewById(R.id.editTextName);
FloatingActionButton fab = (FloatingActionButton) findViewById(R.id.fab);
fab.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
startActivity(new Intent(MainActivity.this, FruitListActivity.class));
}
});
}
public void setDefaultText(View view) {
editTextName.setText("");
}
}
Writing an Espresso Test
To Add a test class corresponding to MainActivity
-
Open the Java file containing the code you want to test.
-
Click the class or method you want to test, then press Ctrl+Shift+T.
-
In the menu that appears, click Create New Test.
-
In the Create Test dialog, edit any fields and select any methods to generate, then click OK.
-
In the Choose Destination Directory dialog i.e. androidTest for an instrumented test click OK.
Note : Generating your test class in the same package of the original class which is being tested, will give you access to everything inside that class and package without making them public.
Add the following code into MainActivityTest class:
package com.pavneet_singh;
// Note the static imports, which enhance the code clarity by reducing code length
import static android.support.test.espresso.Espresso.onView;
import static android.support.test.espresso.action.ViewActions.click;
import static android.support.test.espresso.action.ViewActions.closeSoftKeyboard;
import static android.support.test.espresso.action.ViewActions.typeText;
import static android.support.test.espresso.matcher.ViewMatchers.withHint;
import static android.support.test.espresso.matcher.ViewMatchers.withId;
import static android.support.test.espresso.assertion.ViewAssertions.matches;
import static android.support.test.espresso.matcher.ViewMatchers.withText;
import android.support.test.espresso.ViewAssertion;
import android.support.test.espresso.action.ViewActions;
import android.support.test.filters.LargeTest;
import android.support.test.rule.ActivityTestRule;
import android.support.test.runner.AndroidJUnit4;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import com.pavneet_singh.espressotestingdemo.R;
@RunWith(AndroidJUnit4.class)
@LargeTest
public class MainActivityTest {
// To launch the mentioned activity under testing
@Rule
public ActivityTestRule<MainActivity> mActivityRule = new ActivityTestRule<>(MainActivity.class);
@Test
public void testHintVisibility(){
// check hint visibility
onView(withId(R.id.editTextName)).check(matches(withHint("Enter Name")));
// enter name
onView(withId(R.id.editTextName)).perform(typeText("Pavneet"),closeSoftKeyboard());
onView(withId(R.id.editTextName)).check(matches(withText("Pavneet")));
}
@Test
public void testButtonClick(){
// enter name`
onView(withId(R.id.editTextName)).perform(typeText("Pavneet"),closeSoftKeyboard());
// clear text
onView(withText("Clear")).perform(click());
// check hint visibility after the text is cleared
onView(withId(R.id.editTextName)).check(matches(withHint("Enter Name")));
}
}
To Run Test:
-
Select the MainActivityTest under package-name/androidTest.
-
Right click on the file.
-
Select run MainActivityTest.
Or via shortcuts:
-
Windows : shift + F10
-
Mac : Control + R
onView also has an overloaded version which can accept powerful hamcrest Matcher methods to go one step beyond and perform specific operations like:
- AllOf : To perform multiple operations together.
onView(AllOf.allOf(withId(R.id.editTextName),withId(R.id.editTextSameName))).check(matches(withText("Pavneet")));
- StringContains : To match a part of the input string in the targeted view text.
onView(withId(R.id.editTextName)).check(matches(withText(Matchers.containsString("Pavneet"))));
- StringStartsWith : To find the view with prefix.
onView(withText(startsWith("prefix"))).perform(click());
References
Read Test Cases with Espresso to explore more about testing various android components and view.
I hope that testing will now help you to be a stellar programmer, you can share your love by giving this guide a thumbs up.