我无法测试 Android 可组合 UI 出现“FINGERPRINT 不得为空”错误

问题描述 投票:0回答:1

我正在尝试写下我的可组合项的 UI 测试用例,即

package com.lbg.project.presentation.ui.view

import NavigationScreens
import android.annotation.SuppressLint
import android.widget.Toast
import androidx.compose.foundation.BorderStroke
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.wrapContentHeight
import androidx.compose.foundation.layout.wrapContentSize
import androidx.compose.foundation.lazy.staggeredgrid.LazyVerticalStaggeredGrid
import androidx.compose.foundation.lazy.staggeredgrid.StaggeredGridCells
import androidx.compose.foundation.lazy.staggeredgrid.items
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Home
import androidx.compose.material3.Card
import androidx.compose.material3.CardDefaults
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.NavigationBar
import androidx.compose.material3.NavigationBarItem
import androidx.compose.material3.Scaffold
import androidx.compose.material3.SnackbarDuration
import androidx.compose.material3.SnackbarHostState
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar
import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.res.colorResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.navigation.NavGraph.Companion.findStartDestination
import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable
import androidx.navigation.compose.rememberNavController
import coil.annotation.ExperimentalCoilApi
import com.lbg.project.R
import com.lbg.project.data.models.mappers.CatDataModel
import com.lbg.project.presentation.contracts.BaseContract
import com.lbg.project.presentation.contracts.CatContract
import com.lbg.project.presentation.ui.theme.ComposeSampleTheme
import com.lbg.project.utils.TestTags.PROGRESS_BAR
import com.skydoves.landscapist.CircularReveal
import com.skydoves.landscapist.ShimmerParams
import com.skydoves.landscapist.glide.GlideImage
import getBottomNavigationItems
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.onEach

@SuppressLint("UnusedMaterial3ScaffoldPaddingParameter")
@OptIn(ExperimentalMaterial3Api::class)
@ExperimentalCoilApi
@Composable
fun CatScreen(
    state: CatContract.State,
    effectFlow: Flow<BaseContract.Effect>?,
    onNavigationRequested: (itemUrl: String, imageId: String,isFavourite:Boolean) -> Unit
) {
    val snackBarHostState = remember { SnackbarHostState() }
    val context = LocalContext.current
    val catMessage = stringResource(R.string.cats_are_loaded)
    //initializing the default selected item
    var navigationSelectedItem by remember {
        mutableIntStateOf(0)
    }

    /**
     * by using the rememberNavController()
     * we can get the instance of the navController
     */
    val navController = rememberNavController()
    // Listen for side effects from the VM
    LaunchedEffect(effectFlow) {
        effectFlow?.onEach { effect ->
            if (effect is BaseContract.Effect.DataWasLoaded)
                snackBarHostState.showSnackbar(
                    message = catMessage,
                    duration = SnackbarDuration.Short
                )
        }?.collect { value ->
            if (value is BaseContract.Effect.Error) {
                // Handle other emitted values if needed
                Toast.makeText(context, value.errorMessage, Toast.LENGTH_LONG).show()
            }

        }
    }
    Scaffold(
        topBar = {
            CatAppBar()
        }, bottomBar = {
            NavigationBar {
                //getting the list of bottom navigation items for our data class
                getBottomNavigationItems(context).forEachIndexed { index, navigationItem ->

                    //iterating all items with their respective indexes
                    NavigationBarItem(
                        selected = index == navigationSelectedItem,
                        label = {
                            Text(navigationItem.title)
                        },
                        icon = {
                            Icon(
                                navigationItem.icon,
                                contentDescription = navigationItem.title
                            )
                        },
                        onClick = {
                            navigationSelectedItem = index
                            navController.navigate(navigationItem.screenRoute) {
                                popUpTo(navController.graph.findStartDestination().id) {
                                    saveState = true
                                }
                                launchSingleTop = true
                                restoreState = true
                            }
                        }
                    )
                }
            }
        }
    ) { paddingValues ->
        //We need to setup our NavHost in here
        NavHost(
            navController = navController,
            startDestination = NavigationScreens.Home.screenRoute,
            modifier = Modifier.padding(paddingValues = paddingValues)
        ) {
            composable(NavigationScreens.Home.screenRoute) {
                UserView(
                    state,
                    false,
                    onNavigationRequested = onNavigationRequested
                )
            }
            composable(NavigationScreens.MyFavorites.screenRoute) {
                UserView(
                    state,
                    true,
                    onNavigationRequested = onNavigationRequested
                )
            }
        }
    }
}

@Composable
fun UserView(
    state: CatContract.State,
    isFavCatsCall: Boolean,
    onNavigationRequested: (itemUrl: String, imageId: String,isFavourite:Boolean) -> Unit
) {
    Surface {
        Box {
            val cats = if (isFavCatsCall) state.favCatsList else state.cats
            CatsList(cats = cats) { itemUrl, imageId ->
                onNavigationRequested(itemUrl, imageId,isFavCatsCall)
            }
            if (state.isLoading)
                LoadingBar()
        }

    }
}


