Site icon AndroidVille

Unit Testing in Android with a sample application

unit testing in android

In the last article, I listed out the benefits of Unit Testing your applications. In this tutorial, we’ll take a look at how to begin Unit Testing your Android Applications.

If you haven’t checked out the previous article on why you should unit test your android app, then you must take a quick look at it before moving ahead with this one.

 

Install the dependencies

Place these dependencies in your app level build.gradle file:

testImplementation 'junit:junit:4.12'
testImplementation 'org.mockito:mockito-core:1.10.19'

 

Create a sample app:

**IMPORTANT**

Unit Tests are generally written before writing the actual application. But for the sake of explanation in this article, I am creating a sample app before writing Unit Tests.

Unit Testing is done to ensure that the developer would be unable to write low quality/erroneous code. It makes sense to write Unit Tests before writing the actual app as then you wouldn’t have a bias towards the success of your tests, you will write tests beforehand and the actual code will have to adhere to the design guidelines laid out by the test.

Now, lets create our sample app.

We’ll be creating a simple app, whose sole purpose would be to get user data from input fields and save it using in a shared preference file.

First things first, here is the code for activity_main.xml layout file:

<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
            xmlns:tools="http://schemas.android.com/tools"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:orientation="vertical"
            android:padding="@dimen/activity_horizontal_margin"
            tools:context=".MainActivity">
    <LinearLayout android:layout_width="match_parent"
                  android:layout_height="wrap_content"
                  android:orientation="vertical"
                  android:padding="@dimen/activity_horizontal_margin"
                  tools:context=".MainActivity">

        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_gravity="center_horizontal"
            android:layout_marginBottom="@dimen/header_margin"
            android:text="@string/settings_title"
            android:textAppearance="?android:attr/textAppearanceLarge"/>

        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_gravity="start"
            android:text="@string/name_label"
            android:layout_marginTop="@dimen/activity_vertical_margin"
            android:textAppearance="?android:attr/textAppearanceMedium"/>

        <EditText
            android:id="@+id/userNameInput"
            android:hint="@string/name_hint"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_gravity="center_horizontal"/>

        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_gravity="start"
            android:text="@string/dob_label"
            android:layout_marginTop="@dimen/activity_vertical_margin"
            android:textAppearance="?android:attr/textAppearanceMedium"/>

        <DatePicker
            android:id="@+id/dateOfBirthInput"
            android:inputType="text|date"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:datePickerMode="spinner"
            android:calendarViewShown="false"
            android:layout_gravity="center_horizontal"/>

        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_gravity="start"
            android:text="@string/email_label"
            android:layout_marginTop="@dimen/activity_vertical_margin_small"
            android:textAppearance="?android:attr/textAppearanceMedium"/>

        <EditText
            android:id="@+id/emailInput"
            android:hint="@string/email_hint"
            android:inputType="textEmailAddress"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_gravity="center_horizontal"/>

        <LinearLayout
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:orientation="horizontal"
            android:layout_gravity="center_horizontal"
            android:layout_marginTop="@dimen/activity_vertical_margin">

            <Button
                android:id="@+id/saveButton"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:textSize="@dimen/buttonTextSize"
                android:text="@string/save"
                android:onClick="onSaveClick"
                android:layout_marginRight="@dimen/activity_horizontal_margin"
                android:layout_marginEnd="@dimen/activity_horizontal_margin"
                android:layout_gravity="center_horizontal"/>

            <Button
                android:id="@+id/revertButton"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:textSize="@dimen/buttonTextSize"
                android:onClick="onRevertClick"
                android:text="@string/revert"
                android:layout_gravity="center_horizontal"/>

        </LinearLayout>

    </LinearLayout>
</ScrollView>

 

Now, here is the code for MainActivity.java:

import android.app.Activity;
import android.content.SharedPreferences;
import android.os.Bundle;
import android.preference.PreferenceManager;
import android.util.Log;
import android.view.View;
import android.widget.DatePicker;
import android.widget.EditText;
import android.widget.Toast;

