Using RenamingDelegatingContext to mock ContentResolver in Android

Mocking Context

Testing in Android can be complex, especially when your component is not isolated from the Android framework. One example of this is when your component is performing file system tasks using Context. E.g, the Activity class has the following methods (inherited from Context):

openOrCreateDatabase();
deleteDatabase();
getDatabasePath();
openFileInput();
openFileOutput();
getFileStreamPath();
deleteFile();
getCacheDir();

When calling any of the methods above, Context directs the calls to parts of the file system dedicated to your specific application, usually located under /data/data/<your.applications.package.name>/. To mock away these type of operations, there is a component called RenamingDelegatingContext. It basically allows you to replace the context used when calling file system methods, re-directing the call to a mocked data file instead of the real file. The source code for RenamingDelegatingContext can be looked at here:

RenamingDelegatingContext

There are a number of tutorials on how to use RenamingDelegatingContext to mock away your files or databases;

Advanced use of RenamingDelegatingContext

In addition to open resources directly using Context, one common task is to open a resoure using a ContentResolver:

Uri uri = Uri.parse("content://authority/path");
InputStream is = getContext().getContentResolver().openInputStream(uri);

The getContext() call can be omitted if you’re making the call from within an Activity or Service, where the component itself is a Context.

One might want the returned InputStream to point to a test resource instead of the real resource. This can be achieved with some research. When opening the InputStream, the ContentResolver finds the proper provider for the given authority (see ContentProvider section for more detail). The source code reveals more on how this is done:

ContentResolver.openInputStream

public final InputStream openInputStream(Uri uri) throws FileNotFoundException {
   ...
   } else {
       AssetFileDescriptor fd = openAssetFileDescriptor(uri, "r");
       try {
           return fd != null ? fd.createInputStream() : null;
       } catch (IOException e) {
           throw new FileNotFoundException("Unable to create stream");
       }
   }
}

ContentResolver.openAssetFileDescriptor

public final AssetFileDescriptor openAssetFileDescriptor(Uri uri, String mode) throws FileNotFoundException {
    ...
    } else {
        IContentProvider provider = acquireProvider(uri);
        ...
        try {
            AssetFileDescriptor fd = provider.openAssetFile(uri, mode);
            ...
            ParcelFileDescriptor pfd = new ParcelFileDescriptorInner(fd.getParcelFileDescriptor(), provider);
            return new AssetFileDescriptor(pfd, fd.getStartOffset(), fd.getDeclaredLength());
        } catch ( ...

As seen in the extracts above, the ContentResolver acquires the correct provider based on the given Uri (authority). In order for us to mock away this behavior, we need to do four things:

  1. Use a RenamingDelegatingContext to mock away the real Context (target Context) in the test.
  2. Override the getContentResolver in RenamingDelegatingContext to return a MockContentResolver.
  3. Create a ContentProvider to return a AssetFileDescriptor pointing to the test resource.
  4. Register the ContentProvider with the MockContentResolver to handle the authority we want to override.

Mocking ContentResolver

public class MockContentResolverTestCase extends AssetManagerAndroidTestCase {

    public static final String URI_PREFIX = "content://";
    public static final String URI_AUTHORITY = "com.my.authority";
    public static final String URI_PATH = "/additional_path/";
    public static final String URI_FULL = URI_PREFIX + URI_AUTHORITY + URI_PATH;

    private RenamingSystemSettingsContext mockContext;
    private LinkedList assetFileQueue;

    public void createMockContext(Context targetContext) {
        mockContext = new RenamingSystemSettingsContext(targetContext);
        assetFileQueue = new LinkedList();
    }

    public Context getMockContext() {
        return mockContext;
    }

    public void pushAssetFile(String fileName) {
        assetFileQueue.offer(fileName);
    }

    public class RenamingSystemSettingsContext extends RenamingDelegatingContext {

        private static final String PREFIX = "test.";   // Not actually used, but needed by RenamingDelegatingContext

        public RenamingSystemSettingsContext(Context targetContext) {
            super(targetContext, PREFIX);
            super.makeExistingFilesAndDbsAccessible();
        }

        @Override
        public ContentResolver getContentResolver() {
            MockContentProvider provider = new MockContentProvider() {
                AssetFileDescriptor ret = null;

                @Override
                public AssetFileDescriptor openAssetFile(Uri uri, String mode) throws FileNotFoundException {
                    try {
                        ret = openTestResourceAsAssetFileDescriptor(assetFileQueue.poll());
                    } catch (Exception e) {
                        e.printStackTrace();
                        throw new FileNotFoundException();
                    }
                    return ret;
                }
            };
            MockContentResolver resolver = new MockContentResolver();
            resolver.addProvider(URI_AUTHORITY, provider);
            return resolver;
        }
    }
}

AssetManagerAndroidTestCase

public class AssetManagerAndroidTestCase extends AndroidTestCase {
    /*
      * For unknown reason, the 'getTestContext' method is hidden in source. Use
      * reflection to access it.
      */
    private AssetManager getAssetManagerForTestContext() throws Exception {
        Method m = AndroidTestCase.class.getMethod("getTestContext",
                new Class[]{});
        mContext = (Context) m.invoke(this, (Object[]) null);
        return mContext.getAssets();
    }
    protected InputStream openTestResourceAsStream(String fileName) throws Exception {
        AssetManager testAssets = getAssetManagerForTestContext();
        return testAssets.open(fileName, AssetManager.ACCESS_STREAMING);
    }

    protected AssetFileDescriptor openTestResourceAsAssetFileDescriptor(String fileName) throws Exception {
        AssetManager testAsset = getAssetManagerForTestContext();
        return testAsset.openFd(fileName);
    }
}

Creating an example test

In the following example, the test_file.midi is a text file stored in the res/asset/ directory. The .midi extension is a workaround to prevent the Android Asset Packaging Tool (aapt) from compressing the file. If the file is compressed, it can only be opened as an InputStream and not as a FileDescriptor, which is needed for mocking away the ContentProvider. See this bug for more info.

public class ExampleTests extends MockContentResolverTestCase {

    public void testUseCustomSettingsFile() throws IOException {
        String assetFile = "test_file.midi";
        createMockContext(getContext());
        pushAssetFile(assetFile);
        Uri uri = Uri.parse(URI_FULL);

        InputStream is = getMockContext().getContentResolver().openInputStream(uri);

        assertEquals("Text in test file", getStringFromStream(is));
    }

    public static String getStringFromStream(InputStream is) throws IOException {
        int size = is.available();
        byte[] buf = new byte[size];
        is.read(buf);
        return new String(buf);
    }
}

That’s it!

Leave a Reply

Close Menu