Robolectric 和 test.R.java

Robolectric with test.R.java

我在 API21 有一个使用 robolectric 3.0 的库项目,com.android.tools.build:gradle:1.3.1

我想在 robolectric 测试中使用测试资源(就像在 src/androidTest/res/... 下一样),即 com.mypackage.test.R.java(与用于生产的 com.mypackage.R.java 相对)。

我目前拥有的:

目录结构为

src/
  main/
    java
    res
  test/
    java
    // no res here because it's not picked up 
  androidTest/
    res    // picked up by androidTest build variant to generate test.R.java

然后在build.gradle:

android {
  compileSdkVersion 21
  buildToolsVersion = "22.0.1"

  defaultConfig {
    minSdkVersion 15
    targetSdkVersion 21
  }

  sourceSets {
    test {
        java {
            srcDir getTestRJavaDir()
        }
    }
  }
}

def private String getTestRJavaDir(){
  def myManifestRoot = (new XmlParser()).parse("${project.projectDir}/src/main/AndroidManifest.xml")
  def myPackageNamespace = myManifestRoot.@package
  def myPackagePath = myPackageNamespace.replaceAll("\.", "/")

  return "${project.buildDir}/generated/source/r/androidTest/debug/${myPackagePath}/test"
}

afterEvaluate { project ->
  [tasks.compileDebugUnitTestSources, tasks.compileReleaseUnitTestSources]*.dependsOn("compileDebugAndroidTestSources")
}

我的测试现在成功 编译 和 test.R.java。

但是,在运行时,它们会失败,因为 robolectric 现在无法找到我的资产文件,因为它们现在位于 ${project.buildDir}/intermediates/assets/androidTest/debug 而以前位于 ${project.buildDir}/intermediates/assets/debug。我怀疑 robo 也找不到资源文件,因为它们也已移动到 androidTest(构建变体?)目录下。

所以有两个问题:1) 有更好的方法吗? 2) 如果没有,我如何告诉 robolectric 去哪里寻找资产文件?

我试过 @Config(assetDir="build/intermediates/assets/androidTest/debug")@Config(assetDir="../build/intermediates/assets/androidTest/debug") 都没有用。

您可以创建自定义 robolectric 测试运行器,例如:

public class CustomRobolectricGradleTestRunner extends RobolectricGradleTestRunner {

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

    // Fix for the NotFound error with openRawResource()
    @Override
    protected AndroidManifest getAppManifest(Config config) {
        String manifest = "src/main/AndroidManifest.xml";
        String res = String.format("../app/build/intermediates/res/merged/%1$s/%2$s", BuildConfig.FLAVOR, BuildConfig.BUILD_TYPE);
        String asset = "src/test/assets";
        return new AndroidManifest(Fs.fileFromPath(manifest), Fs.fileFromPath(res), Fs.fileFromPath(asset));
    }
}

在可变资产中,您可以定义类似“../build/intermediates/assets/androidTest/debug”的内容

因此,结合@motou 的回答和 this post,我制定了一个解决方案,它只是 有点 坏了,而不是 完全坏了。

这是我的一组测试运行器。

第一个测试运行程序是必要的只是为了在我的 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;
      }
    }
  }


}

这个 class 中的操作是我用 "androidTest" 覆盖了味道以强制查看 androidTest 构建输出目录,这是我强制 compileDebugAndroidTestSources 作为先决条件任务,因此我可以在我的编译路径中包含 test.R.java

当然,这会破坏其他一些东西(特别是因为发布构建类型在 androidTest 输出目录中没有任何内容)所以我在此处的路径测试中添加了一些回退逻辑。如果buildvariant/type组合目录存在,则使用;否则退回到单独的类型。我还必须围绕资产添加第二层逻辑,因为它正在查找 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;
      }
    };
  }
}

当然你在问,"why not just always use the derived test runner?" 答案是 "because it breaks Android's default resource loading somehow." 我遇到的这个例子是默认的 TextView 字体返回 null 因为它找不到从主题初始化期间字体系列的默认值。

我感觉我 真的 接近完全有效的东西,在这里,但我必须深入研究为什么 Android 突然不能在其 textview 主题中找到 com.android.internal.R.whatever 的值,我现在真的没有空闲带宽。