Test better with marvin

The Android SDK supports testing. However, this testing is cumbersome. Indeed, Android tests focuses on one components, and does not easily support cross-component tests (scenarios or stories). Moreover, simulating user actions, such as key input, require a lot of boilerplate. Marvin is a test library simplifying the development of Android tests by providing a high-level API to test complex scenarios. Using marvin reduce thew code you need to test your application, allow advanced tests and so make your application more robust.

  • Source Code: GitHub
  • License: Apache License 2.0

Motivation

Testing Android applications using Android instrumentation is possible but exhausting. It involves writing a lot of boiler-plate code. Nowadays, there are several test frameworks around (e.g. Robotium) that offer a much simpler API to have a much clearer test story and increase readability and maintainability.

When testing several activities at once, especially the interaction between them, your test must monitor which activities are started at which point in time. Assertions you define will be related to one of several activity instances. And your test has to shutdown all activities once your are through to ensure a clean state for the next test.

This is where Marvin comes in. Its simple API let's you start activities, wait for activities that are started by the production code, inject events and define assertions on the behavior. At then end of each test, Marvin will shut down all started Activities, leaving the device in a clean state for the next test.

Consider the following scenario: You want to check whether the main activity is displayed after entering user credentials. To test this behavior requires the following steps:

  1. Enter username

  2. Enter password

  3. Click the login button

  4. Wait for the main activity to appear

  5. Confirm that the main activity did load

public class LoginActivityTest extends AndroidTestCase {
    public void testLogin() {
        Instrumentation instrumentation = getInstrumentation();

        ActivityMonitor monitor = instrumentation
			.addMonitor(MainActivity.class.getName(), null, false);
        Intent intent = new Intent(Intent.ACTION_MAIN);
        intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
        intent.setClassName(instrumentation.getTargetContext(),
			LoginActivity.class.getName());
        final LoginActivity loginActivity= 
			(LoginActivity) instrumentation.startActivitySync(intent);
        instrumentation.waitForIdleSync();

        injectViewTestActivity.runOnUiThread(new Runnable() {
            public void run() {
                EditText username = loginActivity
					.findViewById(R.id.username);
                EditText password = loginActivity
					.findViewById(R.id.password);

				username.setText("Username");	// Step 1
                password.setText("Password");	// Step 2
                loginActivity.findViewById(R.id.loginbutton)
					.performClick();	// Step 3
            }
        });

        Activity mainActivity = instrumentation
			.waitForMonitorWithTimeout(monitor, 30000); // Step 4
        assertNotNull(mainActivity); // Step 5
    }
}

With Marvin the code would look like this

public class LoginActivityTest extends ActivityTestCase<> {
    public LoginActivityTest(Class activityType) {
        super(LoginActivity.class);
    }

    public void testLogin() {
		// Step 1
        activity().view().withId(R.id.username).setText("Username");
		// Step 2
        activity().view().withId(R.id.password).setText("Password");
		// Step 3
        activity().view().withId(R.id.loginbutton).click();
		// Step 4
        Activity mainActivity = await().activity(MainActivity.class, 30,
			TimeUnit.SECONDS);
        assertNotNull(mainActivity); //Step 5
    }
}

Brief description

To use Marvin, your test will extend one of Marvins TestCase classes: AndroidTestCase, AndroidActivityTestCase, AndroidServiceTestCase.

The following shows an example test using Marvin:

