ZXing自动化测试终极指南:Espresso与UI Automator实战对比
1. 项目概述:为什么我们需要一份“终极”的ZXing测试指南?
在移动应用开发里,集成二维码/条形码扫描功能几乎是标配,而ZXing(Zebra Crossing)库无疑是这个领域的“老大哥”。但不知道你有没有遇到过这种场景:功能开发完了,测试同学跑过来说,“扫码页面在XX机型上闪退”、“连续扫多个码识别率不稳定”、“从相册选择二维码图片偶尔没反应”。这时候,你可能会手忙脚乱地写几个简单的JUnit单元测试,或者让测试同学手动点点点。问题在于,单元测试覆盖不了UI交互和相机硬件的调用,而纯手动测试又低效、不可重复,尤其是在需要覆盖多种设备、多种场景(如弱光、倾斜、多码同屏)时,简直是一场噩梦。
这就是我写这份“终极指南”的初衷。它不仅仅是一份API调用文档,而是一份从实战中摔打出来的、针对ZXing集成场景的自动化测试解决方案深度对比与实操手册。核心要解决两个问题:第一,面对ZXing这个涉及相机、UI、图像处理的多层复杂组件,我们该用什么工具来测?第二,这些工具(主要是Espresso和UI Automator)在实际项目中到底怎么用,有哪些“坑”是官方文档不会告诉你的?我会结合我过去在电商、票务等多个重度依赖扫码功能的应用中的测试实践,把Espresso和UI Automator在ZXing测试场景下的优劣掰开揉碎了讲,并给出不同团队、不同阶段下的选型建议和可直接复制粘贴的“最佳实践”代码模板。
无论你是负责开发ZXing功能的Android工程师,还是专注质量保障的测试开发,或者是团队的技术负责人,正在为扫码功能的稳定性发愁,这份指南都能给你提供一条清晰的、可落地的自动化测试建设路径。我们会从最简单的页面元素断言,一直讲到如何模拟真实世界的复杂扫码场景,确保你的扫码功能坚如磐石。
2. 测试框架选型深度解析:Espresso vs UI Automator
选择正确的工具是成功的一半。在Android UI自动化测试领域,Espresso和UI Automator是Google官方主推的两大框架,但它们的设计哲学和适用场景有显著区别。用在ZXing测试上,这个区别会被放大,选错了工具,可能会事倍功半。
2.1 Espresso:精准快速的“白盒”测试利器
Espresso的核心思想是“与待测应用共舞”。它运行在同一个进程内,能直接访问应用的UI组件和资源,因此速度极快,执行稳定性高。你可以把它想象成一位在应用内部工作的“质检员”,对自家产品的每一个零件都了如指掌。
在ZXing测试中的典型应用场景:
- 扫描界面UI控件校验:断言“扫描框”视图是否可见、位置是否正确;“手电筒”开关按钮的文本和状态;“相册选择”按钮是否存在并可点击。
- 权限弹窗处理:测试应用首次打开时,相机权限、存储权限弹窗的弹出逻辑,以及用户授权/拒绝后的应用状态流转。Espresso可以方便地监听和操作系统弹窗(需结合
GrantPermissionRule)。 - 扫描结果页面的验证:扫码成功后,通常会跳转到一个结果页面(比如商品详情页、网页链接页)。Espresso可以快速断言这个页面是否成功启动,并且页面上的关键信息(如商品名称、链接URL)是否正确显示。
它的优势在于“快”和“准”。因为运行在应用内,它几乎可以实时同步应用状态,避免了因等待界面稳定而产生的超时问题。对于验证ZXing集成后应用内部的UI逻辑和状态流转,Espresso是首选。
但它的局限性也很明显——无法跨进程。这意味着:
- 无法真正测试相机预览:你无法通过Espresso去断言相机预览画面是否正常开启,或者模拟摄像头捕捉到的图像变化。
- 无法测试真正的扫码识别过程:ZXing的核心
CaptureActivity或BarcodeScanner内部复杂的图像解码逻辑,对于Espresso来说是个黑盒。你只能测试“触发扫码”和“接收结果”这两个端点。 - 难以模拟复杂的物理交互:比如模拟手机晃动、对准不同角度和距离的二维码,这些涉及传感器和相机硬件的交互,Espresso无能为力。
实操心得:很多团队刚开始做ZXing自动化时,试图用Espresso去点击“扫描按钮”然后等待结果,却发现测试极其脆弱。原因就在于,他们测试的其实是“从点击到启动相机”这段逻辑,而真正的识别过程是不可控的。正确的做法是,用Espresso验证扫描界面元素和权限流,然后用Mock(模拟)的方式替换掉真正的ZXing解码器,直接注入一个预设的扫码结果,来验证后续的业务逻辑。这属于“白盒”测试的范畴。
2.2 UI Automator:功能强大的“黑盒”测试专家
UI Automator则走了另一条路。它运行在独立的进程,通过Android的辅助功能服务(Accessibility Service)来查看和操作屏幕上的所有元素,不关心应用内部实现。它就像一位从外部操作手机的“用户”,能看到什么就点什么。
在ZXing测试中的“杀手级”应用场景:
- 测试完整的端到端(E2E)扫码流程:这是UI Automator的舞台。它可以:启动你的应用 -> 找到并点击“扫一扫”按钮 -> 等待系统相机界面出现(这可能是另一个应用进程)-> 甚至可以通过截图、图像处理的方式,在屏幕上“模拟”出一个二维码(例如,在另一台设备上显示二维码,或用测试机打开一张二维码图片),让相机去识别 -> 最后验证应用是否跳转到正确的结果页面。
- 与系统UI和第三方应用交互:测试从相册选择二维码图片的流程。UI Automator可以打开系统相册应用,滚动并选择指定的测试图片,整个过程完全模拟真实用户操作。
- 多应用协同场景:测试“朋友从微信发来一个二维码,你长按识别后跳转到我的应用”这种场景。虽然复杂,但理论上UI Automator可以操作微信(如果设备有root或特定权限)。
它的优势在于“广”和“真”。它能覆盖更真实、更完整的用户操作路径,特别是那些需要与系统或其他应用交互的部分。对于验收“扫码功能作为一个整体是否可用”,UI Automator提供的信心更足。
它的代价是“慢”和“脆”。跨进程通信和基于坐标/组件树的查找,使得它的执行速度远慢于Espresso,并且更容易受界面变化、动画、弹窗干扰而失败。脚本的稳定性维护成本较高。
2.3 实战选型决策矩阵
那么,到底该用哪个?我的建议从来不是二选一,而是组合拳。根据你的测试金字塔和团队资源来分配。
| 测试目标 | 推荐工具 | 理由与实操要点 |
|---|---|---|
| 验证扫描界面UI组件 | Espresso | 快速、稳定。适合在每次代码提交后运行,作为CI/CD流水线的一部分。 |
| 验证权限获取逻辑 | Espresso | 结合GrantPermissionRule,可以优雅地处理权限弹窗,测试授权/拒绝分支。 |
| 验证扫码成功后的业务逻辑 | Espresso (Mock) | 最佳实践!在单元测试或Instrumentation测试中,Mock掉ZXing的BarcodeCallback,直接返回预设的扫码结果,然后验证你的业务处理代码(如解析URL、查询商品)。这又快又准。 |
| 完整的E2E扫码用户体验 | UI Automator | 用于核心场景的冒烟测试或每日构建后的验证。例如,主流程:“打开App -> 扫一个静态打印的二维码 -> 进入正确页面”。脚本不宜多,但要精。 |
| 从相册选择二维码 | UI Automator | 必须用它来操作系统相册。需要提前在测试设备相册里准备好测试用的二维码图片。 |
| 性能与兼容性测试 | 自定义脚本 + UI Automator | 测试连续扫码速度、不同尺寸/模糊度二维码的识别率、低光照下的表现等。这需要编写更复杂的脚本,可能还需要控制外部环境(如调节灯光),UI Automator作为操作入口。 |
给团队的建议:初期,优先用Espresso + Mock的方式覆盖核心业务逻辑,保证代码质量。然后,用UI Automator编写少量(3-5个)关键E2E场景脚本,作为发布前的守门员。随着团队测试能力成熟,再考虑用UI Automator扩展更多边界和兼容性用例。
3. 核心测试场景构建与实操详解
理论说完了,我们直接上干货。下面我将构建几个最核心的ZXing测试场景,分别用Espresso和UI Automator来实现,你会看到清晰的代码对比和背后的设计思考。
3.1 场景一:扫描页面加载与基本UI断言
这个场景的目标是确保扫码界面能正常启动,并且关键UI元素都正确显示。
Espresso 实现方案:
@RunWith(AndroidJUnit4::class) class ScanActivityEspressoTest { @get:Rule val activityRule = ActivityScenarioRule(ScanActivity::class.java) @Test fun scanActivity_launchesSuccessfully() { // 1. 验证Activity已启动(规则已处理) // 2. 验证扫描框视图存在且可见 onView(withId(R.id.viewfinder_view)) .check(matches(isDisplayed())) // 3. 验证手电筒开关按钮存在,并且初始文本是“打开手电筒” onView(withId(R.id.flash_switch_button)) .check(matches(isDisplayed())) .check(matches(withText("打开手电筒"))) // 4. 验证相册选择按钮存在且可点击 onView(withId(R.id.album_select_button)) .check(matches(isDisplayed())) .check(matches(isClickable())) // 5. 验证可能有的一些提示文本,比如“将二维码放入框内” onView(withId(R.id.scan_hint_text)) .check(matches(withText("将二维码放入框内"))) .check(matches(isDisplayed())) } @Test fun flashSwitch_buttonClick_togglesText() { // 点击手电筒按钮 onView(withId(R.id.flash_switch_button)).perform(click()) // 验证文本变为“关闭手电筒” onView(withId(R.id.flash_switch_button)) .check(matches(withText("关闭手电筒"))) // 再次点击,文本应变回来 onView(withId(R.id.flash_switch_button)).perform(click()) onView(withId(R.id.flash_switch_button)) .check(matches(withText("打开手电筒"))) } }注意事项:这里测试的是按钮的UI交互逻辑,而非真实控制手电筒。控制手电筒需要相机权限和硬件操作,这部分逻辑应该单独单元测试,或者放在后面的E2E测试中。
UI Automator 实现方案:对于这个纯属应用内部的UI校验,使用UI Automator是大材小用,且不稳定。但如果你的扫描页面是WebView或动态加载的,Espresso可能定位不到元素,这时才考虑UIAutomator。这里仅展示其不同:
@RunWith(AndroidJUnit4::class) class ScanActivityUIAutomatorTest { private val device = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation()) @Test fun scanActivity_launchesSuccessfully_UIAutomator() { // 启动应用(假设MainActivity是入口) val context = InstrumentationRegistry.getInstrumentation().targetContext val intent = context.packageManager.getLaunchIntentForPackage(context.packageName) intent?.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK) context.startActivity(intent) // 点击进入扫描页(假设有一个ID为“scan_btn”的按钮) val scanBtn = device.findObject(By.res(context.packageName, "scan_btn")) scanBtn.click() // 使用UI Automator查找元素 - 效率低,且依赖辅助功能 device.wait(Until.findObject(By.res(context.packageName, "viewfinder_view")), 3000) val viewfinder = device.findObject(By.res(context.packageName, "viewfinder_view")) assertTrue(viewfinder.exists()) // 通过文本查找手电筒按钮(如果ID不稳定) val flashButton = device.findObject(By.text("打开手电筒")) assertTrue(flashButton.exists()) } }踩坑实录:UI Automator通过
By.res查找需要应用的辅助功能开启,且在某些定制ROM上可能不稳定。通过By.text查找则受语言环境影响。因此,对于应用内静态页面的元素断言,强烈优先使用Espresso。
3.2 场景二:模拟扫码成功并验证业务跳转
这是业务逻辑测试的核心。我们不应该依赖不稳定的真实摄像头去识别一个物理二维码,而是应该“模拟”扫码成功的事件。
Espresso + Mock 实现方案(推荐):这是单元测试思维在UI测试上的延伸。我们需要拦截ZXing的回调。
首先,确保你的扫描组件是可测试的。例如,你的
ScanActivity持有一个BarcodeScanner的实例,它有一个setResultCallback方法。class ScanActivity : AppCompatActivity() { lateinit var barcodeScanner: BarcodeScanner // 通过依赖注入更好 override fun onCreate(...) { // ... barcodeScanner.resultCallback = { barcodeResult -> // 处理结果,比如跳转到ProductActivity handleScanResult(barcodeResult.text) } } }在测试中,使用Mockito等框架替换掉真实的BarcodeScanner。
@RunWith(AndroidJUnit4::class) class ScanResultTest { @MockK lateinit var mockScanner: BarcodeScanner @Before fun setup() { MockKAnnotations.init(this) // 在Activity启动前,通过某种方式(如依赖注入框架的测试模块) // 将mockScanner注入到ScanActivity中。 // 这里假设我们有一个可测试的Activity架构。 } @Test fun scanSuccessful_navigatesToProductDetail() { // 1. 启动Activity val scenario = ActivityScenario.launch(ScanActivity::class.java) // 2. 在Activity中,模拟扫码回调被触发 scenario.onActivity { activity -> // 获取Activity内部对mockScanner的回调引用并触发 // 这需要你的Activity提供测试钩子方法,例如: // activity.triggerMockScan("https://example.com/product/123") } // 3. 验证是否跳转到了正确的目标页面 intended(hasComponent(ProductDetailActivity::class.java.name)) // 4. 验证传递的数据是否正确(如果使用Intent) intended(hasExtraWithKey("scan_result")) intended(hasExtra("scan_result", "https://example.com/product/123")) } }如果架构难以注入,一个更直接(但稍显粗糙)的方法是:在测试构建变种中,提供一个Fake(伪造)的
BarcodeScanner实现,它在收到启动指令后,延迟几毫秒直接返回预设结果。这样测试就完全可控了。
UI Automator 实现真实E2E:如果你就是想测试从打开相机到识别的完整链条,那就需要准备一个真实的、稳定的二维码。
@Test fun e2e_scanStaticQrCode_navigatesToWebView() { // 0. 前提:在测试机相册里有一张名为“test_qr_code.png”的图片,内容是一个固定URL。 // 或者,用另一台设备屏幕显示一个二维码。 // 1. 启动应用并进入扫描页(同上) // ... // 2. 难点:如何让相机对准二维码? // 方案A(推荐):测试专用页面。开发一个测试专用的“模拟扫描”Activity,它不打开相机,而是直接显示一个图片选择按钮,选择后调用ZXing解码库解析图片。 // 方案B(不稳定):使用UI Automator控制手机物理移动?这不可行。 // 方案C(折中):测试“从相册选择二维码”的流程。这更可控。 // 我们测试方案C: // 点击“从相册选择”按钮 val albumBtn = device.findObject(By.res(packageName, "album_select_button")) albumBtn.click() // 等待并允许权限(如果需要) device.wait(Until.findObject(By.textContains("允许")), 2000)?.click() // 操作系统相册(这里高度依赖设备相册UI) device.wait(Until.findObject(By.text("相册")), 3000)?.click() // 滚动找到测试图片(通过描述或文字) val testImage = device.findObject(By.desc("test_qr_code.png")) // 需要图片有描述 testImage.click() // 3. 等待应用处理图片并跳转 device.wait(Until.findObject(By.pkg(packageName).depth(0)), 5000) // 验证是否跳转到了WebView或特定页面 val webViewTitle = device.findObject(By.res(packageName, "webview_title")) assertTrue(webViewTitle.exists()) }重要提示:纯UI Automator的完整扫码E2E测试极其脆弱,不适合纳入高频的CI流程。它更适合作为手动测试的自动化辅助,或在受控的实验室环境中运行。方案A(测试专用入口)是平衡可靠性和真实性的最佳实践。
3.3 场景三:异常与边界情况处理
健壮的测试必须覆盖异常情况。
拒绝相机权限:
@Test fun cameraPermissionDenied_showsErrorMessage() { // 使用Espresso的GrantPermissionRule在测试前拒绝权限 // 注意:这条规则需要在Activity启动前生效 @get:Rule val grantPermissionRule: GrantPermissionRule = GrantPermissionRule.grant(android.Manifest.permission.CAMERA) // 但我们要测试拒绝,所以需要自定义逻辑。更常见的做法是: // 在测试构建变种中,让权限检查代码直接返回“拒绝”状态,然后验证是否显示了正确的提示UI。 onView(withId(R.id.permission_denied_hint)) .check(matches(isDisplayed())) onView(withId(R.id.go_to_settings_btn)) .check(matches(isDisplayed())) }扫描无法识别的图片:
@Test fun scanUnrecognizableImage_showsFailureToast() { // 使用Mock/Fake的扫描器,让其回调返回null或失败状态 scenario.onActivity { activity -> activity.triggerMockScanFailure() } // 验证Toast弹出(Espresso有onToast方法) onToast(withText("未识别到二维码,请重试")).check(matches(isDisplayed())) }网络错误(扫码结果是需要请求网络的URL):
@Test fun scanValidCode_butNetworkError_showsRetryUI() { // 使用MockWebServer等工具,在扫码后的网络请求环节模拟网络错误 val mockWebServer = MockWebServer() mockWebServer.enqueue(MockResponse().setResponseCode(500)) // 启动应用,其网络请求BaseURL指向mockWebServer // 触发模拟扫码(内容为指向mockServer的URL) // 验证界面显示“网络错误,点击重试”的UI组件 onView(withId(R.id.retry_layout)).check(matches(isDisplayed())) }
4. 搭建可维护的ZXing自动化测试基础设施
写几个测试用例不难,难的是构建一个稳定、可维护、能持续运行的测试套件。下面分享我总结的几点基础设施建议。
4.1 测试数据管理
二维码不是静态的,尤其是测试电商扫码,商品ID、状态可能变化。
使用测试专用二维码生成器:在测试代码中集成一个二维码生成库(如
ZXing本身),动态生成测试数据。fun generateTestQrCodeBitmap(content: String, size: Int = 300): Bitmap { val writer = MultiFormatWriter() val bitMatrix = writer.encode(content, BarcodeFormat.QR_CODE, size, size) val width = bitMatrix.width val height = bitMatrix.height val bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.RGB_565) for (x in 0 until width) { for (y in 0 until height) { bitmap.setPixel(x, y, if (bitMatrix[x, y]) Color.BLACK else Color.WHITE) } } return bitmap }在UI Automator测试中,可以将这个Bitmap保存到相册的固定位置。在Espresso的Mock测试中,直接使用这个
content字符串作为模拟结果。二维码内容模板化:使用模板定义二维码内容,如
"product://{id}",在测试运行时替换{id}为随机或固定的测试商品ID。便于管理。
4.2 测试代码架构与Page Object模式
无论是Espresso还是UI Automator,都强烈建议使用Page Object(页面对象)模式。将页面的元素定位和操作封装成类,使测试用例更清晰,更易于维护。
// Espresso Page Object示例 class ScanPage { companion object { val viewfinder = withId(R.id.viewfinder_view) val flashButton = withId(R.id.flash_switch_button) val albumButton = withId(R.id.album_select_button) val hintText = withId(R.id.scan_hint_text) } fun clickFlash(): ScanPage { onView(flashButton).perform(click()) return this } fun verifyFlashText(text: String): ScanPage { onView(flashButton).check(matches(withText(text))) return this } } // 在测试用例中使用 @Test fun testScanPageUI() { ScanPage() .verifyFlashText("打开手电筒") .clickFlash() .verifyFlashText("关闭手电筒") }对于UI Automator,同样可以封装,只是定位方式换成By.res或By.text。
4.3 CI/CD集成与稳定性提升
分套运行:
- 快速套件(CI每次提交触发):只运行Espresso的UI校验和Mock业务逻辑测试。它们应该在5分钟内跑完。
- 慢速套件(每日夜间构建触发):运行UI Automator的E2E核心流程测试。这些测试可以运行在云真机平台(如Firebase Test Lab)上,覆盖多种设备。
处理不稳定性:
- 显式等待:UI Automator中多用
device.wait(Until..., timeout),避免硬性Thread.sleep。 - 重试机制:对于非逻辑性的失败(如元素偶尔找不到),可以在测试框架层加入重试逻辑。
- 截图与日志:测试失败时,自动截屏并保存Logcat,这是排查UI Automator测试失败原因的救命稻草。
- 环境隔离:确保测试设备在测试前处于干净状态(无无关弹窗、固定亮度、关闭动画)。
- 显式等待:UI Automator中多用
Mock Server:对于扫码后涉及网络请求的流程,务必使用
MockWebServer或WireMock。这不仅能模拟各种网络情况(成功、失败、超时),还能确保测试不依赖外部不稳定的测试环境。
5. 进阶:复杂场景与性能考量
当基础测试稳定后,可以考虑一些进阶场景,进一步提升扫码功能的质量。
5.1 多码同屏与连续扫描测试
有些应用需要支持同时识别多个二维码,或者快速连续扫描。
- 测试策略:这更多是ZXing库本身的能力测试。我们可以在单元测试级别,为解码器提供一张包含多个二维码的图片,断言其是否能返回所有结果。
- UI测试:对于连续扫描,可以模拟多次触发“扫描成功”回调,验证应用界面是否能正确处理(例如,是每次结果都跳转,还是累积结果)。注意:要测试应用是否在第一次扫码后就暂停了扫描,避免重复处理。
5.2 性能与兼容性测试脚本
这不是单次功能测试,而是需要收集数据的专项测试。
- 识别成功率测试:编写脚本,自动循环遍历一个包含数百张图片的测试集(包含清晰、模糊、残缺、不同大小的二维码),统计ZXing解码器的识别成功率。这可以用纯JUnit测试配合ZXing核心库完成,无需启动App。
- 识别速度测试:在UI Automator脚本中,记录从点击“扫描”按钮到收到结果回调的时间。在大批量测试中收集数据,监控版本迭代是否引入性能回归。
- 兼容性测试矩阵:将你的E2E测试脚本,在云真机平台上针对几十款不同品牌、型号、Android版本的设备运行。重点关注低端机型的表现和崩溃率。
5.3 与AI图像处理的结合(前瞻性思考)
“使用AI写代码的最佳实践”是热词,而AI在测试领域也能大放异彩。例如,你可以训练一个简单的图像分类模型,用于判断测试过程中相机预览画面是否“正常”(如是否对焦模糊、是否过暗、是否有强光反射)。但这已经超出了传统功能测试的范畴,属于质量效能团队的探索方向了。
6. 常见问题排查与调试技巧实录
即使按照最佳实践,测试过程中还是会遇到各种“妖孽”问题。这里记录几个我踩过的坑和解决方法。
问题1:Espresso测试中,onView找不到扫描页面的元素。
- 可能原因A:页面使用
SurfaceView或TextureView(相机预览)导致。Espresso的默认视图匹配器可能无法很好地与这些视图协作。- 解决:尝试使用
onView(withId(R.id.viewfinder)).check(matches(isDisplayed()))如果不行,考虑给这些视图包裹一个FrameLayout,或者通过检查其父视图或兄弟视图的状态来间接断言。
- 解决:尝试使用
- 可能原因B:页面元素是动态加载或延迟渲染的。
- 解决:使用Espresso的
IdlingResource。让扫描页面在相机初始化完成、UI渲染完毕后再通知测试框架。这是处理异步加载的标准做法。
- 解决:使用Espresso的
问题2:UI Automator脚本在部分机型上,点击“相册”按钮无效。
- 可能原因A:权限弹窗遮挡。第一次访问相册会弹出存储权限请求。
- 解决:在点击“相册”按钮后,加入一个等待和检查权限弹窗的逻辑,并自动点击“允许”。
device.wait(Until.findObject(By.textContains("允许")), 2000)?.click() - 可能原因B:系统相册的UI差异巨大。不同厂商的相册应用包名、布局完全不同。
- 解决:这是UI Automator跨应用测试的最大痛点。策略是:
- 优先测试“从相册选择”这个功能本身,可以使用一个应用内的图片选择器(如使用
Intent.ACTION_PICK并Mock掉系统选择器)。 - 如果必须测系统相册,则编写多个
try-catch分支,针对主流厂商(小米、华为、三星等)的相册UI进行适配。这维护成本很高,需谨慎评估。
- 优先测试“从相册选择”这个功能本身,可以使用一个应用内的图片选择器(如使用
- 解决:这是UI Automator跨应用测试的最大痛点。策略是:
问题3:Mock测试时,如何优雅地注入Mock对象到Activity中?
- 解决:这指向了应用架构。采用依赖注入框架(如Hilt、Koin)是终极解决方案。在测试中,你可以提供一个测试模块,将真实的
BarcodeScanner替换为FakeBarcodeScanner。 - 临时方案:如果项目没有DI,可以在
ScanActivity中提供一个setTestScanner方法(仅debug构建类型可用),或在Application类中设置一个全局的测试标志位和测试桩。
问题4:测试总是不稳定,时而过时而过不了。
- 黄金法则:将不稳定的测试从CI阻塞门禁中移除。不稳定的测试比没有测试更糟糕,因为它会带来“狼来了”效应,导致团队忽视所有测试失败。
- 排查步骤:
- 分析失败日志和截图,看是元素找不到,还是超时,还是应用崩溃。
- 如果是元素找不到/超时,增加等待时间,或检查动画是否关闭(在开发者选项中关闭窗口动画、过渡动画等)。
- 如果是应用崩溃,查看崩溃堆栈,可能是测试环境与生产环境数据差异导致。
- 考虑为这些不稳定测试打上
@FlakyTest标签,并定期手动分析原因。
最后,我个人最深刻的体会是:没有银弹。Espresso和UI Automator各有优劣,将它们组合使用,并辅以坚实的单元测试和巧妙的Mock策略,才能为ZXing这样的复杂功能构建起一道可靠的质量防线。从简单的UI断言开始,逐步扩展到可控的集成测试,最后用少量E2E场景进行兜底,这个渐进的过程既能快速看到收益,又能持续积累测试资产。记住,测试代码也是产品代码,需要同样的设计、重构和维护意识。