import java.util.Calendar;

/**
 * An {@link Activity} that represents an input form page where the user can provide his name, date
 * of birth and email address. The personal information can be saved to {@link SharedPreferences}
 * by clicking a button.
 */
public class MainActivity extends Activity {

    // Logger for this class.
    private static final String TAG = "MainActivity";

    // The helper that manages writing to SharedPreferences.
    private SharedPreferencesHelper mSharedPreferencesHelper;

    // The input field where the user enters his name.
    private EditText mNameText;

    // The date picker where the user enters his date of birth.
    private DatePicker mDobPicker;

    // The input field where the user enters his email.
    private EditText mEmailText;

    // The validator for the email input field.
    private EmailValidator mEmailValidator;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        // Shortcuts to input fields.
        mNameText = (EditText) findViewById(R.id.userNameInput);
        mDobPicker = (DatePicker) findViewById(R.id.dateOfBirthInput);
        mEmailText = (EditText) findViewById(R.id.emailInput);

        // Setup field validators.
        mEmailValidator = new EmailValidator();
        mEmailText.addTextChangedListener(mEmailValidator);

        // Instantiate a SharedPreferencesHelper.
        SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this);
        mSharedPreferencesHelper = new SharedPreferencesHelper(sharedPreferences);

        // Fill input fields from data retrieved from the SharedPreferences.
        populateUi();
    }

    /**
     * Initialize all fields from the personal info saved in the SharedPreferences.
     */
    private void populateUi() {
        SharedPreferenceEntry sharedPreferenceEntry;
        sharedPreferenceEntry = mSharedPreferencesHelper.getPersonalInfo();

        mNameText.setText(sharedPreferenceEntry.getName());
        Calendar dateOfBirth = sharedPreferenceEntry.getDateOfBirth();
        mDobPicker.init(dateOfBirth.get(Calendar.YEAR), dateOfBirth.get(Calendar.MONTH),
                dateOfBirth.get(Calendar.DAY_OF_MONTH), null);
        mEmailText.setText(sharedPreferenceEntry.getEmail());
    }


    /**
     * Called when the "Save" button is clicked.
     */
    public void onSaveClick(View view) {
        // Don't save if the fields do not validate.
        if (!mEmailValidator.isValid()) {
            mEmailText.setError("Invalid email");
            Log.w(TAG, "Not saving personal information: Invalid email");
            return;
        }

        // Get the text from the input fields.
        String name = mNameText.getText().toString();
        Calendar dateOfBirth = Calendar.getInstance();
        dateOfBirth.set(mDobPicker.getYear(), mDobPicker.getMonth(), mDobPicker.getDayOfMonth());
        String email = mEmailText.getText().toString();

        // Create a Setting model class to persist.
        SharedPreferenceEntry sharedPreferenceEntry =
                new SharedPreferenceEntry(name, dateOfBirth, email);

        // Persist the personal information.
        boolean isSuccess = mSharedPreferencesHelper.savePersonalInfo(sharedPreferenceEntry);
        if (isSuccess) {
            Toast.makeText(this, "Personal information saved", Toast.LENGTH_LONG).show();
            Log.i(TAG, "Personal information saved");
        } else {
            Log.e(TAG, "Failed to write personal information to SharedPreferences");
        }
    }

    /**
     * Called when the "Revert" button is clicked.
     */
    public void onRevertClick(View view) {
        populateUi();
        Toast.makeText(this, "Personal information reverted", Toast.LENGTH_LONG).show();
        Log.i(TAG, "Personal information reverted");
    }
}

 

SharedPreferencesHelper.java:

import android.content.SharedPreferences;

import java.util.Calendar;

/**
 *  Helper class to manage access to {@link SharedPreferences}.
 */
public class SharedPreferencesHelper {

