Skip to content
Fabio Collini edited this page Mar 31, 2014 · 2 revisions

Testable Android Apps

This is a demo project of a simple GitHub repository browser, it showcases a Dagger usage that help to write testable code.

App description

The app contains just an Activity with a search field and a button, clicking on button a network call is executed to retrieve and show a list of github repository.

The first time in the morning the app is started a welcome message is displayed with a dialog.

A repository can be shared clicking on an item in the list.

Branch 00_start: Standard Android architecture

https://github.com/fabioCollini/TestableAndroidApps/tree/00_start

The app is developed using standard Android components, network call is executed in an IntentService and the data are passed back to the Activity using a LocalBroadcastManager.

The activity is tested in a simple Espresso test:

public class MainActivityTest extends BaseActivityTest<MainActivity> {

    public MainActivityTest() {
        super(MainActivity.class);
    }

    public void testSearch() {
        onView(withId(R.id.query))
                .perform(ViewActions.typeText("abc"));

        onView(withId(R.id.search))
                .perform(click());

        onData(is(instanceOf(Repo.class))).atPosition(3)
                .perform(click());
    }
}

This test works, but we can't trust it:

  1. the first time in the morning it doesn't work, there is the welcome dialog in front of the search field
  2. it requires an internet connection to work, you need to manually disable wifi to test reload button
  3. if github is down, the test fail (and we need to wait github is down to test error management)
  4. the share is not covered in this test, we don't know if it's invoked correctly

Branch 01_dagger

https://github.com/fabioCollini/TestableAndroidApps/tree/01_dagger

We introduced Dagger to test MainActivity ignoring the welcome dialog.

Branch 02_welcome_dialog_manager_tests

https://github.com/fabioCollini/TestableAndroidApps/tree/02_welcome_dialog_manager_tests

In 01_dagger we ignored welcome dialog, now it's time to test it using some test modules.

Branch 03_githubservice

https://github.com/fabioCollini/TestableAndroidApps/tree/03_githubservice

We don't want to call remote server on each test, in this branch we created a stub class to load and parse a local file.

Branch 04_reload_button

https://github.com/fabioCollini/TestableAndroidApps/tree/04_reload_button

We can test reload button using a stub that throws an exception the first time is used:

public class GitHubServiceErrorStub extends GitHubServiceStub {

    private boolean firstTime = true;

    public GitHubServiceErrorStub(Resources resources, int id) {
        super(resources, id);
    }

    @Override public RepoResponse listRepos(String query) {
        if (firstTime) {
            firstTime = false;
            throw new RuntimeException("Error");
        }
        return super.listRepos(query);
    }
}

Branch 05_activity_module

https://github.com/fabioCollini/TestableAndroidApps/tree/05_activity_module

Dagger scopes can be useful to avoid passing Context object everywhere. Here we create an ActivityModule to provide activity object:

@Module(injects = MainActivity.class, addsTo = AppModule.class)
public static class ActivityModule {
    private FragmentActivity activity;

    public ActivityModule(FragmentActivity activity) {
        this.activity = activity;
    }

    @Provides @Singleton public FragmentActivity provideActivity() {
        return activity;
    }
}

We inject activity directly in other objects:

@Singleton
public class WelcomeDialogManager {

    @Inject FragmentActivity activity;

    @Inject Clock clock;

    @Inject DatePrefsSaver datePrefsSaver;

    public void showDialogIfNeeded() {
        if (isMorning() && !datePrefsSaver.isTodaySaved()) {
            new WelcomeDialog().show(activity.getSupportFragmentManager(), "welcome");
            datePrefsSaver.saveNow();
        }
    }

    public boolean isMorning() {
        int hour = clock.now().get(GregorianCalendar.HOUR_OF_DAY);
        return hour > 6 && hour < 12;
    }
}

Master

Finally we replaced IntentService and LocalBroadcastManager with otto.