You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

多模块Android应用测试:E2E与集成测试的定位及模块归属疑问

Android测试分层:场景、模块组织与登录流程实践

核心疑问梳理

  1. E2E测试、集成测试的适用边界
  2. 不同测试类型在data/domain/presentation模块的放置规则
  3. 登录流程的测试分层策略:是否需要同时做UI的E2E、VM的集成测试,还是仅用E2E覆盖?

测试类型的适用场景

单元测试

  • 聚焦单一独立组件的逻辑验证,比如密码校验规则、单个用例的业务逻辑
  • 特点:执行速度快,依赖全部Mock,能精准定位代码逻辑问题
  • 示例:你写的ValidatePasswordUsecase测试,就是典型的单元测试,只测密码校验的独立逻辑

集成测试

  • 验证多个组件之间的协作交互,核心是看数据流转、依赖调用是否符合预期
  • 适用场景:
    • Domain层:用例与Repository接口的交互(比如你写的LoginUsecase与Mock AuthRepository的测试)
    • Data层:Repository与真实数据源(Room数据库、Retrofit接口)的协作
    • Presentation层:ViewModel与用例的交互(验证VM调用用例后状态更新是否正确)
  • 特点:部分依赖可以用真实实现,部分Mock,重点验证组件间的协作链路

E2E测试(含UI测试)

  • 模拟真实用户操作路径,验证从前端UI到后端服务的完整业务流程
  • 适用场景:核心业务流程的端到端验证,比如登录、支付、下单等
  • 示例:你写的登录UI测试,模拟输入账号密码、点击按钮、验证页面跳转,就是典型的E2E测试
  • 特点:依赖真实环境或接近真实的测试环境,执行速度较慢,但能保障核心流程的可用性

测试在模块中的组织规则

  • domain模块:
    • 放置单元测试:测用例、实体、业务规则的独立逻辑
    • 放置集成测试:测用例与Repository接口的交互(用Mock Repository,比如你的LoginUsecase集成测试)
  • data模块:
    • 放置集成测试:测Repository与真实数据源(Room、Retrofit)的协作,验证数据读写、网络请求的正确性
  • presentation模块:
    • 放置UI测试/E2E测试:测View与ViewModel的交互、页面跳转、用户操作的反馈,比如你的登录UI测试

登录流程的测试分层策略

不能仅靠E2E测试覆盖,分层测试才能兼顾效率与可靠性:

  1. 单元测试层:
    • 测密码校验、账号格式校验等独立用例逻辑(比如你的ValidatePasswordUsecase测试)
    • 测登录用例的核心逻辑(比如参数传递、错误处理)
  2. 集成测试层:
    • 测LoginUsecase与AuthRepository的交互(你的示例已经覆盖,验证用例正确调用Repo)
    • 测ViewModel与LoginUsecase的交互:验证VM调用用例后,状态(如加载中、成功、错误)是否正确更新
    • 测AuthRepository与真实登录API/数据库的交互:验证数据请求、存储的正确性
  3. E2E/UI测试层:
    • 模拟用户完整登录流程:输入账号密码→点击登录→验证跳转至首页(你的示例已经覆盖)
    • 还可以扩展测试异常场景:比如输入错误密码时的错误提示、网络异常时的状态反馈

你的测试代码示例分析

1. 验证用例与Mock Repository交互的集成测试

这个测试属于domain模块的集成测试,逻辑正确:通过Mock AuthRepository,验证LoginUsecase是否正确调用了Repo的登录方法,确保组件间的协作符合预期。

private lateinit var authRepository: AuthRepository
private lateinit var loginUsecase: LoginUsecase

@ExperimentalCoroutinesApi
@get:Rule
var mainCoroutineRole = MainCoroutineRole()

@BeforeEach
fun setup() {
    mockkClass(AuthRepository::class)

    authRepository = mockk()
    loginUsecase = LoginUsecase(authRepository)
}

@Test
fun `login with valid credentials returns user`() = runBlocking {
    val expected = User(username = "spozap", password = "P@ssw0rd!")
    coEvery { authRepository.login(expected.username, expected.password) } returns flow { Resource.Success(expected) }
    loginUsecase("spozap", "P@ssw0rd!")

    coVerify { authRepository.login("spozap", "P@ssw0rd!") }
}

2. 验证用例的单元测试

这是domain模块的标准单元测试,精准覆盖了密码校验的两种场景(空密码、有效密码),逻辑清晰,执行效率高。

private lateinit var validatePasswordUsecase: ValidatePasswordUsecase

@BeforeEach
fun init() {
    validatePasswordUsecase = ValidatePasswordUsecase()
}

@Test
fun `it returns validation error with NOT_NULL_FILLED code when empty password`() {
    val result = validatePasswordUsecase("")
    val expected = ValidationResult( successful = false, errorMessage = ValidationError.NOT_NULL_FIELD )
    assertThat(result).isEqualTo(expected)
}

@Test
fun `it returns successful validation result with no errorMessage when password is valid`() {
    val result = validatePasswordUsecase("P@ssw0rd!")
    val expected = ValidationResult(successful = true, errorMessage = null)
    assertThat(result).isEqualTo(expected)
}

3. UI测试

这是presentation模块的E2E测试,正确模拟了用户登录的完整操作,验证了页面跳转逻辑,能有效保障核心登录流程的可用性。

private lateinit var navController: NavHostController

@get:Rule(order = 0)
val hiltRule = HiltAndroidRule(this)

@get:Rule(order = 1)
val composeRule = createAndroidComposeRule<MainActivity>()


@Before
fun setup() {
    hiltRule.inject()

    composeRule.activity.setContent {
        navController = rememberNavController()
        AppTheme {
            NavHost(navController = navController, startDestination = AppScreens.Login.route ) {
                composable(route = AppScreens.Login.route) {
                    LoginScreen(navController = navController)
                }
                composable(route = AppScreens.Home.route) {
                    HomeScreen()
                }
            }
        }
    }
}

@Test
fun loginWithValidCredentialsRedirectsToHomeScreen() {
    composeRule.onNodeWithTag(TestTags.USERNAME_INPUT).performTextInput("user1")
    composeRule.onNodeWithTag(TestTags.PASSWORD_INPUT).performTextInput("P@ssw0rd")
    composeRule.onNodeWithText("Entrar").performClick()

    assertThat(navController.currentBackStackEntry?.destination?.route)
        .isEqualTo(AppScreens.Home.route)
}

内容的提问来源于stack exchange,提问作者Sergi Poza Paterna

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.06 03:30:03