    // Keys for saving values in SharedPreferences.
    static final String KEY_NAME = "key_name";
    static final String KEY_DOB = "key_dob_millis";
    static final String KEY_EMAIL = "key_email";

    // The injected SharedPreferences implementation to use for persistence.
    private final SharedPreferences mSharedPreferences;

    /**
     * Constructor with dependency injection.
     *
     * @param sharedPreferences The {@link SharedPreferences} that will be used in this DAO.
     */
    public SharedPreferencesHelper(SharedPreferences sharedPreferences) {
        mSharedPreferences = sharedPreferences;
    }

    /**
     * Saves the given {@link SharedPreferenceEntry} that contains the user's settings to
     * {@link SharedPreferences}.
     *
     * @param sharedPreferenceEntry contains data to save to {@link SharedPreferences}.
     * @return {@code true} if writing to {@link SharedPreferences} succeeded. {@code false}
     *         otherwise.
     */
    public boolean savePersonalInfo(SharedPreferenceEntry sharedPreferenceEntry){
        // Start a SharedPreferences transaction.
        SharedPreferences.Editor editor = mSharedPreferences.edit();
        editor.putString(KEY_NAME, sharedPreferenceEntry.getName());
        editor.putLong(KEY_DOB, sharedPreferenceEntry.getDateOfBirth().getTimeInMillis());
        editor.putString(KEY_EMAIL, sharedPreferenceEntry.getEmail());

        // Commit changes to SharedPreferences.
        return editor.commit();
    }

    /**
     * Retrieves the {@link SharedPreferenceEntry} containing the user's personal information from
     * {@link SharedPreferences}.
     *
     * @return the Retrieved {@link SharedPreferenceEntry}.
     */
    public SharedPreferenceEntry getPersonalInfo() {
        // Get data from the SharedPreferences.
        String name = mSharedPreferences.getString(KEY_NAME, "");
        Long dobMillis =
                mSharedPreferences.getLong(KEY_DOB, Calendar.getInstance().getTimeInMillis());
        Calendar dateOfBirth = Calendar.getInstance();
        dateOfBirth.setTimeInMillis(dobMillis);
        String email = mSharedPreferences.getString(KEY_EMAIL, "");

        // Create and fill a SharedPreferenceEntry model object.
        return new SharedPreferenceEntry(name, dateOfBirth, email);
    }
}

 

EmailValidator.java :

import android.text.Editable;
import android.text.TextWatcher;

import java.util.regex.Pattern;

/**
 * An Email format validator for {@link android.widget.EditText}.
 */
public class EmailValidator implements TextWatcher {

    /**
     * Email validation pattern.
     */
    public static final Pattern EMAIL_PATTERN = Pattern.compile(
            "[a-zA-Z0-9\\+\\.\\_\\%\\-\\+]{1,256}" +
                    "\\@" +
                    "[a-zA-Z0-9][a-zA-Z0-9\\-]{0,64}" +
                    "(" +
                    "\\." +
                    "[a-zA-Z0-9][a-zA-Z0-9\\-]{0,25}" +
                    ")+"
    );

    private boolean mIsValid = false;

    public boolean isValid() {
        return mIsValid;
    }

    /**
     * Validates if the given input is a valid email address.
     *
     * @param email        The email to validate.
     * @return {@code true} if the input is a valid email. {@code false} otherwise.
     */
    public static boolean isValidEmail(CharSequence email) {
        return email != null && EMAIL_PATTERN.matcher(email).matches();
    }

    @Override
    final public void afterTextChanged(Editable editableText) {
        mIsValid = isValidEmail(editableText);
    }

    @Override
    final public void beforeTextChanged(CharSequence s, int start, int count, int after) {/*No-op*/}

    @Override
    final public void onTextChanged(CharSequence s, int start, int before, int count) {/*No-op*/}
}

 

SharedPreferenceEntry.java:

import java.util.Calendar;

