Robolectric с test.R.java

У меня есть проект библиотеки, использующий robolectric 3.0 в API21, с com.android.tools.build:gradle:1.3.1.

Я хочу использовать тестовые ресурсы (как бы под 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") безрезультатно.


comment
Джон, можете ли вы объяснить, почему вы хотите использовать другой файл R?   -  person Eugen Martynov    schedule 06.11.2015
comment
Мы в значительной степени модернизируем тесты для существующего кода. Иногда некоторые вещи, такие как наша собственная структура анимации, фактически потребляют ресурсы. Этот фреймворк не был создан с учетом возможности тестирования, и мы не хотим его модифицировать (пока), потому что тестов нет. Самый простой подход к тестированию этого унаследованного кода — предоставить ему урезанные ресурсы, предназначенные только для тестирования.   -  person Jon O    schedule 06.11.2015
comment
Я вижу, вам нужно создать собственный тестовый бегун, но я здесь не большой экспортер.   -  person Eugen Martynov    schedule 06.11.2015
comment
Если я правильно понимаю, то вашей библиотеке нужны определенные ресурсы для работы, но они предоставляются только проектом, использующим вашу библиотеку? Тогда можно было бы применить более простой подход: использовать типы сборки или разновидности с необходимыми тестовыми ресурсами. Другой подход — создать модуль приложения, который использует вашу библиотеку, а затем написать там тест с необходимыми ресурсами. Я не думаю, что вы получите стабильную настройку, используя ресурсы androidTest.   -  person nenick    schedule 08.11.2015
comment
Это специально для тестирования. Нам не нужны ресурсы в производственной сборке. Вы предлагаете нам сделать версию тестовой сборки? Чем это отличается от использования androidTest для инструментального тестирования вместо robolectric для модульного тестирования? (Это не инструментальный тест.)   -  person Jon O    schedule 09.11.2015


Ответы (2)


Вы можете создать собственное средство запуска роболетрических тестов, например:

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"

person motou    schedule 06.11.2015
comment
Это почти сработало. Вы подали мне идею, поэтому я отдаю вам должное, но я также опубликую свой собственный ответ. - person Jon O; 09.11.2015

Итак, с некоторой комбинацией ответа @motou и этого сообщения я разработал решение, которое просто немного сломано, а не полностью сломано.

Вот мой набор тестовых бегунов.

Этот первый запуск тестов был необходим только для того, чтобы исправить мои ранее работающие тесты после того, как я испортил сборку gradle.

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 outputs), поэтому я добавил здесь некоторую резервную логику в тестировании пути. Если каталог комбинации варианта/типа сборки существует, используйте его; иначе вернуться к типу в одиночку. Мне также пришлось добавить второй уровень логики вокруг ресурсов, потому что он находил каталог build/intermediates/assets и пытался его использовать, в то время как тот, который работал, находился в папке пакетов.

Что на самом деле позволило мне использовать тестовые ресурсы, так это другой производный бегун:

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 внезапно не может найти значение на com.android.internal.R. что угодно в его текстовой тематике, и у меня сейчас нет свободной пропускной способности для этого.

person Jon O    schedule 09.11.2015