public class SearchTest extends AndroidTestCase {
    public void testSearchWorkflow() throws Exception {
        // Start SearchActivity. This call will block until the activity
        // is started and has completed its onResume cycle.
        SearchActivity searchActivity = 
			perform().startActivity(SearchActivity.class);

        // The activity might need some time to be fully operable.
        // Let's wait until particular view becomes visible that we
        // want to use. If it does not become visible within 10 secs,
        // we fail.
        await().view(searchActivity, R.id.editTextInput, 10,
			TimeUnit.SECONDS);

        // Set some content for the EditText field.
        activity(searchActivity).view(R.id.editTextSearch)
			.setText("test");

        // Click a button.
        activity(searchActivity).view(R.id.buttonSubmit).click();

        // This should start ResultsActivity.
		// Let's wait for that one (20 seconds max).
        ResultsActivity resultsActivity =
                await().activity(ResultsActivity.class, 20,
					TimeUnit.SECONDS);
        // The activity must be non-null or we fail.
        assertThat(resultsActivity, notNullValue());

        // We know that ResultsActivity does a server call and will
        // then populate its list view with results. 
		// Let's wait until one list view item becomes visible. 
        await().view(resultsActivity, R.id.resultEntry, 30,
			TimeUnit.SECONDS);

        // Our ResultsActivity must have at least one text view
		// with the word "test".
        View view = activity(resultsActivity).findTextView(".*test.*")
			.getView();
        assertThat(view, notNullValue());
    }
}

General usage

ActivityTestCase is a convenience class for automatically starting a specific activity for all test cases. The activity will be started during setUp and stopped on tearDown.

public class MyActivityTest extends ActivityTestCase {
    public MyActivityTest () {
        super(MyActivity.class);
    }
    ...
}

Interacting with activities

Marvin lets you interact with your by using actions. To get available options you can use the methods perform(), await() and activity() or activity(Activity).

perform()

perform() allows user operations that are not tied to a specific activity. The available operations include:

  • click(float,float) - will cause a click on the specified coordinates
  • sleep(int) - sleeps for the specified time
  • instrument() - allows access to the android instrumentation API
  • startActivity(Intent) - starts an activity for the given intent (this is syncronous)
  • startActivity(Class) - starts an activity of the given class (this is syncronous)
  • bindService(Intent, long, TimeUnit) - binds a service for the given intent (this is syncronous)
  • bindService(Class, long, TimeUnit) - binds a service of the given class (this is syncronous)

Example:

perform().click(0,0); //clicks the top left corner of the screen;

await()

await() operations are used to wait for certain events like the start of an activity or the initialization of a specific view.

  • condition(Condtion, long, TimeUnit) - waits a specific time for the condition to be true, throws TimeoutException if the condition remains false
  • condition(Matcher, long, TimeUnit) - waits for the specified matcher to match
  • activity(Activity, long, TimeUnit) - waits for the start of the specified activity, the activity is considerd started once onCreate is finished
  • idle() - waits until application is idle

activity()

activity() and activity(Activity) can be used to access activity specific operations. Those include:

  • finish() - stops the activity
  • view() - allows access to different views inside the activity, see description of view()
  • flipOrientation() - toggles between portrait and landscape
  • setOrientation(int) - sets the specific orientation

view()

  • withId(int) - returns the view with the id
  • withText(RegExp) - returns the view containing the specified text
  • root() - returns the root view of the activity

Android specific Hamcrest matcher

  • hasText(String) - available for views, checks if the view has the given text, will fail if the text does not match or the view is not a text view
  • isEnabled() - available for views, checks if the view is enabled
  • isOnScreen() - available for views, checks if the view is drawn inside the screen
  • isVisible() - available for views, checks if the view is visible
  • ViewGroupComparison(ViewGroup) - available for viewgroups, checks if the view group has the same number of child views

Global view on activities

To observe the current state of all launched activities you can use the activityMonitor:

  • getMostRecentlyStartedActivity() - returns the last started activity
  • getStartedActivities() - returns all currently running activities
  • waitForActivity(Class, long, TimeUnit) - waits for the start of a specific activity

Download

You can download the Marvin sources from github. We also provides binaries for convenience:

  • marvin-1.1.0.jar (just add this jar to your build path)
  • Maven dependency (to use with the maven-android-plugin)
<dependency>
    <groupId>de.akquinet.android.marvin</groupId>
    <artifactId>marvin</artifactId>
    <version>1.1.0</version>
    <<scope>compile</scope>
</dependency>

Marvin is available on maven central, so you don't need to customize your maven settings.