4

我在 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成功编译。

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

所以有两个问题:1)有没有更好的方法来做到这一点?2)如果没有,我如何告诉 robolectric 在哪里寻找资产文件?

我已经尝试过@Config(assetDir="build/intermediates/assets/androidTest/debug")@Config(assetDir="../build/intermediates/assets/androidTest/debug")但无济于事。

4

2 回答 2

4

您可以创建一个自定义的机器人测试运行器,例如:

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”的内容

于 2015-11-06T13:57:35.007 回答
2

因此,结合@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 的值,我现在真的没有多余的带宽。

于 2015-11-09T19:04:52.693 回答