Samstag, 14. März 2015

Android Studio 1.1.0 and Robolectric 3.0

Step by step guide how to use Robolectric within Android Studio


Ready to use examples can be found at https://github.com/nenick/AndroidStudioAndRobolectric

Android Project


Create or open an existing android project with Android Studio. I used the "Blank Activity with Fragment" to proof my step by step guide and choosed Android SDK 21 as my target.

Latest tested build tools version:
classpath 'com.android.tools.build:gradle:1.1.3'

Speed up Unit Test compilation (Optional)


This part is optional but improve a bit the test compiling time. Replace the "Make" task with  "Gradle-Aware-Task" for your run configuration (Run configuration -> Defaults -> JUnit -> Run External). 


1. Start with a simple JUnit Test


Next Step is to enable the new experimental Unit Test support for Android Studio. Here is a guide from google: http://tools.android.com/tech-docs/unit-testing-support

Add JUnit test dependencies


  testCompile 'junit:junit:4.12'
  testCompile "org.mockito:mockito-core:1.9.5"

And activate the experimental unit test feature: Settings / Gradle / Experimental

Now write a simple test and check if the unit test support works.

import org.junit.Test;
import static org.hamcrest.CoreMatchers.is;
import static org.hamcrest.MatcherAssert.assertThat;

public class SimpleUnitTest {
    @Test
    public void checkJUnitWork() {
        // failing test gives much better feedback 
        // to show that all works correctly ;)
        assertThat(true, is(false)); 
    }
}

Just right click on class or method and choose > Run and you will see a failing test, when you replace the expected value with true then the test should be successful.


2. Add initial Robolectric support


Add Robolectric as test dependency and sync your gradle configuration.

repositories {
    maven { url = "https://oss.sonatype.org/content/repositories/snapshots" }
}
dependencies {
    testCompile "org.robolectric:robolectric:3.0-SNAPSHOT"
}

Now you can use Robolectric to write tests. First we will only check if a Robolectric faked android context exists.

import org.junit.Test;
import org.junit.runner.RunWith;
import org.robolectric.RuntimeEnvironment;
import org.robolectric.RobolectricTestRunner;
import static org.hamcrest.CoreMatchers.nullValue;
import static org.hamcrest.CoreMatchers.notNullValue;
import static org.hamcrest.MatcherAssert.assertThat;

@RunWith(RobolectricTestRunner.class)
public class RobolectricTest {
    @Test
    public void testIt() {
        // failing test gives much better feedback
        // to show that all works correctly ;)
        assertThat(RuntimeEnvironment.application, nullValue());
    }
}

Just right click on class or method and choose > Run and you will see a failing test, when you replace the expected value with notNullValue() then the test should be successful.

3. Setup Robolectric Tests


If you now try to use a simple Robolectric example test you will only see failing tests and strange errors which give less feedback for the root cause https://github.com/robolectric/robolectric but here are the solutions.

Add TextView with id for testing

A fresh created android project contains most times a text view with "Hello World". Give this TextView an id to be accessible from code.

Test the TextView content

Let's start with basic test based on Robolectric which will first not work but then we fix all the errors step by step.

import android.app.Activity;
import android.widget.TextView;
import com.example.myapplication.MainActivity;
import com.example.myapplication.R;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.robolectric.Robolectric;
import static org.hamcrest.CoreMatchers.equalTo;
import static org.hamcrest.MatcherAssert.assertThat;

@RunWith(CustomRobolectricRunner.class)
public class RobolectricTest {
    @Test
    public void testIt() {
        Activity activity = 
                Robolectric.setupActivity(MainActivity.class);

        TextView results = 
                (TextView) activity.findViewById(R.id.textView);
        String resultsText = results.getText().toString();
        
        // failing test gives much better feedback
        // to show that all works correctly ;)
        assertThat(resultsText, equalTo("Testing Android Rocks!"));
    }
}

The CustomRobolectricRunner is optional when you prefer the @Config annotation. I prefer the custom runner because then there is less configuration at Android Studio necessary.

import org.junit.runners.model.InitializationError;
import org.robolectric.RobolectricTestRunner;
import org.robolectric.annotation.Config;

public class CustomRobolectricRunner extends RobolectricTestRunner {
    public CustomRobolectricRunner(Class<?> testClass) 
                throws InitializationError {
        super(testClass);
    }
}



Now start the test (which will fail) to get a feeling what we do at the following step.

4. Manifest location must be known


This is an important warning wich must be avoided:

