ZXing自动化测试终极指南:Espresso与UI Automator实战对比

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测试中的典型应用场景:

  1. 扫描界面UI控件校验:断言“扫描框”视图是否可见、位置是否正确;“手电筒”开关按钮的文本和状态;“相册选择”按钮是否存在并可点击。
  2. 权限弹窗处理:测试应用首次打开时,相机权限、存储权限弹窗的弹出逻辑,以及用户授权/拒绝后的应用状态流转。Espresso可以方便地监听和操作系统弹窗(需结合GrantPermissionRule)。
  3. 扫描结果页面的验证:扫码成功后,通常会跳转到一个结果页面(比如商品详情页、网页链接页)。Espresso可以快速断言这个页面是否成功启动,并且页面上的关键信息(如商品名称、链接URL)是否正确显示。

它的优势在于“快”和“准”。因为运行在应用内,它几乎可以实时同步应用状态,避免了因等待界面稳定而产生的超时问题。对于验证ZXing集成后应用内部的UI逻辑和状态流转,Espresso是首选。

但它的局限性也很明显——无法跨进程。这意味着:

  • 无法真正测试相机预览:你无法通过Espresso去断言相机预览画面是否正常开启,或者模拟摄像头捕捉到的图像变化。
  • 无法测试真正的扫码识别过程:ZXing的核心CaptureActivityBarcodeScanner内部复杂的图像解码逻辑,对于Espresso来说是个黑盒。你只能测试“触发扫码”和“接收结果”这两个端点。
  • 难以模拟复杂的物理交互:比如模拟手机晃动、对准不同角度和距离的二维码,这些涉及传感器和相机硬件的交互,Espresso无能为力。

实操心得:很多团队刚开始做ZXing自动化时,试图用Espresso去点击“扫描按钮”然后等待结果,却发现测试极其脆弱。原因就在于,他们测试的其实是“从点击到启动相机”这段逻辑,而真正的识别过程是不可控的。正确的做法是,用Espresso验证扫描界面元素和权限流,然后用Mock(模拟)的方式替换掉真正的ZXing解码器,直接注入一个预设的扫码结果,来验证后续的业务逻辑。这属于“白盒”测试的范畴。

2.2 UI Automator:功能强大的“黑盒”测试专家

UI Automator则走了另一条路。它运行在独立的进程,通过Android的辅助功能服务(Accessibility Service)来查看和操作屏幕上的所有元素,不关心应用内部实现。它就像一位从外部操作手机的“用户”,能看到什么就点什么。

