So with some combination of @motou's answer and this post, I worked out a solution that's merely sort of broken, and not completely broken.
Here's my set of test runners.
This first test runner was necessary just to fix my previously-working tests after mucking with my gradle build.
import com.google.common.base.Joiner;
import org.junit.runners.model.InitializationError;
import org.robolectric.RobolectricGradleTestRunner;
import org.robolectric.annotation.Config;
import org.robolectric.manifest.AndroidManifest;
import org.robolectric.res.FileFsFile;
import org.robolectric.res.FsFile;
import org.robolectric.util.Logger;
import org.robolectric.util.ReflectionHelpers;
import java.io.File;
/**
* Extension of RobolectricGradleTestRunner to provide more robust path support since
* we're kind of hacking androidTest resources into our build which moves stuff around.
*
* Most stuff is copy/pasted from the superclass and modified to suit our needs (with
* an internal class to capture file info and a builder for clarity's sake).
*/
public class AssetLoadingRobolectricGradleTestRunner extends RobolectricGradleTestRunner {
public AssetLoadingRobolectricGradleTestRunner( Class<?> klass ) throws InitializationError {
super( klass );
}
private static final String BUILD_OUTPUT = "build/intermediates";
@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 packageName = getPackageName( config );
final FileFsFile res;
final FileFsFile assets;
final FileFsFile manifest;
FileInfo.Builder builder = new FileInfo.Builder()
.withFlavor( flavor )
.withType( type );
// res/merged added in Android Gradle plugin 1.3-beta1
if ( FileFsFile.from( BUILD_OUTPUT, "res", "merged" ).exists() ) {
res = FileFsFile.from(
getPathWithFlavorAndType(
builder.withPrefix( BUILD_OUTPUT, "res", "merged" )
.build() ) );
} else if ( FileFsFile.from( BUILD_OUTPUT, "res" ).exists() ) {
res = FileFsFile.from(
getPathWithFlavorAndType(
builder.withPrefix( BUILD_OUTPUT, "res" )
.build() ) );
} else {
res = FileFsFile.from(
getPathWithFlavorAndType(
builder.withPrefix( BUILD_OUTPUT, "bundles" )
.build() ),
"res" );
}
FileFsFile tmpAssets = null;
if ( FileFsFile.from( BUILD_OUTPUT, "assets" ).exists() ) {
tmpAssets = FileFsFile.from(
getPathWithFlavorAndType(
builder.withPrefix( BUILD_OUTPUT, "assets" )
.build() ) );
}
if ( tmpAssets == null || !tmpAssets.exists() ) {
assets = FileFsFile.from(
getPathWithFlavorAndType(
builder.withPrefix( BUILD_OUTPUT, "bundles" )
.build() ),
"assets" );
} else {
assets = tmpAssets;
}
if ( FileFsFile.from( BUILD_OUTPUT, "manifests" ).exists() ) {
manifest = FileFsFile.from(
getPathWithFlavorAndType(
builder.withPrefix( BUILD_OUTPUT, "manifests", "full" )
.build() ),
"AndroidManifest.xml" );
} else {
manifest = FileFsFile.from(
getPathWithFlavorAndType(
builder.withPrefix( BUILD_OUTPUT, "bundles" )
.build() ),
"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: " + packageName );
return getAndroidManifest( manifest, res, assets, packageName );
}
protected String getType( Config config ) {
try {
return ReflectionHelpers.getStaticField( config.constants(), "BUILD_TYPE" );
} catch ( Throwable e ) {
return null;
}
}
private String getPathWithFlavorAndType( FileInfo info ) {
FileFsFile typeDir = FileFsFile.from( info.prefix, info.flavor, info.type );
if ( typeDir.exists() ) {
return typeDir.getPath();
} else {
// Try to find it without the flavor in the path
return Joiner.on( File.separator ).join( info.prefix, info.type );
}
}
protected String getFlavor( Config config ) {
// TODO HACK! Enormous, terrible hack! Odds are this will barf our testing
// if we ever want multiple flavors.
return "androidTest";
}
protected String getPackageName( Config config ) {
try {
final String packageName = config.packageName();
if ( packageName != null && !packageName.isEmpty() ) {
return packageName;
} else {
return ReflectionHelpers.getStaticField( config.constants(), "APPLICATION_ID" );
}
} catch ( Throwable e ) {
return null;
}
}
// We want to be able to override this to load test resources in a child test runner
protected AndroidManifest getAndroidManifest( FsFile manifest, FsFile res, FsFile asset, String packageName ) {
return new AndroidManifest( manifest, res, asset, packageName );
}
public static class FileInfo {
public String prefix;
public String flavor;
public String type;
public static class Builder {
private String prefix;
private String flavor;
private String type;
public Builder withPrefix( String... strings ) {
prefix = Joiner.on( File.separator ).join( strings );
return this;
}
public Builder withFlavor( String flavor ) {
this.flavor = flavor;
return this;
}
public Builder withType( String type ) {
this.type = type;
return this;
}
public FileInfo build() {
FileInfo info = new FileInfo();
info.prefix = prefix;
info.flavor = flavor;
info.type = type;
return info;
}
}
}
}
The operative thing in this class is that I overrode the flavor with "androidTest" to force looking inside the androidTest build outputs directory, which is where everything gets put the moment that I force compileDebugAndroidTestSources
as a prerequisite task so I can include test.R.java
on my compile path.
Of course, this then breaks a bunch of other stuff (especially because the release buildtype doesn't have anything inside the androidTest
outputs directory) so I added some fallback logic in the path testing here. If the build variant/type combination directory exists, use it; else fall back to the type alone. I also had to add a second level of logic around the assets because it was finding a build/intermediates/assets directory and attempting to use that, while the one that would work was in the bundles folder.
The thing that actually enabled me to use test resources was this other, derived runner:
import com.mypackage.BuildConfig;
import org.junit.runners.model.InitializationError;
import org.robolectric.RobolectricGradleTestRunner;
import org.robolectric.annotation.Config;
import org.robolectric.manifest.AndroidManifest;
import org.robolectric.res.Fs;
import org.robolectric.res.FsFile;
import org.robolectric.res.ResourcePath;
import java.util.List;
public class TestResLoadingRobolectricGradleTestRunner extends AssetLoadingRobolectricGradleTestRunner {
public TestResLoadingRobolectricGradleTestRunner( Class<?> klass ) throws InitializationError {
super(klass);
}
@Override
protected AndroidManifest getAndroidManifest( FsFile manifest, FsFile res, FsFile asset, String packageName ) {
return new AndroidManifest( manifest, res, asset, packageName ) {
public String getTestRClassName() throws Exception {
getRClassName(); // forces manifest parsing to be called
// discard the value
return getPackageName() + ".test.R";
}
public Class getTestRClass() {
try {
String rClassName = getTestRClassName();
return Class.forName(rClassName);
} catch (Exception e) {
return null;
}
}
@Override
public List<ResourcePath> getIncludedResourcePaths() {
List<ResourcePath> paths = super.getIncludedResourcePaths();
paths.add(new ResourcePath(getTestRClass(), getPackageName(), Fs.fileFromPath("src/androidTest/res"), getAssetsDirectory()));
return paths;
}
};
}
}
Of course you're asking, "why not just always use the derived test runner?" and the answer is "because it breaks Android's default resource loading somehow." The example of this that I came across was that the default TextView typeface comes back null because it can't find the default value for font family during initialization from the theme.
I sense that I'm really close to something that works completely, here, but I'd have to dig more into why Android suddenly can't find a value at com.android.internal.R.whatever in its textview theming and I don't really have the spare bandwidth for that right now.