WARNING: No manifest file found at ./AndroidManifest.xml.Falling back to the Android OS resources only.

 the next try could be to adjust the "Working Directory" of the default unit test configuration pointing to the module directory "app". This will work but would us force to make custom settings to the run configuration and will not work great with multimodule projects.

With this guide, we will try to have minimal configuration effort and easy support for many setups. So here comes another solution where we make the AndroidManifest.XML location dynamic.

Remove the @Config Annotation and extend CustomRobolectricRunner constructor.

public CustomRobolectricRunner(Class<?> testClass)
        throws InitializationError {
    super(testClass);
    String buildVariant = (BuildConfig.FLAVOR.isEmpty()
            ? "" : BuildConfig.FLAVOR+ "/") + BuildConfig.BUILD_TYPE;
    String intermediatesPath = BuildConfig.class.getResource("")
            .toString().replace("file:", "");
    intermediatesPath = intermediatesPath
            .substring(0, intermediatesPath.indexOf("/classes"));

    System.setProperty("android.package", 
            BuildConfig.APPLICATION_ID);
    System.setProperty("android.manifest",
            intermediatesPath + "/manifests/full/" 
                    + buildVariant + "/AndroidManifest.xml");
    System.setProperty("android.resources", 
            intermediatesPath + "/res/" + buildVariant);
    System.setProperty("android.assets", 
            intermediatesPath + "/assets/" + buildVariant);
}

Expected result: You will not see the warning with Android Studio or command-line and a new message should appear likely following:

DEBUG: Loading resources for com.example.myapplication from ./app/src/main/res...
DEBUG: Loading resources for android from jar:/Users/UserName/.m2/repository/org/robolectric/android-all/4.3_r2-robolectric-0/android-all-4.3_r2-robolectric-0.jar!/res...

You are ready. More examples can be found at https://github.com/nenick/AndroidStudioAndRobolectric

Samstag, 7. Februar 2015

Android Studio 1.1.0 (Beta 4) and Robolectric 2.4


(Depricated) 



Step by step guide how to enable Robolectric Support



Until now it was not easy to use Robolectric with gradle and Android Studio, but the new experimental unit test feature may remove the necessity of extra plugins.

An example can be found at https://github.com/nenick/AndroidStudioAndRobolectric. The commits are separated like the steps of this guide.

For a flavors example see https://github.com/nenick/AndroidStudioAndRobolectric/tree/flavors.

Java Version


This guide was only tested with Java 7.

Android Studio


Download and install Android Studio http://developer.android.com/sdk/index.html

Update to Android Studio 1.1.0 Beta 4 from the Beta Channel. After the start and a pre-setup tour of Android Studio, select Configure/Preferences/Updates. Here switch to Beta Channel and "Check Now" for updates.

Android Project


Create or open an existing android project with Android Studio. I used the "Blank Activity with Fragment" to proof my step by step guide.

As the target and minimal android SDK version you can use what you like. Later we will force that Robolectric tests run with v18, but this may be ignored until you use APIs which only exists in versions above v18. Current the Robolectric team works on support for android versions above v18, but there exist no release at this time.

At your android project root build.gradle file you must set the new build version. After that sync your project.

classpath 'com.android.tools.build:gradle:1.1.0-rc1'

Speed up Unit Test compilation (Optional)


This part may be optional but improve a bit the test compiling time. Open you run configuration and select Defaults / JUnit. Here remove the make task and add Gradle-Aware-Task (let the Select Gradle Task empty).

1. Start with a simple JUnit Test


Next Step is to enable the new experimental Unit Test support for Android Studio. Here is a guide from google: http://tools.android.com/tech-docs/unit-testing-support

Add JUnit test dependencies


  testCompile 'junit:junit:4.12'
  testCompile "org.mockito:mockito-core:1.9.5"

And activate the experimental unit test feature: Settings / Gradle / Experimental

Now write a simple test and check if the unit test support works.

import org.junit.Test;
import static org.hamcrest.CoreMatchers.is;
import static org.hamcrest.MatcherAssert.assertThat;

public class SimpleUnitTest {
    @Test
    public void checkJUnitWork() {
        // failing test gives much better feedback 
        // to show that all works correctly ;)
        assertThat(true, is(false)); 
    }
}

Just right click on class or method and choose > Run and you will see a failing test, when you replace the expected value with true then the test should be successful.

2. Add initial Robolectric support


Add Robolectric as test dependency and sync your gradle configuration.

testCompile "org.robolectric:robolectric:2.4"

