9

THE PROBLEM

I have two Android classes that I want to test:

I currently have two test classes:

  • CommentContentProviderTest, which extends ProviderTestCase2<CommentContentProvider> and uses a MockContentResolver. This works fine.
  • CommentActivityTest, which extends ActivityInstrumentationTestCase2<CommentActivity>. This works fine, except for the parts of CommentActivity that access CommentContentProvider.

The problem is that, when CommentActivity accesses CommentContentProvider, it does so through the standard ContentResolver:

ContentResolver resolver = getContentResolver();
Cursor cursor = resolver().query(...);

Thus, when CommentActivityTest is run, it launches CommentActivity, which accesses (read and write) the production database, as shown in the above two lines.

My question is how to make CommentActivity use the standard ContentResolver in production but MockContentResolver during test.

RELATED QUESTIONS

POSSIBLE SOLUTIONS

It would be nice if I could inject a ContentResolver (possibly a MockContentResolver or RenamingDelegatingContext) through the Intent that starts CommentActivity, but I can't do that, since Contexts are not Parcelable.

Which of the following options are best, or is there a better option?

OPTION 1

Add a debug flag to the Intent that starts CommentActivity:

public class CommentActivity extends Activity {
    public static final String DEBUG_MODE = "DEBUG MODE";
    private ContentResolver mResolver;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        :
        // If the flag is not present, debugMode will be set to false.
        boolean debugMode = getIntent().getBooleanExtra(DEBUG_MODE, false);
        if (debugMode) {
            // Set up MockContentResolver or DelegatingContextResolver...
        } else {
            mResolver = getContentResolver();
        }
        :
    }

I don't like this option because I don't like to put test-related code in my non-test classes.

OPTION 2

Use the abstract factory pattern to pass a Parcelable class that either provides the real ContentProvider or a MockContentProvider:

public class CommentActivity extends Activity {
    public static final String FACTORY = "CONTENT RESOLVER FACTORY";
    private ContentResolver mResolver;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        :
        ContentResolverFactory factory = getIntent().getParcelableExtra(FACTORY);
        mResolver = factory.getContentResolver(this);
        :
    }

where I also have:

public abstract class ContentResolverFactory implements Parcelable {
    public abstract ContentResolver getContentResolver(Context context);
}

public abstract class RealContentResolverFactory extends ContentResolverFactory
    public ContentResolver getContentResolver(Context context) {
        return context.getContextResolver();
    }
}

public abstract class MockContentResolverFactory extends ContentResolverFactory
    public ContentResolver getContentResolver(Context context) {
        MockContentResolver resolver = new MockContentResolver();
        // Set up MockContentResolver...
        return resolver;
    }
}

In production, I pass in (via an intent) an instance of RealContentResolverFactory, and in test I pass in an instance of MockContentResolverFactory. Since neither has any state, they're easily Parcelable/Serializable.

My concern about this approach is that I don't want to be "that guy" who overuses design patterns when simpler approaches exist.

OPTION 3

Add the following method to CommentActivity:

public void static setContentResolver(ContentResolver) {
  :
}

This is cleaner than Option 1, since it puts the creation of the ContentResolver outside of CommentActivity, but, like Option 1, it requires modifying the class under test.

OPTION 4

Have CommentActivityTest extend ActivityUnitTestCase<CommentActivity> instead of ActivityInstrumentationTestCase2<CommentActivity>. This lets me set CommentActivity's context through setActivityContext(). The context I pass overrides the usual getContentResolver() to use a MockContentResolver (which I initialize elsewhere).

private class MyContext extends RenamingDelegatingContext {
    MyContext(Context context) {
        super(context, FILE_PREFIX);
    }

    @Override
    public ContentResolver getContentResolver() {
        return mResolver;
    }
}

This works and does not require modifying the class under test but adds more complexity, since ActivityUnitTestCase<CommentActivity>.startActivity() cannot be called in the setUp() method, per the API.

Another inconvenience is that the activity must be tested in touch mode, and setActivityInitialTouchMode(boolean) is defined in ActivityInstrumentationTestCase2<T> but not ActivityUnitTestCase<T>.

FWIW, I am being a little obsessive about getting this right because I will be presenting it in an Android development class I am teaching.

Community
  • 1
  • 1
Ellen Spertus
  • 6,576
  • 9
  • 50
  • 101
  • I am confused about how option 2 works. How is the actual factory selected in prod vs test? – NamshubWriter Mar 24 '14 at 15:25
  • @NamshubWriter, thanks for reading. I added to the question: In production, I pass in (via an intent) an instance of RealContentResolverFactory, and in test I pass in an instance of MockContentResolverFactory. Since neither has any state, they're easily Parcelable/Serializable. – Ellen Spertus Mar 24 '14 at 17:41
  • There is a small spelling error. In option 2 getContextResolver should be getContentResolver. – evgeny.myasishchev Apr 12 '15 at 07:46
  • In the same situation. I like option 3, but unfortunately keeping that in a static field leads to memory leaks (as warned in AS 2.2) – sargas Jun 01 '16 at 16:37
  • @EllenSpertus: Did you ever settle on a good solution? – MWB Oct 20 '18 at 12:12
  • @MWB No, I haven't. – Ellen Spertus Oct 20 '18 at 14:04
  • 1
    @EllenSpertus: I just posted a question of my own on the subject: https://stackoverflow.com/questions/52905933/test-method-that-uses-contentprovider – MWB Oct 20 '18 at 14:05

1 Answers1

1

Option 2 seems best to me. I'm not bothered about the use of a factory; I'm more bothered by the intent causing a change in behavior at a distance. But the other solutions put non-production code in the production code, so what you are testing isn't much like how things work in production. Hope that helps.

NamshubWriter
  • 23,549
  • 2
  • 41
  • 59