在ZXing测试中的“杀手级”应用场景:

  1. 测试完整的端到端(E2E)扫码流程:这是UI Automator的舞台。它可以:启动你的应用 -> 找到并点击“扫一扫”按钮 -> 等待系统相机界面出现(这可能是另一个应用进程)-> 甚至可以通过截图、图像处理的方式,在屏幕上“模拟”出一个二维码(例如,在另一台设备上显示二维码,或用测试机打开一张二维码图片),让相机去识别 -> 最后验证应用是否跳转到正确的结果页面。
  2. 与系统UI和第三方应用交互:测试从相册选择二维码图片的流程。UI Automator可以打开系统相册应用,滚动并选择指定的测试图片,整个过程完全模拟真实用户操作。
  3. 多应用协同场景:测试“朋友从微信发来一个二维码,你长按识别后跳转到我的应用”这种场景。虽然复杂,但理论上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的回调。

  1. 首先,确保你的扫描组件是可测试的。例如,你的ScanActivity持有一个BarcodeScanner的实例,它有一个setResultCallback方法。

    class ScanActivity : AppCompatActivity() { lateinit var barcodeScanner: BarcodeScanner // 通过依赖注入更好 override fun onCreate(...) { // ... barcodeScanner.resultCallback = { barcodeResult -> // 处理结果,比如跳转到ProductActivity handleScanResult(barcodeResult.text) } } }
  2. 在测试中,使用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 场景三:异常与边界情况处理

健壮的测试必须覆盖异常情况。

  1. 拒绝相机权限

    @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())) }
  2. 扫描无法识别的图片

    @Test fun scanUnrecognizableImage_showsFailureToast() { // 使用Mock/Fake的扫描器,让其回调返回null或失败状态 scenario.onActivity { activity -> activity.triggerMockScanFailure() } // 验证Toast弹出(Espresso有onToast方法) onToast(withText("未识别到二维码,请重试")).check(matches(isDisplayed())) }
  3. 网络错误(扫码结果是需要请求网络的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.resBy.text

4.3 CI/CD集成与稳定性提升

  1. 分套运行

    • 快速套件(CI每次提交触发):只运行Espresso的UI校验和Mock业务逻辑测试。它们应该在5分钟内跑完。
    • 慢速套件(每日夜间构建触发):运行UI Automator的E2E核心流程测试。这些测试可以运行在云真机平台(如Firebase Test Lab)上,覆盖多种设备。
  2. 处理不稳定性

    • 显式等待:UI Automator中多用device.wait(Until..., timeout),避免硬性Thread.sleep
    • 重试机制:对于非逻辑性的失败(如元素偶尔找不到),可以在测试框架层加入重试逻辑。
    • 截图与日志:测试失败时,自动截屏并保存Logcat,这是排查UI Automator测试失败原因的救命稻草。
    • 环境隔离:确保测试设备在测试前处于干净状态(无无关弹窗、固定亮度、关闭动画)。
  3. Mock Server:对于扫码后涉及网络请求的流程,务必使用MockWebServerWireMock。这不仅能模拟各种网络情况(成功、失败、超时),还能确保测试不依赖外部不稳定的测试环境。

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:页面使用SurfaceViewTextureView(相机预览)导致。Espresso的默认视图匹配器可能无法很好地与这些视图协作。
    • 解决:尝试使用onView(withId(R.id.viewfinder)).check(matches(isDisplayed()))如果不行,考虑给这些视图包裹一个FrameLayout,或者通过检查其父视图或兄弟视图的状态来间接断言。
  • 可能原因B:页面元素是动态加载或延迟渲染的。
    • 解决:使用Espresso的IdlingResource。让扫描页面在相机初始化完成、UI渲染完毕后再通知测试框架。这是处理异步加载的标准做法。

问题2:UI Automator脚本在部分机型上,点击“相册”按钮无效。

  • 可能原因A:权限弹窗遮挡。第一次访问相册会弹出存储权限请求。
    • 解决:在点击“相册”按钮后,加入一个等待和检查权限弹窗的逻辑,并自动点击“允许”。
    device.wait(Until.findObject(By.textContains("允许")), 2000)?.click()
  • 可能原因B:系统相册的UI差异巨大。不同厂商的相册应用包名、布局完全不同。
    • 解决:这是UI Automator跨应用测试的最大痛点。策略是:
      1. 优先测试“从相册选择”这个功能本身,可以使用一个应用内的图片选择器(如使用Intent.ACTION_PICK并Mock掉系统选择器)。
      2. 如果必须测系统相册,则编写多个try-catch分支,针对主流厂商(小米、华为、三星等)的相册UI进行适配。这维护成本很高,需谨慎评估。

问题3:Mock测试时,如何优雅地注入Mock对象到Activity中?

  • 解决:这指向了应用架构。采用依赖注入框架(如Hilt、Koin)是终极解决方案。在测试中,你可以提供一个测试模块,将真实的BarcodeScanner替换为FakeBarcodeScanner
  • 临时方案:如果项目没有DI,可以在ScanActivity中提供一个setTestScanner方法(仅debug构建类型可用),或在Application类中设置一个全局的测试标志位和测试桩。

问题4:测试总是不稳定,时而过时而过不了。

  • 黄金法则将不稳定的测试从CI阻塞门禁中移除。不稳定的测试比没有测试更糟糕,因为它会带来“狼来了”效应,导致团队忽视所有测试失败。
  • 排查步骤
    1. 分析失败日志和截图,看是元素找不到,还是超时,还是应用崩溃。
    2. 如果是元素找不到/超时,增加等待时间,或检查动画是否关闭(在开发者选项中关闭窗口动画、过渡动画等)。
    3. 如果是应用崩溃,查看崩溃堆栈,可能是测试环境与生产环境数据差异导致。
    4. 考虑为这些不稳定测试打上@FlakyTest标签,并定期手动分析原因。

最后,我个人最深刻的体会是:没有银弹。Espresso和UI Automator各有优劣,将它们组合使用,并辅以坚实的单元测试和巧妙的Mock策略,才能为ZXing这样的复杂功能构建起一道可靠的质量防线。从简单的UI断言开始,逐步扩展到可控的集成测试,最后用少量E2E场景进行兜底,这个渐进的过程既能快速看到收益,又能持续积累测试资产。记住,测试代码也是产品代码,需要同样的设计、重构和维护意识。