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
Activity
.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.
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.
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.
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.
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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With