因此,结合@motou 的回答和这篇文章,我想出了一个解决方案,它只是有点坏了,而不是完全坏了。
这是我的一组测试运行器。
这个第一个测试运行器是必要的,只是为了在我的 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;
}
}
}
}
这个类中的操作是我用“androidTest”覆盖了风格,以强制查看 androidTest 构建输出目录,这是我强制compileDebugAndroidTestSources
作为先决任务的那一刻的所有内容,因此我可以包含test.R.java
在我的编译路径中。
当然,这会破坏一堆其他的东西(特别是因为发布构建类型在输出目录中没有任何内容androidTest
)所以我在此处的路径测试中添加了一些后备逻辑。如果构建变体/类型组合目录存在,则使用它;否则回退到单独的类型。我还必须在资产周围添加第二层逻辑,因为它正在查找 build/intermediates/assets 目录并尝试使用该目录,而可以使用的目录位于 bundles 文件夹中。
实际上使我能够使用测试资源的是另一个派生的运行器:
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;
}
};
}
}
当然你会问,“为什么不总是使用派生的测试运行器?” 答案是“因为它以某种方式破坏了 Android 的默认资源加载。” 我遇到的示例是默认的 TextView 字体返回 null,因为它在主题初始化期间找不到字体系列的默认值。
我觉得我真的很接近完全有效的东西,在这里,但我必须更深入地研究为什么 Android 突然在它的 textview 主题中找不到 com.android.internal.R.whatever 的值,我现在真的没有多余的带宽。