多模块Android应用测试:E2E与集成测试的定位及模块归属疑问
Android测试分层:场景、模块组织与登录流程实践
核心疑问梳理
- E2E测试、集成测试的适用边界
- 不同测试类型在data/domain/presentation模块的放置规则
- 登录流程的测试分层策略:是否需要同时做UI的E2E、VM的集成测试,还是仅用E2E覆盖?
测试类型的适用场景
单元测试
- 聚焦单一独立组件的逻辑验证,比如密码校验规则、单个用例的业务逻辑
- 特点:执行速度快,依赖全部Mock,能精准定位代码逻辑问题
- 示例:你写的
ValidatePasswordUsecase测试,就是典型的单元测试,只测密码校验的独立逻辑
集成测试
- 验证多个组件之间的协作交互,核心是看数据流转、依赖调用是否符合预期
- 适用场景:
- Domain层:用例与Repository接口的交互(比如你写的
LoginUsecase与Mock AuthRepository的测试) - Data层:Repository与真实数据源(Room数据库、Retrofit接口)的协作
- Presentation层:ViewModel与用例的交互(验证VM调用用例后状态更新是否正确)
- Domain层:用例与Repository接口的交互(比如你写的
- 特点:部分依赖可以用真实实现,部分Mock,重点验证组件间的协作链路
E2E测试(含UI测试)
- 模拟真实用户操作路径,验证从前端UI到后端服务的完整业务流程
- 适用场景:核心业务流程的端到端验证,比如登录、支付、下单等
- 示例:你写的登录UI测试,模拟输入账号密码、点击按钮、验证页面跳转,就是典型的E2E测试
- 特点:依赖真实环境或接近真实的测试环境,执行速度较慢,但能保障核心流程的可用性
测试在模块中的组织规则
- domain模块:
- 放置单元测试:测用例、实体、业务规则的独立逻辑
- 放置集成测试:测用例与Repository接口的交互(用Mock Repository,比如你的LoginUsecase集成测试)
- data模块:
- 放置集成测试:测Repository与真实数据源(Room、Retrofit)的协作,验证数据读写、网络请求的正确性
- presentation模块:
- 放置UI测试/E2E测试:测View与ViewModel的交互、页面跳转、用户操作的反馈,比如你的登录UI测试
登录流程的测试分层策略
不能仅靠E2E测试覆盖,分层测试才能兼顾效率与可靠性:
- 单元测试层:
- 测密码校验、账号格式校验等独立用例逻辑(比如你的
ValidatePasswordUsecase测试) - 测登录用例的核心逻辑(比如参数传递、错误处理)
- 测密码校验、账号格式校验等独立用例逻辑(比如你的
- 集成测试层:
- 测
LoginUsecase与AuthRepository的交互(你的示例已经覆盖,验证用例正确调用Repo) - 测ViewModel与
LoginUsecase的交互:验证VM调用用例后,状态(如加载中、成功、错误)是否正确更新 - 测AuthRepository与真实登录API/数据库的交互:验证数据请求、存储的正确性
- 测
- 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
相关产品推荐
相关产品推荐

