How to unit test if an image was loaded using Coil + Compose

1.8k Views Asked by At

I'm loading an image using Coil for Compose like below.

@Composable
fun SvgImageSample() {
    val painter = rememberAsyncImagePainter(
        model = ImageRequest.Builder(LocalContext.current)
            .decoderFactory(SvgDecoder.Factory())
            .data("https://someserver.com/SVG_image.svg")
            .size(Size.ORIGINAL)
            .build()
    )
    Image(
        painter = painter,
        modifier = Modifier.size(100.dp).testTag("myImg"),
        contentDescription = null
    )
}

The image is loaded properly. Now, I would like to write a test to check if the image was loaded. Is there any assertion out-of-the-box for that?

Something like this:

class MyTest {
    @get:Rule
    val composeTestRule = createComposeRule()

    @Test
    fun checkIfTheImageLoads() {
        composeTestRule.setContent {
            MyAppThemeTheme {
                SvgImageSample()
            }
        }
        composeTestRule.onNodeWithTag("myImg")
            .assertCoilImageIsLoaded() // <- this is what I want
    }
}
3

There are 3 best solutions below

4
On BEST ANSWER

Another way would be to implement an EventListener and check the right events are emitted. Will save you using testTags and semantic properties in the app code.

https://coil-kt.github.io/coil/api/coil-base/coil-base/coil/-event-listener/index.html

A quick hacky attempt, but you could wrap this in a Composable helper that does this for any block passed in.

@Test
fun imageLoader() {
    var success = 0
    var errors = 0

    composeTestRule.setContent {
        Coil.setImageLoader(
            ImageLoader.Builder(context)
                .eventListener(object : EventListener {
                    override fun onSuccess(
                        request: ImageRequest, 
                        result: SuccessResult
                    ) {
                        success++
                    }

                    override fun onError(
                        request: ImageRequest, 
                        result: ErrorResult
                    ) {
                        errors++
                    }
                })
                .build()
        )
        MyAppThemeTheme {
            SvgImageSample()
        }
   }
   Thread.sleep(500)
   assertThat(errors).isEqualTo(0)
   assertThat(success).isEqualTo(1)
}
0
On

I found what I was looking for... Please let me know if anyone has a better solution.

This is what I did:

  1. Add this dependency in your build.gradle.
implementation "androidx.test.espresso.idling:idling-concurrent:3.5.0-alpha07"

This is necessary to use the IdlingThreadPoolExecutor class.

  1. Declare the an IdlingThreadPool object like below:
object IdlingThreadPool: IdlingThreadPoolExecutor(
    "coroutinesDispatchersThreadPool",
    Runtime.getRuntime().availableProcessors(),
    Runtime.getRuntime().availableProcessors(),
    0L,
    TimeUnit.MILLISECONDS,
    LinkedBlockingQueue(),
    Executors.defaultThreadFactory()
)

I get this hint from this issue in the Coil github page.

  1. Use the object declared above in the ImageRequest object.
@Composable
fun SvgImageSample() {
    val painter = rememberAsyncImagePainter(
        model = ImageRequest.Builder(LocalContext.current)
            .dispatcher(IdlingThreadPool.asCoroutineDispatcher()) // << here
            .decoderFactory(SvgDecoder.Factory())
            .data("https://someserver.com/SVG_image.svg")
            .size(Size.ORIGINAL)
            .build()
    )
    Image(
        painter = painter,
        modifier = Modifier
            .size(100.dp)
            .semantics {
                testTag = "myImg"
                coilAsyncPainter = painter
            },
        contentDescription = null
    )
}

Notice the IdlingThreadPool object was used in the dispatcher function. The other detail is coilAsyncPainter property which is receiving the painter object. It will be necessary during the test to check if the image was loaded.

  1. Declare the coilAsyncPainter semantic property.
val CoilAsyncPainter = SemanticsPropertyKey<AsyncImagePainter>("CoilAsyncPainter")
var SemanticsPropertyReceiver.coilAsyncPainter by CoilAsyncPainter

This is what you need to do in the application code.

  1. In the test code, declare a new SemanticNodeInteration.
fun SemanticsNodeInteraction.isAsyncPainterComplete(): SemanticsNodeInteraction {
    assert(
        SemanticsMatcher("Async Image is Success") { semanticsNode ->
            val painter = semanticsNode.config.getOrElseNullable(CoilAsyncPainter) { null }
            painter?.state is AsyncImagePainter.State.Success
        }
    )
    return this;
}

So here, basically the painter object is obtained from the semantic property and then is checked if the current state is Success.

  1. Finally, here it is the test.
class MyTest {
    @get:Rule
    val composeTestRule = createComposeRule()

    @Test
    fun async_image_was_displayed() {
        composeTestRule.setContent {
            MyAppThemeTheme {
                SvgImageSample()
            }
        }
        composeTestRule.waitForIdle()
        composeTestRule.onNodeWithTag("myImg")
            .isAsyncPainterComplete()
    }
}
0
On

First, I have to say that the approach suggested by @nglauber worked. However, I cringed at that level of complexity for a simple test, so I tried a straight forward test and that works as well and I will keep so.

First, I loaded the image simply with AsyncImage

 AsyncImage(
            model = ImageRequest.Builder(LocalContext.current)
                .data(template.previewUrl)
                .crossfade(true)
                .build(),
            placeholder = painterResource(template.thumbNailResId),
            contentDescription = stringResource(R.string.template_description),
            contentScale = ContentScale.Fit,
        )

Then in the test, I simply checked for the node with content description is displayed like so

@Test
fun intialImageDisplayedTest() {
    val template = TemplateRepository.getTemplate()[0]
    composeTestRule.setContent {
        val selectedIndex = remember{ mutableStateOf(-1) }
        TemplateItem(
            selectedId = selectedIndex,
            template = template,
            onPreviewButtonClicked = {}
        )
    }
    composeTestRule.onNodeWithTag("template_${template.templateId}").assertIsDisplayed()
    composeTestRule.onNodeWithContentDescription(getImageDescriptionText()).assertIsDisplayed()
}

private fun getImageDescriptionText(): String {
    return composeTestRule.activity.resources.getString(R.string.template_description)
}

Again keeping it simple. I also added a matcher with a test tag. No Idling resource needed.