Zuhaib

ActiveAndroid Testing with Robolectric

SUNDAY FEBRUARY 19 2017 - 8 MIN

Testing has been painful for android developers from day one. The problem lies probably in the way the foundations of the android system has been laid. Over the years, many significant improvements have been made yet Android remains one of the most hard-to-test platforms to date.

Due to the fact that Android is open-sourced and has a HUGE community of skilled and creative developers. They left no stone unturned in their attempts to make things better. From new libraries and strategies for testing to the varying styles of organizing and architecting the code (e.g. MVP and Clean architecture). We have seen attempts made to tackle the problem from every possible angle.

Robolectric was one of the revolutionizing tools that paved way for TDD on Android. It helps you run most of the tests that requires an Android Device or emulator (Services, Database, UI etc) locally on JVM in a matter of seconds.

Another brilliant (and popular these days) library that made lives easier was the ActiveAndroid ORM that removes all the boilerplate code required to even write the simplest databases in Android while using traditional SQLite. However, testing remains as troublesome as it is with the old lad that we are upgrading from.

Luckily, ActiveAndroid comes in with a partial support form temporary in-memory databases which combined with the power of Robolectric, makes testing no different from your everyday unit tests. In this post, we'll learn to integrate both libraries to setup a testing environment for our database.



Adding Dependencies

Before we begin, we need to include Robolectric and ActiveAndroid to our project along with some other necessary libraries. Add the following dependencies to your app/build.gradle file:

// Support libraries compile 'com.android.support:design:25.1.0' compile 'com.android.support:appcompat-v7:25.1.0' // Active Android compile 'com.michaelpardo:activeandroid:3.1.0-SNAPSHOT' // JUnit testCompile 'junit:junit:4.12' // Robolectric testCompile 'org.robolectric:robolectric:3.2.2'

Writing A Simple App

We'll start by writing a very simple application that would allow us to save and retrieve information to and from our ActiveAndroid database.

MainActivity.java

