samedi 1 février 2020

Loading of asset images in androidTest fails using Picasso/Glide

Short question

How can I sucessfully load an asset image in an androidTest with Picasso or Glide?

Long explaination

In an UI test I want to invoke my application to load images into an ImageView with certain operations applied to them (e.g. cropping, centering, etc.). To perform the image loading, cropping, etc., I use Picasso. In the UI test I want to verify that after triggering the loading via UI the bitmap attached to the ImageView is exactly what I intended using Picasso, i.e. I’m connecting Picasso and applying the Picasso DSL correctly.

To do that I spin up an androidTest, supplying the images to verify with as assets in src/androidTest/resources/assets. I inject file:///android_asset/$filename as image filename into my application to have Picasso use the AssetManager for loading the image.

Although the application works with regular images stored on the device / in the emulator, the test fails loading the asset images -- the AssetManager reports a FileNotFoundException:

     Caused by: java.io.FileNotFoundException: australia.jpg
        at android.content.res.AssetManager.nativeOpenAsset(Native Method)
        at android.content.res.AssetManager.open(AssetManager.java:744)
        at android.content.res.AssetManager.open(AssetManager.java:721)
        at com.squareup.picasso.AssetRequestHandler.load(AssetRequestHandler.java:45)
        at com.squareup.picasso.BitmapHunter.hunt(BitmapHunter.java:206)
        at com.squareup.picasso.RequestCreator.get(RequestCreator.java:396)

By the way, using Glide leads to the same FileNotFoundException.

I’ve set up a small instrumented test AssetTest.kt to analyze and showcase the problem. Please follow the link to view it completely on GitHub.

@RunWith(AndroidJUnit4::class)
@LargeTest
class AssetTest {

    private lateinit var activityScenario: ActivityScenario<TasksActivity>
    private val filename = "victoria-priessnitz-KBIui3I44SY-unsplash.jpg"
    // NOTE: the tests show the same behavior regardless of whether the activity is launched or not
    private val enableActivityUsage = true

    @Before
    fun launchActivity() {
        if (enableActivityUsage) {
            activityScenario = ActivityScenario.launch(TasksActivity::class.java)
        }
    }

    @After
    fun shutdownActivity() {
        if (enableActivityUsage) {
            activityScenario.close()
        }
    }

    @Test
    fun loadingAssetImageWithInstrumentationContextSucceeds() {
        val ctx = InstrumentationRegistry.getInstrumentation().context
        val inputStream = ctx.resources.assets.open(filename)
        val bytes = inputStream.readBytes()
        assertEquals(1903567, bytes.size)
    }

    @Test(expected = FileNotFoundException::class)
    fun loadingAssetImageWithInstrumentationTargetContextFails() {
        val targetCtx = InstrumentationRegistry.getInstrumentation().targetContext
        val inputStream = targetCtx.resources.assets.open(filename)
        inputStream.readBytes()
    }

    @Test(expected = FileNotFoundException::class)
    fun loadingAssetImageWithInstrumentationTargetContextsApplicationContextFails() {
        val targetCtx = InstrumentationRegistry.getInstrumentation().targetContext.applicationContext
        val inputStream = targetCtx.resources.assets.open(filename)
        inputStream.readBytes()
    }

    // NOTE: when not using the test orchestrator to separate tests, this tests may fail
    // because the execution shows the same behavior as with targetContext
    @Test(expected = NullPointerException::class)
    fun picassoFailsWithNullPointerExceptionWhenUsingContext() {
        val uri = Uri.parse("file:///android_asset/$filename")
        // synchronous image loading cannot be performed in main thread
        val context = InstrumentationRegistry.getInstrumentation().context
        try {
            Picasso.with(context)
                    .load(uri)
                    .get()
        } catch (e: Exception) {
            // sometimes Android Studio does not display callstacks in debugger
            println(e.message)
            throw e
        }
    }

    @Test(expected = FileNotFoundException::class)
    fun picassoFailsWithExceptionInAssetManagerOpenWhenUsingTargetContext() {
        val uri = Uri.parse("file:///android_asset/$filename")
        val context = InstrumentationRegistry.getInstrumentation().targetContext
        try {
            Picasso.with(context)
                    .load(uri)
                    .get()
        } catch (e: Exception) {
            // sometimes Android Studio does not display callstacks in debugger
            println(e.message)
            throw e
        }
    }
}

I observed the following:

  1. The test loadingAssetImageWithInstrumentationContextSucceeds can load the asset while and loadingAssetImageWithInstrumentationTargetContextFails cannot load the asset -- regardless of being run in an ActivityScenario or not. I conclude that the assets are stored in an APK accessible from context, but not in an APK accessible from targetContext. Unfortunately I know no way to look inside the referenced APKs since the Device File Explorer fails to open them in /data/app.

  2. The test picassoFailsWithNullPointerExceptionWhenUsingContext shows that using InstrumentationRegistry.getInstrumentation().context lets Picasso fail due to a null pointer exception. I could see in the Picasso builder internals that Picasso does not directly make use of the supplied context, but calls ContextImpl::getApplicationContext -- where mPackageInfo.getApplication is null:

    @Override
    public Context getApplicationContext() {
        return (mPackageInfo != null) ?
                mPackageInfo.getApplication() : mMainThread.getApplication();
    }
  1. The test picassoFailsWithExceptionInAssetManagerOpenWhenUsingTargetContext shows that using InstrumentationRegistry.getInstrumentation().targetContext will not cause a null pointer exception, but lead to a FileNotFoundException from the AssetManager as mentioned above. I conclude that the APK attached tot hat targetContext.applicationContext does not contain the assets.

  2. I’m rather confused about all the different Context existing on created when spinning up Activities.

But coming back to the question:

There certainly are other (more expensive) ways to test my scenario, but I think there should be a working way to just load an asset with Picasso in an instrumented test.

Aucun commentaire:

Enregistrer un commentaire