Now you can use Robolectric to write tests. First we will only check if a Robolectric faked android context exists.

import org.junit.Test;
import org.junit.runner.RunWith;
import org.robolectric.Robolectric;
import org.robolectric.RobolectricTestRunner;
import static org.hamcrest.CoreMatchers.nullValue;
import static org.hamcrest.CoreMatchers.notNullValue;
import static org.hamcrest.MatcherAssert.assertThat;

@RunWith(RobolectricTestRunner.class)
public class RobolectricTest {
    @Test
    public void testIt() {
        // failing test gives much better feedback
        // to show that all works correctly ;)
        assertThat(Robolectric.application, nullValue());
    }
}

Just right click on class or method and choose > Run and you will see a failing test, when you replace the expected value with notNullValue() then the test should be successful.

3. Setup Robolectric Tests


If you now try to use a simple Robolectric example test you will only see failing tests and strange errors which give less feedback for the root cause https://github.com/robolectric/robolectric but here are the solutions.

Add TextView with id for testing

A fresh created android project contains most times a text view with "Hello World". Give this TextView an id to be accessible from code.

Test the TextView content

Let's start with basic test based on Robolectric which will first not work but then we fix all the errors step by step.

import android.app.Activity;
import android.widget.TextView;
import com.example.myapplication.MainActivity;
import com.example.myapplication.R;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.robolectric.Robolectric;
import static org.hamcrest.CoreMatchers.equalTo;
import static org.hamcrest.MatcherAssert.assertThat;

@RunWith(CustomRobolectricRunner.class)
public class RobolectricTest {
    @Test
    public void testIt() {
        Activity activity = 
                Robolectric.setupActivity(MainActivity.class);

        TextView results = 
                (TextView) activity.findViewById(R.id.textView);
        String resultsText = results.getText().toString();
        
        // failing test gives much better feedback
        // to show that all works correctly ;)
        assertThat(resultsText, equalTo("Testing Android Rocks!"));
    }
}

The CustomRobolectricRunner is optional when you prefer the @Config annotation. I prefer the custom runner because then there is less configuration at Android Studio necessary.

import org.junit.runners.model.InitializationError;
import org.robolectric.RobolectricTestRunner;
import org.robolectric.annotation.Config;

public class CustomRobolectricRunner extends RobolectricTestRunner {
    public CustomRobolectricRunner(Class<?> testClass) 
                throws InitializationError {
        super(testClass);
    }
}

Now start the test (which will fail) to get a feeling what we do at the following 4 parts.

A. Manifest location must be known


This is the most important warning wich must be avoided:

WARNING: No manifest file found at ./AndroidManifest.xml.Falling back to the Android OS resources only.

This warning comes also when you use the command-line with the task testDebug.

One way to fix it is to annotate your test class with @Config annotation.

@Config(manifest = "app/src/main/AndroidManifest.xml")
public class RobolectricTest {

But this conflicts with the command-line task testDebug where this warning will still exist. This could be fixed by adjusting the path but then Android Studio fails.

@Config(manifest = "src/main/AndroidManifest.xml")

So the next try could be to adjust the "Working Directory" of the default unit test configuration pointing to the module directory "app". This will work but would us force to make custom settings to the run configuration and will not work great with multimodule projects.

With this guide, we will try to have minimal configuration effort and easy support for many setups. So here comes another solution where we make the AndroidManifest.XML location dynamic.

Remove the @Config Annotation and add the following method to the CustomRobolectricRunner.

    @Override
    protected AndroidManifest getAppManifest(Config config) {
        String path = "src/main/AndroidManifest.xml";

        // android studio has a different execution root for tests than pure gradle
        // so we avoid here manual effort to get them running inside android studio
        if (!new File(path).exists()) {
            path = "app/" + path;
        }

        config = overwriteConfig(config, "manifest", path);
        return super.getAppManifest(config);
    }

    protected Config.Implementation overwriteConfig(
            Config config, String key, String value) {
        Properties properties = new Properties();
        properties.setProperty(key, value);
        return new Config.Implementation(config, 
                Config.Implementation.fromProperties(properties));
    }

Expected result: You will not see the warning with Android Studio or command-line and a new message should appear likely following:

DEBUG: Loading resources for com.example.myapplication from ./app/src/main/res...
DEBUG: Loading resources for android from jar:/Users/UserName/.m2/repository/org/robolectric/android-all/4.3_r2-robolectric-0/android-all-4.3_r2-robolectric-0.jar!/res...

B. Force specific android version for Robolectric


If you don't have this error then you may skip this part.

java.lang.UnsupportedOperationException: Robolectric does not support API level 1, sorry!

Here you may also use the @Config annotation.

@Config(emulateSdk = 18)

Or use the CustomRobolectricRunner to do it only once for all tests and override the following method.

