简体   繁体   English

Android仪表单元测试中的上下文和资源

[英]Context and Resource in Android Instrumented Unit Tests

I'm designing a system with some not-so-simple classes that require a Context object in order to initialize them. 我正在设计一个系统,其中包含一些不太简单的类,需要Context对象才能初始化它们。 These classes make use of third party classes which also require context initialization. 这些类使用第三方类,这些类也需要上下文初始化。 This class also utilizes the context to load a number of string resources necessary to the functionality. 此类还利用上下文来加载功能所需的许多字符串资源。

The problem comes with writing Instrumented Unit tests for these classes. 问题在于为这些类编写Instrumented Unit测试。 When I attempt to get a Context object for the test using InstrumentationRegistry.getContext() , I run into an exception where the context cannot find the string resources associated with the class (android.content.res.Resources$NotFoundException). 当我尝试使用InstrumentationRegistry.getContext()为测试获取Context对象时,我遇到了一个异常,其中上下文找不到与该类关联的字符串资源(android.content.res.Resources $ NotFoundException)。

My question is this: How can I design these tests so that the context can retrieve the string resources that I need, and also act as suitable context objects for the third party classes? 我的问题是:我如何设计这些测试,以便上下文可以检索我需要的字符串资源,还可以作为第三方类的合适上下文对象? There's only so much mocking I can do as some of these classes handle auth tokens, which would be difficult to mock. 我只能做很多嘲弄,因为其中一些类处理auth令牌,这很难模仿。 I can't be the only person who's run into this issue in the Android domain, so I'm sure there's a common solution for this presumably common problem. 我不可能是唯一一个在Android域中遇到此问题的人,所以我确信这个可能是常见问题的解决方案。

EDIT: As suggested, I've tried integrating Robolectric (version 3.3.2) in my Project, however when I try and run my unit tests I'm met with the following error: 编辑:正如所建议的,我已经尝试在我的项目中集成Robolectric(版本3.3.2),但是当我尝试运行我的单元测试时,我遇到了以下错误:

Error:Error converting bytecode to dex:
Cause: Dex cannot parse version 52 byte code.
This is caused by library dependencies that have been compiled using Java 8 or above.
If you are using the 'java' gradle plugin in a library submodule add 
targetCompatibility = '1.7'
sourceCompatibility = '1.7'
to that submodule's build.gradle file. 

I've tried adding the targetCompatibility and sourceCompatibility lines to my gradle files (in several locations) to no avail. 我尝试将targetCompatibility和sourceCompatibility行添加到我的gradle文件(在几个位置)无济于事。

Here's my mobile build.gradle: 这是我的移动build.gradle:

apply plugin: 'com.android.application'
apply plugin: 'checkstyle'
apply plugin: 'io.fabric'

project.ext {
    supportLibVersion = '25.3.0'
    multiDexSupportVersion = '1.0.1'
    gsonVersion = '2.8.0'
    retrofitVersion = '2.2.0'
    daggerVersion = '2.4'
    butterKnifeVersion = '8.5.1'
    eventBusVersion = '3.0.0'
    awsCoreServicesVersion = '2.2.+'
    twitterKitVersion = '2.3.2@aar'
    facebookVersion = '4.+'
    crashlyticsVersion = '2.6.7@aar'
    autoValueVersion = '1.2'
    autoValueParcelVersion = '0.2.5'
    autoValueGsonVersion = '0.4.4'
    permissionDispatcher = '2.2.0'
    testRunnerVersion = '0.5'
    espressoVersion = '2.2.2'
    junitVersion = '4.12'
    roboelectricVersion = '3.3.2'
}

def gitSha = exec('git rev-parse --short HEAD', "unknown");
def gitCommitCount = 100 + Integer.parseInt(exec('git rev-list --count HEAD', "-1"))
def gitTag = exec('git describe --tags', stringify(gitCommitCount))
def gitTimestamp = exec('git log -n 1 --format=%at', -1)

def appId = "com.example.myapp"
def isCi = "true".equals(System.getenv("CI"))

// Uncomment if you wish to enable Jack & Java8
// apply from: 'jack.gradle'

// Uncomment if you wish to enable Sonar
//apply from: 'sonar.gradle'

