Espresso : Why Manual Testing Could become a Thing of Past — An Android Perspective
Previously , I had a non-technical post which is here , now I have shifted back to technical post .
We all know that technology has been evolving day in and out and has replaced much of the manual work in the past and has made life simpler .
Today in this post , I am gonna focus on another area called Manual Testing and how Android has progressed using Espresso to completely automate it and shifted the flow of testing towards development end or Automation.
Espresso is an open source testing framework launched by Google in Oct 2013 which provides an API that allows creating UI tests to simulate user interactions in an Android application (in version 2.2). onward .
In this post we will execute one example using UI Testing performed using Espresso and the code coverage for that project .
The whole code for the project is available in git repository.
To start with you will need to add few dependencies in gradle … add few dependencies of Espresso and enable testCoverage for codeLines covered while testing .
Changes in gradle files at app level :
Dependencies addition in gradle…
dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar'])
implementation 'com.android.support:appcompat-v7:28.0.0' // For Constraint Layout
implementation 'com.android.support.constraint:constraint-layout:1.1.3' // For Java unit testing framework
testImplementation 'junit:junit:4.12' // Android runner and rules support
androidTestImplementation 'com.android.support.test:rules:1.0.2'
androidTestImplementation 'com.android.support.test:runner:1.0.2' // Espresso support
androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.2' // For Idling Resource during Espresso Testing
implementation 'com.android.support.test.espresso:espresso-idling-resource:3.0.2'
}
Once done with gradle click sync now option as seen in message at top bar of the file .
Once all the dependencies have been added , You need to create a Constraint Layout having views containing UserId & Password along with Log In Button as seen in below layout code.
<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout 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:layout_width="match_parent"
android:layout_height="match_parent">
<TextView
android:id="@+id/textView"
android:layout_width="wrap_content"
android:layout_height="19dp"
android:layout_marginStart="8dp"
android:layout_marginTop="246dp"
android:layout_marginEnd="8dp"
android:gravity="center"
android:text="@string/userid"
app:layout_constraintEnd_toStartOf="@+id/guideline"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<android.support.constraint.Guideline
android:id="@+id/guideline"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="vertical"
app:layout_constraintGuide_percent="0.3" />
<EditText
android:id="@+id/edtUserId"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:layout_marginEnd="8dp"
android:ems="10"
android:inputType="textPersonName"
android:text=""
app:layout_constraintBaseline_toBaselineOf="@+id/textView"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="@+id/guideline" />
<TextView
android:id="@+id/textView2"
android:layout_width="wrap_content"
android:layout_height="25dp"
android:layout_marginStart="8dp"
android:layout_marginTop="40dp"
android:layout_marginEnd="8dp"
android:text="@string/password"
app:layout_constraintEnd_toStartOf="@+id/guideline"
app:layout_constraintHorizontal_bias="0.513"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/textView" />
<EditText
android:id="@+id/edtPass"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:layout_marginEnd="8dp"
android:ems="10"
android:inputType="textPassword"
android:text=""
app:layout_constraintBaseline_toBaselineOf="@+id/textView2"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="@+id/guideline" />
<Button
android:id="@+id/loginBtn"
android:layout_width="wrap_content"
android:layout_height="48dp"
android:layout_marginStart="8dp"
android:layout_marginTop="24dp"
android:layout_marginEnd="8dp"
android:text="LOG IN"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/edtPass" />
</android.support.constraint.ConstraintLayout>
Once done , we need to initialize in Activity class of the corresponding layout where I consider the validation of user id & password if blank and gives out a Toast message if my User Id & Password matches else Failure on click of login .
This is a simple project where I have not connected to remote server or haven’t used any mock data as of now for testing .
You can check the file below .
package com.test.espressodemo;
import android.os.Bundle;
import android.support.v7.app.AppCompatActivity;
import android.view.View;
import android.widget.Button;
import android.widget.EditText;
import android.widget.Toast;
public class MainActivity extends AppCompatActivity {
private EditText mEditPassword;
private EditText mEditUserId;
private Button mButtonLogin;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.login_activity);
mEditPassword = (EditText) findViewById(R.id.edtPass);
mEditUserId = (EditText) findViewById(R.id.edtUserId);
mButtonLogin = (Button) findViewById(R.id.loginBtn);
mButtonLogin.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
if (validData()) {
if(mEditUserId.getText().toString().equalsIgnoreCase(mEditPassword.getText().toString()))
{
Toast.makeText(MainActivity.this,R.string.success,Toast.LENGTH_LONG).show();
}
else
{
Toast.makeText(MainActivity.this,R.string.failure,Toast.LENGTH_LONG).show();
}
}
}
});
}
boolean validData() {
if (mEditUserId.getText().toString().trim().equalsIgnoreCase("")) {
return false;
}
if (mEditPassword.getText().toString().trim().equalsIgnoreCase("")) {
return false;
}
return true;
}
}
Once done you have created your basic project with one layout and one Activity which has its functionality . Now take a closer look what points will you consider for testing .
For me , below where the points for which I created 4 different Test Cases in MainActivityTest file under androidTest/java folder .
- Check if there is valid/blank input data for User Id .
- Check if there is valid/blank input data for Password .
- Check if the app logs in successfully .
4. Check if the app login failure scenario works properly .
Once finalised , I have created a new file called MainActivityTest.java in androidTest folder as it will be used to write the test cases used for MainActivity file .
For each new Activity file a separate Test file is to be created in androidTest folder as a good practice with file name same as Activity’s name and Test appended at end.
Here in this case
We start with annotations at the start to indicate testing file having LargeTest suite.
@RunWith(AndroidJUnit4.class)
@LargeTest
Add below line which indicates to which file or activity the testing file belongs and we annotate with @Rule annotation.
@Rule
public ActivityTestRule<MainActivity> mainActivityActivityTestRule = new ActivityTestRule<>(MainActivity.class);
There are basic annotations to be known to run particular functions as in order of preference . These annotations are as follows
- @BeforeClass — Run once before any of the test methods in the class, as the name suggests it will run only once in entire class . Used basically for creating database connection , connection pool requests etc
- @AfterClass — Run once after all the tests in the class have been run, as the name suggests it will run only once in entire class . Used basically for closing database connection , clean up purpose etc
- @Before — Run before @Test, this method runs before each function having @Test Annotation which means it can run multiple times depending on functions having @Test annotations.
- @After — Run after @Test,this method runs after each function having @Test Annotation which means it can run multiple times depending on functions having @Test annotations.
- @Test — This is the test method to run .
In this project I have used @Before to define my one time string data and then @Test annotation to write 4 different Test Cases to complete my one Large Test Suite in class file .
@Before
public void initValidString() {
// Specify a valid string.
mStringToBetyped = "Espresso";
mValidPass = "Espresso";
mInValidPass = "Invalid";
}
Once knowing the meaning of each annotations used you can proceed towards functions which have @Test and keywords used in these functions ,
The main components of Espresso include the following:
- Espresso — Entry point to interactions with views (via
onView()
andonData()
). Also exposes APIs that are not necessarily tied to any view, such aspressBack()
. - ViewMatchers — A collection of objects that implement the
Matcher<? super View>
interface. You can pass one or more of these to theonView()
method to locate a view within the current view hierarchy. - ViewActions — A collection of
ViewAction
objects that can be passed to theViewInteraction.perform()
method, such asclick()
. - ViewAssertions — A collection of
ViewAssertion
objects that can be passed theViewInteraction.check()
method. Most of the time, you will use the matches assertion, which uses a View matcher to assert the state of the currently selected view.
The code snippet in my project goes as below for Valid Login Test:
// Code snippet for Valid Test Login@Test
public void validLogin() {
// Enter View User Id with Espresso word and close soft keyboard
onView(withId(R.id.edtUserId))
.perform(typeText(mStringToBetyped), closeSoftKeyboard());
// Enter View User Id with Espresso word and close soft keyboard
onView(withId(R.id.edtPass))
.perform(typeText(mValidPass), closeSoftKeyboard());
// Click on Login Button
onView(withId(R.id.loginBtn)).perform(click());
/* Once Clicked on Login we have set condition if Login text Content matches Password Content then The Login is a success */
// Fetch Content of Password Edit Text using Matcher
String textFromPasswordFld = getText(withId(R.id.edtPass));
//Check User Id Content with Content Retreived from Password Edit Text Field
onView(withId(R.id.edtUserId)).check(matches(isEditTextValueEqualTo(R.id.edtUserId, textFromPasswordFld)));
try {
Thread.sleep(10000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
You can check others in the project file MainActivityTest.java
To cut short Below I have a cheat sheet used in Espresso .
So, Once you have written and understood all the code we can proceed further to run the test project if you have written all test cases correctly , you will get results for individual Test cases as seen in image below after running it .
If all the test cases that are executed perfectly you get this left side image .
If any of the test case gets failed then you get information as seen in image on left side .
Moving towards CODE COVERAGE :
Once Done with Testing we will move on step further and check code coverage performed by that Testing for this we need to add below changes in gradle file and sync again .
buildTypes {
debug // For debug mode
{
// Code coverage
testCoverageEnabled = true
}
....
After that you will need to run a command in Terminal of Android Studio
gradlew createDebugCoverageReport
which will generate a report in
YourProjectName\app\build\reports\coverage\debug\index.html
You can open index.html file and check out the coverage report in your preferred browser and if you get 100% coverage report its AWESOME.
That’s it about Espresso Testing in this post , see you in next one .