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.