Android Data Binding with Robolectric 3

Just over a week ago at Google I/O Google announced the new Data Binding library.

If, like me, you were keen to give it a try and dropped it into an existing project with Robolectric, upon runing your test suite you will find that every single unit test fails with an exception:

java.lang.RuntimeException: build/intermediates/res/debug/values is not a directory

There seems to be a trivial change in the file structure of the build/intermediates/res folder when the Data Binding library is included - The res/buildtype folder becomes res/merged/buildtype.

Fortunately, there is an equally trivial fix. The RobolectricGradleTestRunner class has a line which defines where the resources are:

final FileFsFile res = FileFsFile.from(BUILD_OUTPUT, "res", flavor, type);

With a simple modification to the test runner, you can test to make sure the directory exists and fall back to the merged directory so that everything works nicely with the Data Binding library:

final FileFsFile res;
if (FileFsFile.from(BUILD_OUTPUT, "res", flavor, type).exists()) {
	res = FileFsFile.from(BUILD_OUTPUT, "res", flavor, type);
} else {
	// Use res/merged if the output directory doesn't exist for Data Binding compatibility
    res = FileFsFile.from(BUILD_OUTPUT, "res/merged", flavor, type);
}

I created a custom runner called RobolectricDataBindingTestRunner and used it in place of the RobolectricGradleTestRunner and everything worked great after this.

Full source:

public class RobolectricDataBindingTestRunner extends RobolectricTestRunner {

    private static final String BUILD_OUTPUT = "build/intermediates";

    public RobolectricDataBindingTestRunner(Class<?> klass) throws InitializationError {
        super(klass);
    }

    @Override
    protected AndroidManifest getAppManifest(Config config) {
        if (config.constants() == Void.class) {
            Logger.error("Field 'constants' not specified in @Config annotation");
            Logger.error("This is required when using RobolectricGradleTestRunner!");
            throw new RuntimeException("No 'constants' field in @Config annotation!");
        }

        final String type = getType(config);
        final String flavor = getFlavor(config);
        final String applicationId = getApplicationId(config);

        final FileFsFile res;
        if (FileFsFile.from(BUILD_OUTPUT, "res", flavor, type).exists()) {
            res = FileFsFile.from(BUILD_OUTPUT, "res", flavor, type);
        } else {
            // Use res/merged if the output directory doesn't exist for Data Binding compatibility
            res = FileFsFile.from(BUILD_OUTPUT, "res/merged", flavor, type);
        }
        final FileFsFile assets = FileFsFile.from(BUILD_OUTPUT, "assets", flavor, type);

        final FileFsFile manifest;
        if (FileFsFile.from(BUILD_OUTPUT, "manifests").exists()) {
            manifest = FileFsFile.from(BUILD_OUTPUT, "manifests", "full", flavor, type, "AndroidManifest.xml");
        } else {
            // Fallback to the location for library manifests
            manifest = FileFsFile.from(BUILD_OUTPUT, "bundles", flavor, type, "AndroidManifest.xml");
        }

        Logger.debug("Robolectric assets directory: " + assets.getPath());
        Logger.debug("   Robolectric res directory: " + res.getPath());
        Logger.debug("   Robolectric manifest path: " + manifest.getPath());
        Logger.debug("    Robolectric package name: " + applicationId);
        return new AndroidManifest(manifest, res, assets, applicationId);
    }

    private String getType(Config config) {
        try {
            return ReflectionHelpers.getStaticField(config.constants(), "BUILD_TYPE");
        } catch (Throwable e) {
            return null;
        }
    }

    private String getFlavor(Config config) {
        try {
            return ReflectionHelpers.getStaticField(config.constants(), "FLAVOR");
        } catch (Throwable e) {
            return null;
        }
    }

    private String getApplicationId(Config config) {
        try {
            return ReflectionHelpers.getStaticField(config.constants(), "APPLICATION_ID");
        } catch (Throwable e) {
            return null;
        }
    }
}

Update

A similar change has been merged into the master branch of Robolectric 3, it's not incuded in the latest RC but will make the final 3.0 release.