Brain Flush

January 10, 2010

Introducing Calculon – a Java DSL for Android Activity testing

Filed under: Mobile Devices, Software Development & Programming — Tags: , , — Matthias @ 1:05 pm

I have been working on a new test library for Google Android lately, a project which I’m very excited about, and which I feel has loads of potential: Calculon, an Android testing library which lets you write functional tests for your Activities using a very concise and natural syntax.

I am a big fan of test-driven development, and I think there’s no arguing about the massive benefits you get from backing your software up with an automated test suite. The support for writing tests in Android, however, is lacking at best. Even though there has been a test runner since the 1.0 days, writing unit or functional tests in Android is far from being a pleasant experience. For me, the two most important things about tests are that they must be easy to write, and more importantly, easy to read.

That being said, here is a classic example of why Android testing sucks. Our test story is:

Assert that a click on ‘button’ launches a new BarActivity.

Without Calculon, what you do is this:

public void whatInHeavensNameDoesThisEvenTest() {
    final Button b = (Button) getActivity().findViewById(R.id.button);
    ActivityMonitor monitor = getInstrumentation().addMonitor(
        BarActivity.class.getCanonicalName(), null, false);
    getInstrumentation().runOnMainSync(new Runnable() {
        public void run() {
            b.performClick();
        }
    });
    getInstrumentation().waitForIdleSync();
    assertTrue(getInstrumentation().checkMonitorHit(monitor, 1));
}

So, I first have to retrieve a reference to the button that’s supposed to launch a new BarActivity. Then I have to create what Android calls an ActivityMonitor, that’s simply an object which just sits there and waits until an Activity is being launched that matches the description it was configured with. Then I have to perform the actual button click, but sorry, not allowed on the instrumentation thread, so I have to runOnMainSync(). We’re not yet there though: we have to sit and wait, because launching a new activity could take a while, so we also have to waitForIdleSync(). The assertion that follows also couldn’t be less expressive.

When I first wrote this test, my single thought was: Where the heck is my story? It’s buried under a heap of ugly boilerplate code.

I don’t care that the test runner cannot perform a view event on the instrumentation thread. I don’t care that it needs to sit and wait for the Activity to launch. I don’t care about ActivityMonitor and its crummy API. I just want to look at the test, and understand what it’s doing.

Let’s reiterate our test story:

Assert that a click on ‘button’ launches a new BarActivity.

With Calculon, this test is written as:

assertThat(R.id.button).click().starts(BarActivity.class);

Oh the beauty. Our test story was a single sentence, so is the test. That’s how it should be, really. There’s nothing worse than seeing someone else’s tests failing on the build server and having to dissect them line by line to figure out what it’s actually testing.

So let’s move on and look at some nifty Calculon tests. And remember: “Calculon never does two takes!”

Testing with Calculon

Calculon is in a very early stage of development, and currently it only supports writing functional Activity tests, using Android’s ActivityInstrumentationTestCase2. Here is what a Calculon test looks like:

public class FooTest extends FunctionalTest<FooActivity> {
    public FooTest() {
        super("com.example", FooActivity.class);
    }

    public void testStuff() {
        ...
    }
}

Nothing special here, really. You just inherit from a Calculon FunctionalTest.

Assertions

At the heart of a Calculon test are assertions. To create a new assertion, you use the assertThat() method:

public void testStuff() {
    // testing against activities
    assertThat()...
    assertThat(getActivity())...
    assertThat(someOtherActivity)...

    // testing against views
    assertThat(R.id.some_button)...
    assertThat(someButton)...

    // of course all Junit assertions work as well
    assertEquals(...)
    assertNotNull(...)
    ...
}

For now, there are two basic kinds of assertions: ActivityAssertion and ViewAssertion. There is also a third kind, ActionAssertion, but it takes a somewhat special role and is discussed at the end.

Activity Assertions

An ActivityAssertion is an assertion on the state of an Activity and is used like this:

public void testStuff() {
    // testing for an orientation
    assertThat().inPortraitMode();

    // testing for views
    assertThat().viewExists(R.id.some_button);

    // testing for input actions
    assertThat().keyDown(KeyEvent.KEYCODE_BACK)...

    // testing for custom predicates
    assertThat().satisfies(new Predicate<Activity>() {
        public boolean check(Activity target) {
            return target.isTaskRoot();
        }
    });
}

View Assertions

A ViewAssertion is basically the same, just that it tests the state and behavior of a view:

public void testStuff() {
    // testing for view state
    assertThat(R.id.button).isVisible();
    assertThat(R.id.button).isGone();

    // testing for input actions
    assertThat(R.id.button).keyDown(KeyEvent.KEYCODE_BACK)...
    assertThat(R.id.button).click()...
    assertThat(R.id.button).longClick()...

    // testing for custom predicates
    assertThat(R.id.button).satisfies(new Predicate<View>() {
        public boolean check(View target) {
            return target.getVisibility() == View.VISIBLE;
        }
    });
}

Action Assertions

This is a special kind of assertion. You always use it when you want to assert that some action (e.g. a click or a key press) does something. You can use it on both activity and view assertions, and it also allows you to delegate an assertion to another object:

public void testStuff() {
    // testing for actions that launch a new activity
    assertThat(R.id.b1).click().starts(BarActivity.class);

    // testing for actions that finish an activity
    assertThat(R.id.b2).keyDown(KeyEvent.KEYCODE_Q).finishesActivity();

    // testing for actions that change something
    assertThat(R.id.b3).click().implies(R.id.b2).isGone();
    assertThat(R.id.b4).click().implies(getActivity()).inLandscapeMode();
    assertThat(R.id.b5).click().implies(new Predicate<Model>() {
        public boolean check(Model target) {
            return target.someAttribute() == 5;
        }
    });
}

There’s more to come!

This is only a very early version. I intend to expand this library significantly over time. Of course it’s open source, so contributions are always welcome. You can fork the project on GitHub, add your changes, and send me a pull request.

Blog at WordPress.com.