This project will cover samples or Android Test cases : unit and instrumentation tests. For this we used some samples and tutorials like :
- Official Android documentation
- Medium - the basis of Unit & intrumented Tests
- myKong Email Validation Tutorial
- Vogella - Android user interface testing with Espresso
- [QA Automated - Test Toast Message] (http://www.qaautomated.com/2016/01/how-to-test-toast-message-using-espresso.html)
It's about unit testing based on JUnit 4.12 Create a class named app/src/main/.../EmailValidator & app/src/test/.../EmailValidatorTest that will contains methods that test if Email validation is working properly.
package com.leadit.androidtesting.util;
import org.junit.Assert;
import org.junit.Test;
import static org.hamcrest.CoreMatchers.is;
/**
* EmailValidator unit tests class
*
* @author Mohamed Essid on 09/02/2017.
*/
public class EmailValidatorTest {
/**
* valid emails test
*/
@Test
public void testIsEmailValid() {
String[] validEmails = {"[email protected]",
"[email protected]", "[email protected]",
"[email protected]", "[email protected]",
"[email protected]", "[email protected]",
"[email protected]", "[email protected]",
"[email protected]"};
for (String email : validEmails) {
Assert.assertThat(String.format("Valid email test failed for %s ", email), EmailValidator.get().isValidEmail(email), is(true));
}
}
/**
* invalid emails test
*/
@Test
public void InvalidEmailTest() {
String[] invalidEmails = {"mkyong", "[email protected]",
"[email protected]", "[email protected]", "[email protected]",
"[email protected]", "mkyong()*@gmail.com", "mkyong@%*.com",
"[email protected]", "[email protected]",
"mkyong@[email protected]", "[email protected]"};
for (String email : invalidEmails) {
Assert.assertThat(String.format("Invalid email test failed for %s ", email), EmailValidator.get().isValidEmail(email), is(false));
}
}
}
- Create utility class named DateUtils, for date conversion & format.
- Corresponding test class DateUtilsTest which test time conversion & day.
Don't forget to adapt expected day value according to you Default Locale (I used SAM. , because my default locale is FR).
public class DateUtils {
private static final SimpleDateFormat DISPLAY;
private static final SimpleDateFormat DISPLAY_SHORT;
private static final long SECOND_MILLISECONDS = 1000l;
private static final long MINUTE_MILLISECONDS =
SECOND_MILLISECONDS * 60;
private static final long HOUR_MILLISECONDS =
MINUTE_MILLISECONDS * 60;
public static final long DAY_MILLISECONDS =
HOUR_MILLISECONDS * 24;
static {
//Use 12 or 24 hour time depending on device config.
DISPLAY = new SimpleDateFormat(
"EEEE, dd MMMM yyyy",
Locale.getDefault());
DISPLAY_SHORT = new SimpleDateFormat("EEE",
Locale.getDefault());
DISPLAY.setTimeZone(TimeZone.getDefault());
DISPLAY_SHORT.setTimeZone(TimeZone.getDefault());
}
public static Date epocSecondsToDate(long epocSeconds) {
Calendar c = Calendar.
getInstance(TimeZone.getTimeZone("UTC"));
c.setTimeInMillis(epocSeconds * 1000);
return c.getTime();
}
public static String dateToDayDateString(Date date,
boolean useShortFormat) {
if (useShortFormat) {
return DISPLAY_SHORT.format(date).toUpperCase();
} else {
return DISPLAY.format(date).toUpperCase();
}
}
public static String epocSecondsToDisplayDateTimeString(long epocSeconds) {
Date d = epocSecondsToDate(epocSeconds);
return dateToDayDateString(d, false);
}
}
#####DateUtilTest This the corresponding test class which will tests date conversion & format.
public class DateUtilsTest {
/**
* tests epoc to date conversion, and checks the day of the test date
*/
@Test
public void testDateUtilFormat() {
long epoc = 1446885450; //7th Nov 2015
///test EPOC to date conversion
Date date = DateUtils.epocSecondsToDate(epoc);
Assert.assertEquals("failed time millis conversion : ", date.getTime(), epoc * 1000);
//if EPOC conversion succeeds , test if the tested date gives a correct DAY.
String day = DateUtils.dateToDayDateString(date, true);
Assert.assertEquals("day is wrong ", "SAM.", day);
}
}
In this case, we made a class RandomUtils which has a method named randomString(int length) that returns a random alphabetic string which length that corresponds to the parameter.
Below the final RandomUtils & RandomUtilsTest classes
- RandomUtils
package com.leadit.androidtesting.util;
import java.util.Random;
import timber.log.Timber;
/**
* Random values generator utility class
*
* @author Mohamed Essid on 10/02/2017.
*/
public class RandomUtils {
/**
* generates a random string
*
* @param length of the generated string
* @return
*/
public static String randomString(int length) {
String alphabet = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";
Random random = new Random();
char[] result = new char[length];
for (int i = 0; i < result.length; i++) {
result[i] = alphabet.charAt(random.nextInt(alphabet.length() - 1));
}
String randomString = String.copyValueOf(result);
Timber.d("Random generated string %s", randomString);
return randomString;
}
}
- RandomUtilsTest
package com.leadit.androidtesting.util;
import org.junit.Assert;
import org.junit.Test;
import java.util.regex.Pattern;
import static org.hamcrest.Matchers.is;
/**
*Random Utils test class
*/
public class RandomUtilsTest {
/**
* tests random string method
*/
@Test
public void testRandomString() {
int expectedSize = 10;
String random = RandomUtils.randomString(expectedSize);
//verify that the generated string's length is as expected
Assert.assertEquals("error in generating string length", expectedSize, random.length());
//verify that string is alphabetic
Pattern pattern = Pattern.compile("[a-zA-Z]+");
Assert.assertThat(String.format("%s is not alphabetic", random), pattern.matcher(random).matches(), is(true));
}
}
From Google official documentation.
We created a class named StringUtil which contains a method named getAppName(Context). To be able to test this method, we need a mock context. that's why we used Mockito framework.
Note to :
- add Mockito dependency in app/build.gradle : testCompile 'org.mockito:mockito-core:1.10.19'
- add @RunWith(MockitoJUnitRunner.class) to run with Mockito
- add @Mock before context declaration
- simulate getting string from context with R.string.app_name Mockito.when(mMockContext.getString(R.string.app_name)).thenReturn(FAKE_STRING)
package com.leadit.androidtesting;
import android.content.Context;
import com.leadit.androidtesting.util.StringUtil;
import org.junit.Assert;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.Mockito;
import org.mockito.runners.MockitoJUnitRunner;
import static org.hamcrest.core.Is.is;
/**
* StringUtil test class
*
* @author Mohamed Essid on 09/02/2017.
*/
@RunWith(MockitoJUnitRunner.class)
public class StringUtilTest {
/**
* a fake string used in tests
*/
public static final String FAKE_STRING = "AndroidTesting";
/**
* mock context
*/
@Mock
Context mMockContext;
/**
* tests read a string from a mock context
*/
@Test
public void testReadStringFromContext() {
// Given a mocked Context injected into the object under test...
Mockito.when(mMockContext.getString(R.string.app_name)).thenReturn(FAKE_STRING);
// ...when the string is returned from the object under test...
String result = StringUtil.getAppName(mMockContext);
// ...then the result should be the expected one.
Assert.assertThat(result, is(FAKE_STRING));
}
}
Covers some Android Instrumentation tutorials made by Vogella
-
As said in the tutorial, "Espresso is a testing framework for Android to make it easy to write reliable user interface tests."
-
It relies on 3 components basically:
- ViewMatchers finding a view in current view hierarchy.
- ViewActions performing actions on view.
- ViewAssertions asserting state is the following.
Library setup, Gradle dependencies and devices configuration (more info here)
- In this test class MainActivityTest, we'll test 2 features:
- test text change on MainActivity after button click
- check that the value from input is well passed to SecondActivity
Below you can check the final MainActivityTest class:
package com.leadit.androidtesting;
import android.support.test.espresso.action.ViewActions;
import android.support.test.rule.ActivityTestRule;
import android.support.test.runner.AndroidJUnit4;
import com.leadit.androidtesting.util.Constants;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import static android.support.test.espresso.Espresso.onView;
import static android.support.test.espresso.assertion.ViewAssertions.matches;
import static android.support.test.espresso.matcher.ViewMatchers.withId;
import static android.support.test.espresso.matcher.ViewMatchers.withText;
/**
* {@link MainActivity} test class with Espresso framework
*/
@RunWith(AndroidJUnit4.class)
public class MainActivityTest {
@Rule
public ActivityTestRule<MainActivity> mActivityRule
= new ActivityTestRule<>(MainActivity.class);
/**
* tests text change operation on main activity
*/
@Test
public void testTextChange() {
//init input text with a string HELLO
onView(withId(R.id.main_input)).perform(ViewActions.typeText("HELLO"), ViewActions.closeSoftKeyboard());
//performing a click in order to change text
onView(withId(R.id.main_btn_change_text)).perform(ViewActions.click());
//check if the current value has changed
// and verify that it's value corresponds to the expected one
onView(withId(R.id.main_input)).check(matches(withText(Constants.TEST_TEXT)));
}
/**
* tests if text set on MainActivity was displayed on result view in SecondActivity
*/
@Test
public void testTextChangeSecondActivity() {
//setting text to input in MainActivity
onView(withId(R.id.main_input)).perform(ViewActions.typeText("new text"));
//performing a click in MainActivity to open SecondActivity with the text set in the
//previous operation
onView(withId(R.id.main_btn_switch)).perform(ViewActions.click());
//Now we are in SecondActivity, check that the text received matches the expected
onView(withId(R.id.second_result_view)).check(matches(withText("new text")));
}
}
In this case, we will test if SecondActivity
displays correctly the string passed in intent.
For this,
-
we removed the automatic launch of the activity under test (SecondActivity) by setting the flag
launchActivity
tofalse
as below -
Intent now is configured before launching activity from the rule as defined below, in the final
SecondActivityTest.java
/**
* Second activity class
*
* @author Mohamed Essid on 13/02/2017.
*/
public class SecondActivityTest {
//launch activity is set to false here to prevent that the activity is started automatically
//when test is launched
@Rule
public ActivityTestRule<SecondActivity> mRule =
new ActivityTestRule<>(SecondActivity.class, true, false);
@Test
public void testExtraTextDisplay() {
//prepare intent with the extra string
Intent intent = new Intent();
intent.putExtra(Constants.IntentParams.INPUT, "Hello");
mRule.launchActivity(intent);
//verify that the string passed is displayed
onView(withId(R.id.second_result_view)).check(matches(withText("Hello")));
}
}
Test if the MainActivity is correctly configuring the intent in order to launch the SecondActivity. For this, we've tested:
- Intent Package : to verify that the intent's package* corresponds to ```SecondActivity``'s package
- Intent Param : to verify the intent param
INPUT
contains the expected value.
In this test case we used android.support.test.espresso.intent.rule.IntentsTestRuleÌnetentsTestRule
The test case was implemented in MainActivityIntentTest as below:
package com.leadit.androidtesting;
import android.support.test.espresso.ViewInteraction;
import android.support.test.espresso.action.ViewActions;
import android.support.test.espresso.intent.rule.IntentsTestRule;
import android.support.test.runner.AndroidJUnit4;
import com.leadit.androidtesting.util.Constants;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import static android.support.test.espresso.Espresso.onView;
import static android.support.test.espresso.assertion.ViewAssertions.matches;
import static android.support.test.espresso.intent.Intents.intended;
import static android.support.test.espresso.intent.matcher.IntentMatchers.hasExtra;
import static android.support.test.espresso.intent.matcher.IntentMatchers.toPackage;
import static android.support.test.espresso.matcher.ViewMatchers.withId;
import static android.support.test.espresso.matcher.ViewMatchers.withText;
import static org.hamcrest.core.IsNull.notNullValue;
/**
* MainActivity intent test
* tests if activity swicth intent is well configured
*
*/
@RunWith(AndroidJUnit4.class)
public class MainActivityIntentTest {
@Rule
public IntentsTestRule<MainActivity> mActivityRule
= new IntentsTestRule<>(MainActivity.class);
/**
* perform init operations need in this test case
* and tests activity switch intent
*/
@Test
public void testActivitySwitchIntent() {
String expected = "HELLO";
//set text to input to be passed to SecondActivity
onView(withId(R.id.main_input)).perform(ViewActions.typeText(expected));
ViewInteraction btnSwitch = onView(withId(R.id.main_btn_switch));
//checks if the switch button exists
btnSwitch.check(matches(notNullValue()));
//checks the button test value
btnSwitch.check(matches(withText(R.string.switch_activity)));
//preform click on button to switch activity
btnSwitch.perform(ViewActions.click());
//verifies that the correct intent is called for the SecondActivity
intended(toPackage(SecondActivity.class.getPackage().getName()));
//verifies that extra string passed equals to expected
intended(hasExtra(Constants.IntentParams.INPUT, expected));
}
}
In MainActivity, we've added a button that shows a Toast when clicked. In Order to test that the toast is correctly shown we need :
- in MainActivity , add a button and show a toast on click
public void onClick(View view) {
switch (view.getId()) {
//...
case R.id.main_btn_toast:
Toast.makeText(this, R.string.toast, Toast.LENGTH_SHORT).show();
break;
}
}
ToastMatcher
which identifies a Toast.
/**
* ToastMatcher
* <p>
* identifies a toast
*
*/
public class ToastMatcher extends TypeSafeMatcher<Root> {
@Override
protected boolean matchesSafely(Root root) {
int type = root.getWindowLayoutParams().get().type;
if ((type == WindowManager.LayoutParams.TYPE_TOAST)) {
IBinder windowToken = root.getDecorView().getWindowToken();
IBinder apiToken = root.getDecorView().getApplicationWindowToken();
if (windowToken == apiToken) {
//means that the window isn't contained by any other window
return true;
}
}
return false;
}
@Override
public void describeTo(Description description) {
description.appendText("is toast");
}
}
- In MainActivityTest add ```testToastShown()`` which is a test method to check the toast is correctly displayed.
/**
* test show toast
*/
@Test
public void testShowToast() {
onView(withId(R.id.main_btn_toast)).perform(ViewActions.click());
onView(withText(R.string.toast)).inRoot(new ToastMatcher()).check(matches(isDisplayed()));
}
- Now you can run the the test which should be passed.
For this, we've implemented a new feature on MainActivity which will start an AsyncTask on button
click: we've added a ProgressDialog
to show task run status named mProgress, a TextView
named mTaskStatusText that will display task is running or done when task finishes,
a new Button
for launching the task and an inner class named Task* extends basic Android
AsyncTask
class.
- Task's implementation
/**
* fake async task
*/
private class Task extends AsyncTask<String, Void, String> {
private int seconds = 5;
@Override
protected void onPreExecute() {
mProgress.setMax(seconds);
mProgress.show();
}
@Override
protected String doInBackground(String... params) {
int i = 0;
String taskName = params[0];
do {
i++;
Timber.d("Task %s is running @ %s", taskName, i);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
mProgress.setProgress(i);
} while (i < seconds);
return "Long running asynchronous task";
}
@Override
protected void onPostExecute(String s) {
mProgress.cancel();
}
}
- In
onClick(View)
and for the related button , just start the async:
new Task().execute(RandomUtils.randomString(10));
- In MainActivityTest, we performed a click on the task launch related button and will test on
status text view value to check if it matches
R.string.done
that is set after task finishes.
Below the method that's tests async task in MainActivityTest.java
:
@Test
public void testAsyncTask() {
onView(withId(R.id.main_btn_async)).perform(click());
onView(withId(R.id.main_txt_task_status)).check(matches(withText(R.string.done)));
}
PS: we used RandomUtils.randomString(int)
to give a random name to each new task.
We want to test if an ÈditText
has the expected hint or not.
- For this, we created a
CustomMatchers
class that containswithItemHint(String)
to do checking stuff as shown below:
package com.leadit.androidtesting.matcher;
import android.support.test.espresso.matcher.BoundedMatcher;
import android.text.TextUtils;
import android.view.View;
import android.widget.EditText;
import org.hamcrest.Description;
import org.hamcrest.Matcher;
import static android.support.test.espresso.intent.Checks.checkArgument;
import static android.support.test.espresso.intent.Checks.checkNotNull;
import static org.hamcrest.CoreMatchers.is;
/**
* utilities class that contains all custom matchers
*
*/
public class CustomMatchers {
/**
* check if an {@link EditText} has the expected hint
*
* @param itemTextHint the expected item hint
* @return
*/
public static Matcher<View> withItemHint(String itemTextHint) {
//use preconditions to fail fast when a test when a test is creating an invalid matcher
checkArgument(!TextUtils.isEmpty(itemTextHint));
return withItemHint(is(itemTextHint));
}
/**
* check if an {@link EditText} has the expected hint
*
* @param matcherText text matcher
* @return
*/
private static Matcher<View> withItemHint(final Matcher<String> matcherText) {
// use preconditions to fail fast when a test is creating an invalid matcher.
checkNotNull(matcherText);
return new BoundedMatcher<View, EditText>(EditText.class) {
@Override
public void describeTo(Description description) {
description.appendText("with item hint: " + matcherText);
}
@Override
protected boolean matchesSafely(EditText item) {
return matcherText.matches(item.getHint().toString());
}
};
}
}
- And now, in
MainActivityTest
class we will add the test method.
@Test
public void testInputHint() {
//checks that's main input has hint equals to expected value
onView(withId(R.id.main_input)).check(matches(withItemHint("Enter input to be passed to nex activity")));
}