    @Override
    protected SdkConfig pickSdkVersion(
            AndroidManifest appManifest, Config config) {
        // current Robolectric supports not the latest android SDK version
        // so we must downgrade to simulate the latest supported version.
        config = overwriteConfig(config, "emulateSdk", "18");
        return super.pickSdkVersion(appManifest, config);
    }

Expected result: You will not see anymore the "not supported API" error.

C. Could not find any resource (Libraries, Style, Themes)


If you don't have this error then you may skip this part. You must only do it when you use libraries wich have it own resources.

The error comes in many variations so here just an example.

java.lang.RuntimeException: Could not find any resource  from reference ResName{com.example.myapplication:style/Theme_AppCompat_Light_DarkActionBar} from style StyleData{name='AppTheme', parent='Theme_AppCompat_Light_DarkActionBar'} with theme null

This issue is most times related to missing resources which comes from libraries like support-v4 or appcompat-v7. 

Next step is to make them known to Robolectric. For that, we will create following two files next to the AndroidManifest.xml
  • app/src/main
    • AndroidManifest.xml
    • project.properties (dummy but without it Robolectric would produce a NullPointerException)
    • test-project.properties
And at the test-project.properties we add all exploded-aar sources. Check that the path details will match with your versions. they can be found under app/build/intermediates/exploded-aar/...

android.library.reference.1=../../build/intermediates/exploded-aar/com.android.support/appcompat-v7/21.0.3
android.library.reference.2=../../build/intermediates/exploded-aar/com.android.support/support-v4/21.0.3


Expected result: Now you get a different "resource not found exception" and new messages should appear likely following:

DEBUG: Loading resources for android.support.v7.appcompat from ./app/src/main/../../build/intermediates/exploded-aar/com.android.support/appcompat-v7/21.0.3/res...
DEBUG: Loading resources for android.support.v4 from ./app/src/main/../../build/intermediates/exploded-aar/com.android.support/support-v4/21.0.3/res...

D. Resource not found (source is the SupportMenuInflater)


If you don't have this error then you may skip this part. This error comes only when you use the support library for stuff like support Fragments.

The error message looks like:

android.content.res.Resources$NotFoundException: Resource ID #0x7f0d0000
 at android.content.res.Resources.getValue(Resources.java:1118)
 at android.content.res.Resources.loadXmlResourceParser(Resources.java:2304)
 at android.content.res.Resources.getLayout(Resources.java:934)
 at android.support.v7.internal.view.SupportMenuInflater.inflate(SupportMenuInflater.java:115)
 at com.example.nkuchler.myapplication.MainActivity.onCreateOptionsMenu(MainActivity.java:32)

The solution comes from this report https://github.com/robolectric/robolectric/issues/898 and is to create a shadow class at your test package.

import org.robolectric.annotation.Implementation;
import org.robolectric.annotation.Implements;
import org.robolectric.shadows.ShadowMenuInflater;
import android.support.v7.internal.view.SupportMenuInflater;
import android.view.Menu;

@Implements(SupportMenuInflater.class)
public class ShadowSupportMenuInflater extends ShadowMenuInflater {
    @Implementation
    public void inflate(int menuRes, Menu menu) {
        super.inflate(menuRes, menu);
    }
}

Now you have again the choice to use the @Config annotation to add the shadow class to Robolectric.

@Config(shadows = {ShadowSupportMenuInflater.class})

Or do it once for all tests and override the following method.

    @Override
    protected void configureShadows(SdkEnvironment sdkEnvironment, Config config) {
        Properties properties = new Properties();
        // to add more shadows use white space separation + " " +
        properties.setProperty("shadows", ShadowSupportMenuInflater.class.getName());
        super.configureShadows(sdkEnvironment, new Config.Implementation(config, Config.Implementation.fromProperties(properties)));
    }

Expected result: The test is running and show you that there is a different string than expected.

Some open questions


Do we need other plugins to enable testing with Robolectric?


Current I think no. Maybe there will be a plugin to avoid some last config overhead like including exploded-aar packages.

Why I can't use org.robolectric.Config.properties?


Looks like the new experimental Unit Test support does not support tests resources yet. The do not appear under the build directory after a build.

Is there something what I missed?


I guess there are aspects which will not work with this setup, please report them when you found one.