android {
  compileSdkVersion 25
  buildToolsVersion "25.0.2"

  defaultConfig {
    applicationId appId
    minSdkVersion 16
    targetSdkVersion 25

      multiDexEnabled = true

    versionCode gitCommitCount
    versionName gitTag

    testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"

    buildConfigField 'String', 'GIT_SHA', "\"${gitSha}\""
    buildConfigField 'long', 'GIT_TIMESTAMP', "${gitTimestamp}L"
  }

  buildTypes {
    debug {
      applicationIdSuffix '.debug'
    }
    release {
      minifyEnabled true
      proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
    }
    qa.initWith(buildTypes.release)
    qa {
      applicationIdSuffix '.qa'
      debuggable true
    }
  }

  lintOptions {
    abortOnError false
  }

  applicationVariants.all { variant ->
    def strictMode = !variant.name.equals("release")
    buildConfigField 'boolean', 'STRICT_MODE_ENABLED', "${strictMode}"
  }
}

configurations.all {
  resolutionStrategy {
    force "com.android.support:support-annotations:$supportLibVersion"
    force "com.squareup.okhttp3:okhttp:3.4.1"
    force "com.squareup:okio:1.9.0"
    force "com.google.guava:guava:19.0"
  }
}

dependencies {
    compile "com.android.support:appcompat-v7:$supportLibVersion"
    compile "com.android.support:design:$supportLibVersion"
    compile "com.android.support:recyclerview-v7:$supportLibVersion"
    compile "com.android.support:cardview-v7:$supportLibVersion"
    compile "com.android.support:multidex:$multiDexSupportVersion"

    compile "com.squareup.retrofit2:retrofit:$retrofitVersion"
    compile "com.squareup.retrofit2:converter-gson:$retrofitVersion"
    compile 'com.jakewharton.retrofit:retrofit2-rxjava2-adapter:1.0.0'

    compile "com.google.dagger:dagger:$daggerVersion"
    annotationProcessor "com.google.dagger:dagger-compiler:$daggerVersion"
    provided 'javax.annotation:jsr250-api:1.0'

    compile 'com.github.bumptech.glide:glide:3.7.0'
    compile 'com.jakewharton.timber:timber:4.3.1'
    compile "com.jakewharton:butterknife:$butterKnifeVersion"
    annotationProcessor "com.jakewharton:butterknife-compiler:$butterKnifeVersion"

    compile "org.greenrobot:eventbus:$eventBusVersion"
    annotationProcessor "org.greenrobot:eventbus:$eventBusVersion"

    compile 'io.reactivex.rxjava2:rxandroid:2.0.0'
    debugCompile 'com.squareup.okhttp3:logging-interceptor:3.4.2'

    compile "com.google.auto.value:auto-value:$autoValueVersion"
    annotationProcessor "com.google.auto.value:auto-value:$autoValueVersion"

    compile "com.ryanharter.auto.value:auto-value-parcel-adapter:$autoValueParcelVersion"
    annotationProcessor "com.ryanharter.auto.value:auto-value-parcel:$autoValueParcelVersion"

    compile "com.github.hotchemi:permissionsdispatcher:$permissionDispatcher"
    annotationProcessor "com.github.hotchemi:permissionsdispatcher-processor:$permissionDispatcher"

    compile("com.crashlytics.sdk.android:crashlytics:$crashlyticsVersion") {
        transitive = true;
    }

    compile("com.twitter.sdk.android:twitter:$twitterKitVersion") {
        transitive = true
    }

    compile "com.facebook.android:facebook-android-sdk:$facebookVersion"

    compile "com.amazonaws:aws-android-sdk-core:$awsCoreServicesVersion"
    annotationProcessor "com.amazonaws:aws-android-sdk-core:$awsCoreServicesVersion"
    compile "com.amazonaws:aws-android-sdk-apigateway-core:$awsCoreServicesVersion"
    annotationProcessor "com.amazonaws:aws-android-sdk-apigateway-core:$awsCoreServicesVersion"
    compile "com.amazonaws:aws-android-sdk-cognito:$awsCoreServicesVersion"
    annotationProcessor "com.amazonaws:aws-android-sdk-cognito:$awsCoreServicesVersion"
    compile "com.amazonaws:aws-android-sdk-cognitoidentityprovider:$awsCoreServicesVersion"
    annotationProcessor "com.amazonaws:aws-android-sdk-cognitoidentityprovider:$awsCoreServicesVersion"
    compile "com.amazonaws:aws-android-sdk-lambda:$awsCoreServicesVersion"
    annotationProcessor "com.amazonaws:aws-android-sdk-lambda:$awsCoreServicesVersion"
    compile "com.amazonaws:aws-android-sdk-sns:$awsCoreServicesVersion"
    annotationProcessor "com.amazonaws:aws-android-sdk-sns:$awsCoreServicesVersion"

    androidTestCompile "junit:junit:$junitVersion"
    androidTestCompile "com.android.support.test:runner:$testRunnerVersion"
    androidTestCompile "com.android.support.test:rules:$testRunnerVersion"
    androidTestCompile "com.android.support.test.espresso:espresso-intents:$espressoVersion"
    androidTestCompile "com.android.support.test.espresso:espresso-core:$espressoVersion"
    androidTestCompile "com.squareup.retrofit2:retrofit-mock:$retrofitVersion"
    androidTestCompile "org.robolectric:robolectric:$roboelectricVersion"

    testCompile "junit:junit:$junitVersion"
    testCompile 'com.google.truth:truth:0.30'
    testCompile 'org.hamcrest:hamcrest-all:1.3'
    testCompile "org.robolectric:robolectric:$roboelectricVersion"
}

