THE PROBLEM
I have two Android classes that I want to test:
CommentContentProvider
, which extendsContentProvider
and is backed by a SQLiteDatabase.CommentActivity
, which extendsActivity
and accessesCommentContentProvider
indirectly through aContentResolver
.
I currently have two test classes:
CommentContentProviderTest
, which extendsProviderTestCase2<CommentContentProvider>
and uses aMockContentResolver
. This works fine.CommentActivityTest
, which extendsActivityInstrumentationTestCase2<CommentActivity>
. This works fine, except for the parts ofCommentActivity
that accessCommentContentProvider
.
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 Context
s 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.