Thursday, May 28, 2015

Android TDD Series: The Setup

Previously I spoke about the challenges in following Test-Driven Development in Android.  Armed with the fore-knowledge of what we're getting ourselves into we're now ready to dive into the first serious attempt at test-driving some functionality in.

The App


To make this series a little more light-hearted I'm going to develop an app I'll call the "Business Unit Estimator".  This app will let the user take a picture of someone or someplace and assign how many "Units of Business" can be accomplished by that person or place.  For example, take the gentleman in the meme image below:

Via Quickmeme

This fellow is wearing a tie (+5 units of business right there), as well as a business jacket (+3 units of business), his hair is immaculate (+1 units of business), and he's on a cell phone (+2 units of business, as obviously more work gets done when you're on a phone).  That's a grand total of 11 units of business.  This guy means business!  Silliness aside this will provide an app we can work on to demonstrate TDD in Android.



Pre-Reqs

I'm going to make a number of assumptions before we get going.  First, I'm going to assume you have the following installed on your system:

  • Android Studio 1.1+
  • The latest Android SDK (the previous link will get this for you as well)
  • Java 7 (note: we don't want 8 here, as Android is not compatible with 8)
  • Gradle (preferably installed via GVM)
I'm also going to assume that you know how to create an Android project in Android Studio via the usual File -> New -> New Project with a "Blank Activity".  If you'd like a shortcut on creating the project feel free to check out the demo app on my Github page.

Setup

This entry in the series will focus on your environment setup to allow Test-Driving to occur via unit tests and Robolectric.

Robolectric is an Android unit testing framework that allows you to write tests which will run on the JVM, rather than on the emulator.  This has several benefits.  First, it will greatly shorten your feedback loop, as tests which run on the JVM run extremely fast.  Compare this with the out-of-the-box tools provided by Google which by and large require you to deploy code to emulator, wait for it to load, and finally wait for the test to run and you'll see a huge difference.  A secondary benefit here is that running on the JVM allows you to use a mocking framework such as Mockito to control the behavior of your dependencies.

Throughout this series I'll be using Robolectric 3.  Let's add the dependencies we need in our app/build.gradle file:


dependencies {
    compile fileTree(dir: 'libs', include: ['*.jar'])
    compile 'com.android.support:appcompat-v7:22.1.1'

    testCompile 'org.hamcrest:hamcrest-integration:1.3'
    testCompile 'org.hamcrest:hamcrest-core:1.3'
    testCompile 'org.hamcrest:hamcrest-library:1.3'
    testCompile 'junit:junit:4.12'
    testCompile 'org.mockito:mockito-core:1.+'
    testCompile 'org.robolectric:robolectric:3.0-SNAPSHOT'
    testCompile 'org.robolectric:shadows-support-v4:3.0-SNAPSHOT'
}

This block should pull in everything we need to write unit tests.  While we won't be using Mockito just yet, we'll need it soon enough.  The shadows-support dependency will provide additional support for accessing parts of the Android SDK in the test environment.  We're also going to use the 3.0-SNAPSHOT versions here as Robolectric 3 is still in RC at the moment; fear not, the API solid despite that.

Next we need to add the app/src/test/java folder in our project structure, as it is not added for us when the project is generated:

Adding the app/src/test/java folder.

Our First Test

Once this is completed we can add our first test:


package com.jameskbride;

import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.robolectric.Robolectric;
import org.robolectric.RobolectricGradleTestRunner;
import org.robolectric.Shadows;
import org.robolectric.annotation.Config;
import org.robolectric.shadows.ShadowActivity;

import static org.junit.Assert.assertEquals;

@RunWith(RobolectricGradleTestRunner.class)
@Config(constants = BuildConfig.class)
public class MainActivityTest {

    private MainActivity activity;

    @Before
    public void setUp() {
        activity = Robolectric.setupActivity(MainActivity.class);
    }

    @Test
    public void whenTheActivityIsCreatedThenTheContentViewShouldBeSet() {
        ShadowActivity shadowActivity = Shadows.shadowOf(activity);
        assertEquals(R.id.main, shadowActivity.getContentView().getId());
    }
}

There is a lot going on here which we're going to cover in more depth later on, but for now you need to know that the @RunWith and @Config annotations are required to run the Robolectric test. You should also be aware that there are multiple versions of BuildConfig.class, and you'll need to use the one which is generated for your project and not the android.support.v4 or android.support.v7.appcompat versions. Using these versions will cause errors. I'm going to skip over the setup for the moment (we'll cover this in the next entry in the series) and jump straight to the test. When we generated the project a MainActivity class was generated for us under app/src/main/java.

package com.jameskbride;

import android.os.Bundle;
import android.support.v7.app.AppCompatActivity;
import android.view.Menu;
import android.view.MenuItem;


public class MainActivity extends AppCompatActivity {

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

    @Override
    public boolean onCreateOptionsMenu(Menu menu) {
        // Inflate the menu; this adds items to the action bar if it is present.
        getMenuInflater().inflate(R.menu.main_menu, menu);
        return true;
    }

    @Override
    public boolean onOptionsItemSelected(MenuItem item) {
        // Handle action bar item clicks here. The action bar will
        // automatically handle clicks on the Home/Up button, so long
        // as you specify a parent activity in AndroidManifest.xml.
        int id = item.getItemId();

        //noinspection SimplifiableIfStatement
        if (id == R.id.action_settings) {
            return true;
        }

        return super.onOptionsItemSelected(item);
    }
}
Unfortunately (from a TDD perspective) it also added some logic to set the content view. Our first test is going to add coverage for this functionality.
    @Test
    public void whenTheActivityIsCreatedThenTheContentViewShouldBeSet() {
        ShadowActivity shadowActivity = Shadows.shadowOf(activity);
        assertEquals(R.id.main, shadowActivity.getContentView().getId());
    }
As you can see we are using a ShadowActivity, and asserting that the content view ID has been set.  Let's execute the test. From the root of our project we'll run:
./gradlew testDebug

This causes a compilation error, as the id for main doesn't exist yet.
/home/jim/projects/BusinessUnitEstimator/app/src/test/java/com/jameskbride/MainActivityTest.java:28: error: cannot find symbol
        assertEquals(R.id.main, shadowActivity.getContentView().getId());
                         ^
  symbol:   variable main
  location: class id
1 error
:app:compileDebugUnitTestJava FAILED
Let's make this test pass by adding the id field which will allow us to verify that it is set as the content view.
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent"
    android:layout_height="match_parent" android:paddingLeft="@dimen/activity_horizontal_margin"
    android:paddingRight="@dimen/activity_horizontal_margin"
    android:paddingTop="@dimen/activity_vertical_margin"
    android:paddingBottom="@dimen/activity_vertical_margin" tools:context=".MainActivity"
    android:id="@+id/main">

    <TextView android:text="@string/hello_world" android:layout_width="wrap_content"
        android:layout_height="wrap_content" />

</RelativeLayout>

The test is passing now and we've successfully demonstrated how to setup and run a Robolectric test. If you'd like to get hands-on with the example code at this point you can check out out from Github.  Join me next time when we'll go more into depth on test-driving Activities.  Also, I'm always looking for feedback, so please leave comments.  Thank you!

No comments:

Post a Comment