@OptIn(ExperimentalMaterial3Api::class)
@Composable
private fun CatAppBar() {
    TopAppBar(
        navigationIcon = {
            Icon(
                imageVector = Icons.Default.Home,
                modifier = Modifier.padding(horizontal = 12.dp),
                contentDescription = stringResource(R.string.action_icon)
            )
        },
        title = {
            Text(
                text = stringResource(R.string.app_name),
                color = colorResource(id = R.color.white)
            )
        },
        colors = TopAppBarDefaults.smallTopAppBarColors(
            containerColor = colorResource(R.color.colorPrimary),
            titleContentColor = Color(R.color.white),
            navigationIconContentColor = MaterialTheme.colorScheme.onPrimary,
            actionIconContentColor = MaterialTheme.colorScheme.onSecondary
        )
    )
}

@Composable
fun CatsList(
    cats: List<CatDataModel>,
    onItemClicked: (url: String, imageId: String) -> Unit = { _: String, _: String -> }
) {
    LazyVerticalStaggeredGrid(
        columns = StaggeredGridCells.Fixed(2),
        horizontalArrangement = Arrangement.spacedBy(2.dp),
        content = {
            this.items(cats) { item ->
                Card(
                    shape = RoundedCornerShape(8.dp),
                    colors = CardDefaults.cardColors(
                        containerColor = MaterialTheme.colorScheme.surface,
                    ),
                    elevation = CardDefaults.cardElevation(
                        defaultElevation = 6.dp
                    ),
                    border = BorderStroke(0.5.dp, Color.Gray),
                    modifier = Modifier
                        .fillMaxWidth()
                        .padding(start = 10.dp, end = 10.dp, top = 10.dp)
                        .clickable { onItemClicked(item.url, item.imageId) }
                ) {
                    ItemThumbnail(thumbnailUrl = item.url)
                }
            }
        }, modifier = Modifier.fillMaxSize()

    )
}

@Composable
fun ItemThumbnail(
    thumbnailUrl: String
) {
    GlideImage(
        imageModel = thumbnailUrl,
        modifier = Modifier
            .wrapContentSize()
            .wrapContentHeight()
            .fillMaxWidth(),
        // shows a progress indicator when loading an image.
        contentScale = ContentScale.Crop,
        circularReveal = CircularReveal(duration = 100),
        shimmerParams = ShimmerParams(
            baseColor = MaterialTheme.colorScheme.background,
            highlightColor = Color.Gray,
            durationMillis = 500,
            dropOff = 0.55f,
            tilt = 20f
        ), contentDescription = stringResource(R.string.cat_thumbnail_picture)
    )
}

@Composable
fun LoadingBar() {
    Box( modifier = Modifier.fillMaxSize(),
        contentAlignment = Alignment.Center

    ) {
        CircularProgressIndicator( modifier = Modifier.testTag(PROGRESS_BAR) )
    }
}


@OptIn(ExperimentalCoilApi::class)
@Preview(showBackground = true)
@Composable
fun DefaultPreview() {
    ComposeSampleTheme {
        CatScreen(CatContract.State(), null) { _: String, _: String,_:Boolean-> }
    }
}

我尝试像这样测试列表

package com.lbg.project.lbgTest.view

import androidx.compose.foundation.lazy.staggeredgrid.LazyVerticalStaggeredGrid
import androidx.compose.foundation.lazy.staggeredgrid.StaggeredGridCells
import androidx.compose.foundation.lazy.staggeredgrid.items
import androidx.compose.ui.test.junit4.createAndroidComposeRule
import androidx.compose.ui.test.onNodeWithContentDescription
import com.lbg.project.R
import com.lbg.project.presentation.contracts.CatContract
import com.lbg.project.presentation.features.cats.CatsActivity
import com.lbg.project.presentation.ui.view.CatScreen
import com.lbg.project.presentation.ui.view.ItemThumbnail
import org.junit.Before
import org.junit.Rule
import org.junit.Test

class CatsScreenTest {
    @get:Rule
    val composeTestRule = createAndroidComposeRule<CatsActivity>()
    @coil.annotation.ExperimentalCoilApi
    @Before
    fun setUp() {
        composeTestRule.setContent {
            CatScreen(
                state = CatContract.State(isLoading = true), // Set loading state
                effectFlow = null,
                onNavigationRequested = { _, _, _ -> /* Handle navigation in the test */ }
            )
        }
    }
    @Test
    fun lazyVerticalStaggeredGrid_imagesAreLoaded() {
        // Arrange
        val imageUrls = listOf("https://images.freeimages.com/images/large-previews/d4f/www-1242368.jpg",
            "https://images.freeimages.com/images/large-previews/636/holding-a-dot-com-iii-1411477.jpg",
            "https://cdn.pixabay.com/photo/2022/01/11/21/48/link-6931554_1280.png",
            "https://cdn.pixabay.com/photo/2020/09/19/19/37/landscape-5585247_1280.jpg"
        )

        // Act
        composeTestRule.setContent {
            LazyVerticalStaggeredGrid(columns = StaggeredGridCells.Fixed(2),content = {
                this.items(imageUrls) { imageUrl ->
                    ItemThumbnail(thumbnailUrl = imageUrl)
                }
            })
        }

        // Assert
        imageUrls.forEach { _ ->
            composeTestRule.onNodeWithContentDescription(composeTestRule.activity.getString(R.string.cat_thumbnail_picture)).assertExists()
        }
    }


}