task checkCodingStyle(type: Checkstyle) {
  description 'Runs Checkstyle inspection against Android sourcesets.'
  group = 'Code Quality'
  ignoreFailures = false
  showViolations = false
  source 'src'
  include '**/*.java'
  exclude '**/gen/**'
  exclude '**/R.java'
  exclude '**/BuildConfig.java'
  reports {
    xml.destination "$project.buildDir/reports/checkstyle/report.xml"
  }
  classpath = files()
  configFile = file("${rootProject.rootDir}/config/checkstyle/checkstyle.xml")
}

def stringify(int versionCode) {
  def builder = new StringBuilder();
  def dot = ""
  String.format("%03d", versionCode).toCharArray().each {
    builder.append(dot)
    builder.append(it)
    dot = "."
  }
  return builder.toString()
}

def exec(String command, Object fallback = null) {
  def cmd = command.execute([], project.rootDir)
  cmd.waitFor()
  if (cmd.exitValue() != 0) {
    if (fallback == null) {
      throw new RuntimeException("'$command' failed: $cmd.errorStream.text")
    } else {
      return fallback
    }
  }
  return cmd.text.trim()
}

if (isCi) {
  build.finalizedBy(checkCodingStyle)
}

The accepted answer is not the actual solution. 接受的答案不是实际的解决方案。 There are many cases when you want to test your interaction with a real Android framework. 在许多情况下,您希望测试与真实Android框架的交互。 Robolectric, as any other stub, may hide some actual issues. 与任何其他存根一样,Robolectric可能会隐藏一些实际问题。

Your problem is that you use InstrumentationRegistry.getContext() that is not the same that your app uses. 您的问题是您使用的InstrumentationRegistry.getContext()与您的应用程序使用的不同。 According to docs: 根据文件:

Return the Context of this instrumentation's package. 返回此检测包的Context。

And you should have used InstrumentationRegistry.getTargetContext() instead: 你应该使用InstrumentationRegistry.getTargetContext()代替:

Return a Context for the target application being instrumented. 返回正在检测的目标应用程序的上下文。

Because it, in the contrary to the first, will have access to your resourses. 因为它与第一个相反,可以访问您的资源。

Robolectric may be the solution to your problem. Robolectric可能是解决您问题的方法。 It allows you to write unit tests in the test folder (note: not instrumented unit tests in the androidTest folder) that utilise a Context with access to all of your R.string.* resources. 它允许您在test文件夹中编写单元测试(注意: androidTest文件夹中没有检测的单元测试),这些单元测试利用Context访问所有R.string.*资源。

Here is an example: 这是一个例子:

@RunWith(RobolectricTestRunner.class)
public class FooTest {

    private final Contet context;

    @Before
    public void setup() {
        context = RuntimeEnvironment.application;
    }

    public void testMyStringResource() {
        String mySpecialString = context.getString(R.string.mySpecialString);
        assertEquals("special", mySpecialString);
    }
}

Update: 更新:

The error message you were getting from Robolectric was caused by putting the dependency as androidTestCompile instead of testCompile 您从Robolectric获得的错误消息是由将依赖项作为androidTestCompile而不是testCompile

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM