简体   繁体   中英

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. 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. 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).

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. 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.

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:

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.

Here's my mobile 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. Robolectric, as any other stub, may hide some actual issues.

Your problem is that you use InstrumentationRegistry.getContext() that is not the same that your app uses. According to docs:

Return the Context of this instrumentation's package.

And you should have used InstrumentationRegistry.getTargetContext() instead:

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. 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.

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

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

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