/**
 * Model class containing personal information that will be saved to SharedPreferences.
 */
public class SharedPreferenceEntry {

    // Name of the user.
    private final String mName;

    // Date of Birth of the user.
    private final Calendar mDateOfBirth;

    // Email address of the user.
    private final String mEmail;

    public SharedPreferenceEntry(String name, Calendar dateOfBirth, String email) {
        mName = name;
        mDateOfBirth = dateOfBirth;
        mEmail = email;
    }

    public String getName() {
        return mName;
    }

    public Calendar getDateOfBirth() {
        return mDateOfBirth;
    }

    public String getEmail() {
        return mEmail;
    }
}

 

Now try and run the app. I should display a screen like this:

 

Upon entering the name, DOB and email address and clicking on save, the details will be saved to a shared preference file.

Writing Unit Tests:

Now, let’s write some Unit Tests.

We’ll be Unit Testing the following classes:

Navigate to: app/java/com(test) and expand all the folders under com(test).

Create a new java file and name it EmailValidatorTest.java

Here we will be testing our EmailValidator class. We have to come up with all the input cases we can think of. What all can the user enter in the email input field:

  1. Correct Input: test@gmail.com
  2. Email with subdomain: test@gmail.co.uk
  3. Without .com: test@gmail
  4. With extra characters: test@gmail..com
  5. With no username: @gmail.com
  6. Empty Input:
  7. Null value: this can occur if we initialize the string from this field to be null. It doesn’t hurt to have a test case for null check in place.

While the 1st and the 2nd test cases must pass, rest of the inputs are invalid and hence the tests must fail. Let’s write the tests for all of them:

Test Cases

  1. Correct Input
@Test
public void emailValidator_CorrectEmailSimple_ReturnsTrue() {
    assertTrue(EmailValidator.isValidEmail("name@email.com"));
}

 

  1. Email with subdomain
@Test
public void emailValidator_CorrectEmailSubDomain_ReturnsTrue() {
    assertTrue(EmailValidator.isValidEmail("name@email.co.uk"));
}

 

  1. Without .com:
@Test
public void emailValidator_InvalidEmailNoTld_ReturnsFalse() {
    assertFalse(EmailValidator.isValidEmail("name@email"));
}

 

  1. With extra characters:
@Test
public void emailValidator_InvalidEmailDoubleDot_ReturnsFalse() {
    assertFalse(EmailValidator.isValidEmail("name@email..com"));
}

 

  1. With no username:
@Test
public void emailValidator_InvalidEmailNoUsername_ReturnsFalse() {
    assertFalse(EmailValidator.isValidEmail("@email.com"));
}

 

  1. Empty Input:
@Test
public void emailValidator_EmptyString_ReturnsFalse() {
    assertFalse(EmailValidator.isValidEmail(""));
}

 

  1. Null value check:
@Test
public void emailValidator_NullEmail_ReturnsFalse() {
    assertFalse(EmailValidator.isValidEmail(null));
}

 

While most of the code is self-explanatory, here are some things you might not know:

Here is the complete EmailValidatorTest.java class:

import org.junit.Test;

import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;


/**
 * Unit tests for the EmailValidator logic.
 */
public class EmailValidatorTest {


    @Test
    public void emailValidator_CorrectEmailSimple_ReturnsTrue() {
        assertTrue(EmailValidator.isValidEmail("name@email.com"));
    }

    @Test
    public void emailValidator_CorrectEmailSubDomain_ReturnsTrue() {
        assertTrue(EmailValidator.isValidEmail("name@email.co.uk"));
    }

    @Test
    public void emailValidator_InvalidEmailNoTld_ReturnsFalse() {
        assertFalse(EmailValidator.isValidEmail("name@email"));
    }

    @Test
    public void emailValidator_InvalidEmailDoubleDot_ReturnsFalse() {
        assertFalse(EmailValidator.isValidEmail("name@email..com"));
    }

    @Test
    public void emailValidator_InvalidEmailNoUsername_ReturnsFalse() {
        assertFalse(EmailValidator.isValidEmail("@email.com"));
    }

    @Test
    public void emailValidator_EmptyString_ReturnsFalse() {
        assertFalse(EmailValidator.isValidEmail(""));
    }

    @Test
    public void emailValidator_NullEmail_ReturnsFalse() {
        assertFalse(EmailValidator.isValidEmail(null));
    }
}

 

And Voila!! You have written your very first Unit Test. It is as simple as that.

Most of the apps you develop will have much more complicated functionality than this but it is the perfect place to start learning about Annotations, methods provided by the Junit framework for unit testing.

But so far, we have used only the Junit framework, now let’s dive into what Mockito does.

 Mockito

Mockito is a JAVA library that is used for Unit Testing the Java applications. It is used to mock the interfaces so that dummy objects can be created and used to provide the dependencies for the class being tested.

Let’s look at how it’s done.

In our test folder (where you created EmailValidatorTest.java), create another file named SharedPreferencesHelperTest.java

We’ll be testing the SharedPreferencesHelper class now. Here is the code:

import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.CoreMatchers.*;
import static org.mockito.Mockito.*;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.runners.MockitoJUnitRunner;

import android.content.SharedPreferences;

import java.util.Calendar;


/**
 * Unit tests for the {@link SharedPreferencesHelper} that mocks {@link SharedPreferences}.
 */
@RunWith(MockitoJUnitRunner.class)
public class SharedPreferencesHelperTest {

    private static final String TEST_NAME = "Test name";

    private static final String TEST_EMAIL = "test@email.com";

    private static final Calendar TEST_DATE_OF_BIRTH = Calendar.getInstance();

    static {
        TEST_DATE_OF_BIRTH.set(1980, 1, 1);
    }

    private SharedPreferenceEntry mSharedPreferenceEntry;

    private SharedPreferencesHelper mMockSharedPreferencesHelper;

    private SharedPreferencesHelper mMockBrokenSharedPreferencesHelper;

    @Mock
    SharedPreferences mMockSharedPreferences;

    @Mock
    SharedPreferences mMockBrokenSharedPreferences;

    @Mock
    SharedPreferences.Editor mMockEditor;

    @Mock
    SharedPreferences.Editor mMockBrokenEditor;

    @Before
    public void initMocks() {
        // Create SharedPreferenceEntry to persist.
        mSharedPreferenceEntry = new SharedPreferenceEntry(TEST_NAME, TEST_DATE_OF_BIRTH,
                TEST_EMAIL);

        // Create a mocked SharedPreferences.
        mMockSharedPreferencesHelper = createMockSharedPreference();

        // Create a mocked SharedPreferences that fails at saving data.
        mMockBrokenSharedPreferencesHelper = createBrokenMockSharedPreference();
    }

    @Test
    public void sharedPreferencesHelper_SaveAndReadPersonalInformation() {
        // Save the personal information to SharedPreferences
        boolean success = mMockSharedPreferencesHelper.savePersonalInfo(mSharedPreferenceEntry);

        assertThat("Checking that SharedPreferenceEntry.save... returns true",
                success, is(true));

        // Read personal information from SharedPreferences
        SharedPreferenceEntry savedSharedPreferenceEntry =
                mMockSharedPreferencesHelper.getPersonalInfo();

        // Make sure both written and retrieved personal information are equal.
        assertThat("Checking that SharedPreferenceEntry.name has been persisted and read correctly",
                mSharedPreferenceEntry.getName(),
                is(equalTo(savedSharedPreferenceEntry.getName())));
        assertThat("Checking that SharedPreferenceEntry.dateOfBirth has been persisted and read "
                + "correctly",
                mSharedPreferenceEntry.getDateOfBirth(),
                is(equalTo(savedSharedPreferenceEntry.getDateOfBirth())));
        assertThat("Checking that SharedPreferenceEntry.email has been persisted and read "
                + "correctly",
                mSharedPreferenceEntry.getEmail(),
                is(equalTo(savedSharedPreferenceEntry.getEmail())));
    }