public class MainActivity extends AppCompatActivity implements View.OnClickListener { private LinearLayout mContainer; private EditText mNameEditText; private EditText mEmailEditText; private EditText mContactEditText; private Button mSaveButton; private Button mRetrieveButton; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); mContainer = (LinearLayout) findViewById(R.id.LLContainer); mNameEditText = (EditText) findViewById(R.id.ETName); mEmailEditText = (EditText) findViewById(R.id.ETEmail); mContactEditText = (EditText) findViewById(R.id.ETContact); mSaveButton = (Button) findViewById(R.id.BTSave); mRetrieveButton = (Button) findViewById(R.id.BTRetrieve); mSaveButton.setOnClickListener(this); mRetrieveButton.setOnClickListener(this); } @Override public void onClick(View v) { int id = v.getId(); String name = mNameEditText.getText().toString(); String email = mEmailEditText.getText().toString(); String contact = mContactEditText.getText().toString(); if (id == R.id.BTSave) { // Validate input if (isEmpty(name) || isEmpty(email) || isEmpty(contact)){ showSnackBar("Please Fill All Fields Before Saving"); return; } // Save to database boolean isSuccessful = DatabaseUtils.getInstance() .saveToDatabase(name, email, contact); // Notify user regarding operation success String message = isSuccessful ? "Saved Successfully" : "Error While Saving Item"; showSnackBar(message); } else if (id == R.id.BTRetrieve) { // Validate input if (isEmpty(name) && isEmpty(email) && isEmpty(contact)){ showSnackBar("Please Fill At Least One Field " + "Before Retrieving From Database"); return; } // Retrieve from database Item item = DatabaseUtils.getInstance() .retrieveFromDatabase(name, email, contact); // Notify user about error in case of failure if (item == null) { showSnackBar("Error While Retrieving Item" + " From Database"); return; } // Display item data in relevant fields mNameEditText.setText(item.getName()); mEmailEditText.setText(item.getEmail()); mContactEditText.setText(item.getContact()); } else { showSnackBar("Invalid Action!"); } } private void showSnackBar(String message) { Snackbar.make(mContainer, message, Snackbar.LENGTH_SHORT) .show(); } }

activity_main.xml

<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:id="@+id/LLContainer" android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical" android:paddingBottom="@dimen/activity_vertical_margin" android:paddingLeft="@dimen/activity_horizontal_margin" android:paddingRight="@dimen/activity_horizontal_margin" android:paddingTop="@dimen/activity_vertical_margin"> <LinearLayout android:layout_width="match_parent" android:layout_height="0dp" android:layout_weight="1" android:gravity="center" android:orientation="vertical"> <android.support.design.widget.TextInputLayout android:layout_width="match_parent" android:layout_height="wrap_content"> <EditText android:id="@+id/ETName" android:layout_width="match_parent" android:layout_height="wrap_content" android:ems="10" android:hint="Name" android:inputType="textPersonName"/> </android.support.design.widget.TextInputLayout> <android.support.design.widget.TextInputLayout android:layout_width="match_parent" android:layout_height="wrap_content"> <EditText android:id="@+id/ETEmail" android:layout_width="match_parent" android:layout_height="wrap_content" android:ems="10" android:hint="Email" android:inputType="textEmailAddress"/> </android.support.design.widget.TextInputLayout> <android.support.design.widget.TextInputLayout android:layout_width="match_parent" android:layout_height="wrap_content"> <EditText android:id="@+id/ETContact" android:layout_width="match_parent" android:layout_height="wrap_content" android:ems="10" android:hint="Contact Number" android:inputType="phone"/> </android.support.design.widget.TextInputLayout> </LinearLayout> <LinearLayout android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="horizontal"> <Button android:id="@+id/BTSave" android:layout_width="0dp" android:layout_height="wrap_content" android:layout_margin="4dp" android:layout_weight=".5" android:background="@color/colorPrimary" android:text="Save Contact" android:textColor="@android:color/white"/> <Button android:id="@+id/BTRetrieve" android:layout_width="0dp" android:layout_height="wrap_content" android:layout_margin="4dp" android:layout_weight=".5" android:background="@color/colorPrimary" android:text="Retrieve Contact" android:textColor="@android:color/white"/> </LinearLayout> </LinearLayout>

Item.java

/\*\* - Created by Zuhaib Ahmad on 2/7/2017. - <p> - Java POJO representing database model \*/ public class Item extends Model { public static final String KEY_CONTACT = "contact"; public static final String KEY_EMAIL = "email"; public static final String KEY_NAME = "name"; @Column(name = KEY_NAME) private String name; @Column(name = KEY_EMAIL) private String email; @Column(name = KEY_CONTACT) private String contact; public Item() { super(); } public Item(String name, String email, String contact) { super(); this.name = name; this.email = email; this.contact = contact; } public String getName() { return name; } public void setName(String name) { this.name = name; } public String getEmail() { return email; } public void setEmail(String email) { this.email = email; } public String getContact() { return contact; } public void setContact(String contact) { this.contact = contact; } }

DatabaseUtils.java

/\*\* - Created by Zuhaib Ahmad on 1/28/2017. - <p> - Utility class to handle database operations \*/ public class DatabaseUtils { private static DatabaseUtils instance; private DatabaseUtils() { } /** * Gets singleton instance. * * @return the instance */ public static DatabaseUtils getInstance() { if (instance == null) { instance = new DatabaseUtils(); } return instance; } /** * Retrieves {@link Item} object from database * * @param name The name of contact * @param email The email of contact * @param contact The number of contact * @return The retrieved item */ public Item retrieveFromDatabase(String name, String email, String contact) { // Use first non-null parameter for conditional check String whereClause = ""; String key = ""; if (!isEmpty(name)) { whereClause = name; key = Item.KEY_NAME; } else if (!isEmpty(email)) { whereClause = email; key = Item.KEY_EMAIL; } else if (!isEmpty(contact)) { whereClause = contact; key = Item.KEY_CONTACT; } // Query the database and return the fetched item return new Select() .from(Item.class) .where(key + " =?", whereClause) .executeSingle(); } /** * Saves {@link Item} to database * * @param name The name of contact * @param email The email of contact * @param contact The number of contact * @return The operation success status */ public boolean saveToDatabase(String name, String email, String contact) { // Create database item object out of provided parameters Item item = new Item(name, email, contact); // Save to database and get the id of saved item Long id = item.save(); // Mark operation as successful if returned id is non-zero return id > 0; } }

Application.java

/\*\* - Created by Zuhaib on 1/22/2016. - <p> - Application class for global management and data access \*/ public class Application extends android.app.Application { @Override public void onCreate() { super.onCreate(); // Create Active Android configurations Configuration.Builder configuration = new Configuration.Builder(this); configuration.addModelClasses(Item.class); // Initialize ActiveAndroid DB ActiveAndroid.initialize(configuration.create()); } }

Testing with Robolectric

Before writing tests for our database, we will need to configure our temporary database. To achieve this, we will be writing an alternate version of our Application.java class which will replace the original in our tests.

TestApplication.java

/\*\* - Created by Zuhaib Ahmad on 1/20/2017. - <p> - Application subclass to be used in Tests \*/ public class TestApplication extends Application { @Override public void onCreate() { super.onCreate(); // Create configurations for a temporary mock database Configuration.Builder configuration = new Configuration.Builder(this).setDatabaseName(null); configuration.addModelClasses(Item.class); // Initialize ActiveAndroid DB ActiveAndroid.initialize(configuration.create()); } @Override public void onTerminate() { // Dispose temporary database on termination ActiveAndroid.dispose(); super.onTerminate(); } }

Notice the setDatabaseName(null) while building configurations. It allows us to create an anonymous temporary database which we dispose on termination of application.

DatabaseUtilsTest.java

/\*\* - Created by Zuhaib Ahmad on 2/7/2017. - <p> - Tests fro database utils \*/ @RunWith(RobolectricTestRunner.class) @Config( constants = BuildConfig.class, // Run with TestApplication instead of actual application = TestApplication.class, // Test against Lollipop sdk = 21 ) public class DatabaseUtilsTest { private DatabaseUtils mInstance; @Before public void setUp() throws Exception { ShadowLog.stream = System.out; mInstance = DatabaseUtils.getInstance(); } @Test public void retrieveFromDatabase() throws Exception { String name = "Test Retrieve Item"; String email = "Retrieve@abc.com"; String contact = "12345"; Item testItem = new Item(name, email, contact); testItem.save(); // Fetch with name Item item1 = mInstance .retrieveFromDatabase(name, null, null); assertNotNull(item1); assertEquals(testItem, item1); // Fetch with email Item item2 = mInstance .retrieveFromDatabase(null, email, null); assertNotNull(item2); assertEquals(testItem, item2); // Fetch with contact Item item3 = mInstance .retrieveFromDatabase(null, null, contact); assertNotNull(item3); assertEquals(testItem, item3); testItem.delete(); } @Test public void saveToDatabase() throws Exception { String name = "Test Save Item"; String email = "Save@abc.com"; String contact = "67890"; // Create test object from test parameters Item testItem = new Item(name, email, contact); // Save test parameters boolean isSuccessful = mInstance .saveToDatabase(name, email, contact); // Check if successfully saved assertTrue(isSuccessful); // Check if saved item is equal to test object Item savedItem = mInstance .retrieveFromDatabase(name, null, null); assertEquals(testItem.getName(), savedItem.getName()); assertEquals(testItem.getEmail(), savedItem.getEmail()); assertEquals(testItem.getContact(), savedItem.getContact()); } }

Run the tests and hopefully you'll see green lights. That's it, you can now locally test your databases just as you would unit test any other part of your application. You can find complete source code of the application here

Zuhaib Ahmad © 2024