Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to test an Activity that uses a ContentProvider without affecting production database?

THE PROBLEM

I have two Android classes that I want to test:

  • CommentContentProvider, which extends ContentProvider and is backed by a SQLiteDatabase.
  • CommentActivity, which extends Activity and accesses CommentContentProvider indirectly through a ContentResolver.

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

  • This differs from Android unit testing with ContentProviders and other questions I've found about testing ContentProviders because those can extend android.test classes designed for testing ContentProviders while I need to extend a class for testing an Activity.
  • This is similar to How to inject a dependency when testing an Android activity without a third-party framework?, which was also posted by me but is unanswered. I am now willing to use a third-party framework if it will help.
  • Query using MockContentResolver leads to NullPointerException is related and leads to the solution in Option 1 below, but I do not know if it is the best solution in my case.

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.

like image 204
Ellen Spertus Avatar asked Mar 22 '14 23:03

Ellen Spertus


People also ask

What is a ContentProvider and what is it typically used for?

A content provider manages access to a central repository of data. A provider is part of an Android application, which often provides its own UI for working with the data. However, content providers are primarily intended to be used by other applications, which access the provider using a provider client object.

How can an application activate ContentProvider?

Create a class in the same directory where the that MainActivity file resides and this class must extend the ContentProvider base class. To access the content, define a content provider URI address. Create a database to store the application data. Implement the six abstract methods of ContentProvider class.

How do I test my content provider?

To test your content provider in isolation, use the ProviderTestCase2 class. This class allows you to use Android mock object classes such as IsolatedContext and MockContentResolver to access file and database information without affecting the actual user data.


1 Answers

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.

like image 180
NamshubWriter Avatar answered Sep 19 '22 12:09

NamshubWriter