    @Test
    public void sharedPreferencesHelper_SavePersonalInformationFailed_ReturnsFalse() {
        // Read personal information from a broken SharedPreferencesHelper
        boolean success =
                mMockBrokenSharedPreferencesHelper.savePersonalInfo(mSharedPreferenceEntry);
        assertThat("Makes sure writing to a broken SharedPreferencesHelper returns false", success,
                is(false));
    }

    /**
     * Creates a mocked SharedPreferences.
     */
    private SharedPreferencesHelper createMockSharedPreference() {
        // Mocking reading the SharedPreferences as if mMockSharedPreferences was previously written
        // correctly.
        when(mMockSharedPreferences.getString(eq(SharedPreferencesHelper.KEY_NAME), anyString()))
                .thenReturn(mSharedPreferenceEntry.getName());
        when(mMockSharedPreferences.getString(eq(SharedPreferencesHelper.KEY_EMAIL), anyString()))
                .thenReturn(mSharedPreferenceEntry.getEmail());
        when(mMockSharedPreferences.getLong(eq(SharedPreferencesHelper.KEY_DOB), anyLong()))
                .thenReturn(mSharedPreferenceEntry.getDateOfBirth().getTimeInMillis());

        // Mocking a successful commit.
        when(mMockEditor.commit()).thenReturn(true);

        // Return the MockEditor when requesting it.
        when(mMockSharedPreferences.edit()).thenReturn(mMockEditor);
        return new SharedPreferencesHelper(mMockSharedPreferences);
    }

    /**
     * Creates a mocked SharedPreferences that fails when writing.
     */
    private SharedPreferencesHelper createBrokenMockSharedPreference() {
        // Mocking a commit that fails.
        when(mMockBrokenEditor.commit()).thenReturn(false);

        // Return the broken MockEditor when requesting it.
        when(mMockBrokenSharedPreferences.edit()).thenReturn(mMockBrokenEditor);
        return new SharedPreferencesHelper(mMockBrokenSharedPreferences);
    }
}

 

Take a look at all the mock annotations. The SharedPreferencesHelper class takes in a SharedPreference in its constructor. It needs that argument to function properly, so we create a mock/dummy instance of SharedPreferences using the @Mock annotation.

 

Notice that we are creating two instances of SharedPreferences, one is a normal mock and other is a broken mock. These are basically just two test cases. In the first one, the preferences work as expected and data is written to the file successfully, but, in the second mock, we are testing for the failure of writing the data.

Unsuccessful write of data on sharedPreference can occur due to various reasons such as providing the wrong key, wrong context etc.

Annotations

Here you encounter three new annotations:

Any un-annotated method works just as a normal method

JUnit methods:

You encounter 3 new methods provided by JUnit Framework:

We have written tests to ensure that any broken shared preferences should not be able to write/read from the brokenSharedPreferencesFile. Another test case to ensure that a proper(unbroken) sharedPreferencesHelper can write to the preferences successfully.

 

And this is how you write Unit Tests for your android app. We have tested 2 standalone classes, SharedPreferencesHelper and EmailValidator for correctness in all the test cases.

Here is a final roadmap to create a unit test:

  1. Think of all the possible test cases.
  2. Create a method for each test case and annotate it with @Test
  3. Create a @Before method to initialize the Mockito library.
  4. Write your test cases using methods such as when, is, assertThat/False/True etc.

 

Like what you read ? Don’t forget to share this post on FacebookWhatsapp and LinkedIn.

You can follow me on LinkedInQuoraTwitter and Instagram where I answer questions related to Mobile Development, especially Android and Flutter.

 

 

Exit mobile version