但是,当我在这里运行测试用例时,我得到了

java.lang.NullPointerException: FINGERPRINT must not be null

    at androidx.compose.ui.test.AndroidComposeUiTestEnvironment.runTest(ComposeUiTest.android.kt:310)

    at androidx.compose.ui.test.junit4.AndroidComposeTestRule$apply$1.evaluate(AndroidComposeTestRule.android.kt:271)

    at org.junit.runners.ParentRunner$3.evaluate(ParentRunner.java:306)

    at org.junit.runners.BlockJUnit4ClassRunner$1.evaluate(BlockJUnit4ClassRunner.java:100)

    at org.junit.runners.ParentRunner.runLeaf(ParentRunner.java:366)

    at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:103)

    at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:63)

    at org.junit.runners.ParentRunner$4.run(ParentRunner.java:331)

    at org.junit.runners.ParentRunner$1.schedule(ParentRunner.java:79)

    at org.junit.runners.ParentRunner.runChildren(ParentRunner.java:329)

    at org.junit.runners.ParentRunner.access$100(ParentRunner.java:66)

    at org.junit.runners.ParentRunner$2.evaluate(ParentRunner.java:293)

    at org.junit.runners.ParentRunner$3.evaluate(ParentRunner.java:306)

    at org.junit.runners.ParentRunner.run(ParentRunner.java:413)

    at org.gradle.api.internal.tasks.testing.junit.JUnitTestClassExecutor.runTestClass(JUnitTestClassExecutor.java:108)

    at org.gradle.api.internal.tasks.testing.junit.JUnitTestClassExecutor.execute(JUnitTestClassExecutor.java:58)

    at org.gradle.api.internal.tasks.testing.junit.JUnitTestClassExecutor.execute(JUnitTestClassExecutor.java:40)

    at org.gradle.api.internal.tasks.testing.junit.AbstractJUnitTestClassProcessor.processTestClass(AbstractJUnitTestClassProcessor.java:60)

    at org.gradle.api.internal.tasks.testing.SuiteTestClassProcessor.processTestClass(SuiteTestClassProcessor.java:52)

    at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)

    at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:77)

    at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)

    at java.base/java.lang.reflect.Method.invoke(Method.java:568)

    at org.gradle.internal.dispatch.ReflectionDispatch.dispatch(ReflectionDispatch.java:36)

    at org.gradle.internal.dispatch.ReflectionDispatch.dispatch(ReflectionDispatch.java:24)

    at org.gradle.internal.dispatch.ContextClassLoaderDispatch.dispatch(ContextClassLoaderDispatch.java:33)

    at org.gradle.internal.dispatch.ProxyDispatchAdapter$DispatchingInvocationHandler.invoke(ProxyDispatchAdapter.java:94)

    at jdk.proxy1/jdk.proxy1.$Proxy2.processTestClass(Unknown Source)

    at org.gradle.api.internal.tasks.testing.worker.TestWorker$2.run(TestWorker.java:176)

    at org.gradle.api.internal.tasks.testing.worker.TestWorker.executeAndMaintainThreadName(TestWorker.java:129)

    at org.gradle.api.internal.tasks.testing.worker.TestWorker.execute(TestWorker.java:100)

    at org.gradle.api.internal.tasks.testing.worker.TestWorker.execute(TestWorker.java:60)

    at org.gradle.process.internal.worker.child.ActionExecutionWorker.execute(ActionExecutionWorker.java:56)

    at org.gradle.process.internal.worker.child.SystemApplicationClassLoaderWorker.call(SystemApplicationClassLoaderWorker.java:113)

    at org.gradle.process.internal.worker.child.SystemApplicationClassLoaderWorker.call(SystemApplicationClassLoaderWorker.java:65)

    at worker.org.gradle.process.internal.worker.GradleWorkerMain.run(GradleWorkerMain.java:69)

    at worker.org.gradle.process.internal.worker.GradleWorkerMain.main(GradleWorkerMain.java:74)

这是我的 UI 运行的错误日志 请帮助我确定如何为此可组合项编写 UI 测试用例,我们将非常感激并提前感谢您。 请帮助我了解出了什么问题或如何为此可组合文件编写正确的 UI 测试用例。 谢谢你

android unit-testing android-jetpack-compose gui-testing android-jetpack-compose-testing
1个回答
0
投票

我遇到了这些问题,并在我的测试类的顶部添加了

@RunWith(RobolectricTestRunner::class)
并且它起作用了。像这样:

@RunWith(RobolectricTestRunner::class)
public class SandwichTest {

}

您可以从此处的文档中找到更多信息:https://robolectric.org

© www.soinside.com 2019 - 2024. All rights reserved.