为何视觉测试对移动应用质量至关重要

5 小时前   出处: alexzh.com  作/译者:Alex Zhukovich/空山新雨

您是否曾经发布过存在界面问题的应用程序?

在当今快节奏的发展背景下,这种情况变得愈发频繁——即便对那些有专门的人工QA和自动化UI测试人员的团队也是如此。

我们常低估UI(用户界面)问题的重要性。但这些问题远不只是按钮错位那么简单。许多UI缺陷会导致应用无法正常使用,例如:

  • 文本色差导致可读性不足
  • 不完善的UI损害品牌可信度
  • 负面评价导致下载量下滑

但是等一下......我已经做过了UI测试啊。

即使您有UI测试,这并不意味着它们验证了像素的完美性。许多UI测试检查组件、屏幕或应用程序的行为,但不会检查像素级还原度。

不同类型的UI测试验证了应用程序的不同方面。使用人造数据的端到端和UI测试侧重于用户行为,但它们不检查元素之间的对齐、明暗模式的颜色正确性以及其他视觉细节。

现在有一个解决方案——那就是视觉测试。

什么是视觉测试?

视觉测试专注于识别组件和屏幕中的像素缺陷。在Android开发中,视觉测试是通过屏幕截图比较来实现的。这涉及到将UI的当前状态与基线截图(通常称为“黄金基准截图”)所包含的组件或屏幕的预期状态进行比较。

屏幕截图对比技术,允许您精准识别以下问题:

  • 元素对齐与间距异常
  • 深浅模式及自定义主题中的色彩偏差
  • 各种设备(手机、平板电脑、可折叠设备)和字体大小的布局问题
  • 从右到左(RTL)和从左到右(LTR)布局中的版式渲染错误,这通常与区域设置有关
  • 本地化问题,例如当某些语言的内容超过可用空间时,文本就会溢出
  • 与显示内容相关的辅助功能问题,例如颜色对比度问题
  • 特定场景中的意外渲染问题

比如下图:

Android(安卓)中有多个可用于屏幕截图测试的框架,下面的表格对比了当前一些主流的测试框架:

框架名字 测试类型 渲染引擎
Shot 设备测试 原生设备渲染
Roborazzi 本地测试 Robolectric原生图形
Paparazzi 本地测试 Layoutlib引擎
Compose预览测试 本地测试 Layoutlib引擎

注:"Layoutlib引擎"是Android Studio用于预览Compose组件的专用渲染引擎。

这些框架基本都遵循类似的工作流程,主要包含两个命令:

  • “record”命令为您的测试生成黄金基准截图
  • “verify”命令将当前UI状态与黄金基准截图进行比较。

其中命令语法因框架而异。例如,在使用Shot框架时,您将使用-Precord参数来生成基线图像:

./gradlew :app:debugExecuteScreenshotTests -Precord

好的,这看起来很简单,但是来看个具体的例子吧。

视觉测试实战:Shot框架应用示例

为了确保截图测试的一致性,使用一致的模拟数据并使用相同的模拟器或设备(如果您使用仪器测试),这一点至关重要。使用不同的设备或模拟器会导致分辨率变化,并将导致测试失败。

让我们使用Shot框架来探究心情跟踪应用程序中统计屏幕的两个截图测试。测试将侧重于表示空和成功的UI状态(可用于图表渲染的数据)。

    companion object {
        val TEST_DATE = LocalDate(2024, Month.SEPTEMBER, 15)
    }


    @get:Rule
    val composeTestRule = createComposeRule()

    private val dateProvider = mockk<DateProvider>()
    private val moodHistoryRepository = mockk<MoodHistoryRepository>()


    @Before
    fun setUp() {
        initDI()
    }


    @Test
    fun statisticsScreen_noData() {
        val startDate = TEST_DATE.atStartOfMonth().atStartOfDayIn(defaultTimeZone)
        val endDate = TEST_DATE.atEndOfMonth().atEndOfDay()

        every { dateProvider.getCurrentDate() } returns TEST_DATE
        every { moodHistoryRepository.getAverageDayToHappiness(startDate, endDate) } returns flowOf(emptyList())
        every { moodHistoryRepository.getActivityToHappinessByDate(startDate, endDate) } returns flowOf(emptyList())

        composeTestRule.setContent {
            FeelTrackerAppTheme {
                StatisticsScreen(
                    viewModel = koinViewModel(),
                    onHome = { },
                    onBreathingPatternSelection = { },
                    onSettings = { }
                )
            }
        }

        compareScreenshot(
            rule = composeTestRule,
            name = "statisticsScreen_noData"
        )
    }


    @Test
    fun statisticsScreen_hasData() {
        val startDate = TEST_DATE.atStartOfMonth().atStartOfDayIn(defaultTimeZone)
        val endDate = TEST_DATE.atEndOfMonth().atEndOfDay()
        val firstDateOfTheMonth = TEST_DATE.atStartOfMonth()

        every { dateProvider.getCurrentDate() } returns StatisticsScreenScreenshotTest.TEST_DATE
        every { moodHistoryRepository.getAverageDayToHappiness(startDate, endDate) } returns averageDayToHappinessChartData(firstDateOfTheMonth)
        every { moodHistoryRepository.getActivityToHappinessByDate(startDate, endDate) } returns activityToHappinessData()

        composeTestRule.setContent {
            FeelTrackerAppTheme {
                StatisticsScreen(
                    viewModel = koinViewModel(),
                    onHome = { },
                    onBreathingPatternSelection = { },
                    onSettings = { }
                )
            }
        }

        compareScreenshot(
            rule = composeTestRule,
            name = "statisticsScreen_hasData"
        )
    }


    private fun initDI() {
        stopKoin()
        startKoin {
            allowOverride(true)
            androidContext(InstrumentationRegistry.getInstrumentation().targetContext)
            modules(
                ...
                module {
                    single { dateProvider }
                    single { moodHistoryRepository }
                }
            )
        }
    }


    private fun averageDayToHappinessChartData(startDate: LocalDate): Flow<List<MoodDayToHappiness>> {
        return flowOf(listOf(...))
    }


    private fun activityToHappinessData(): Flow<List<ActivityToHappiness>> {
        return flowOf(listOf(...))
    }
}

重要提示:

  • 测试类需要实现“ScreenshotTest”接口的截图比较功能
  • 需要固定日期来确保截图测试的可重复性
  • 该测试使用数据提供商和存储库的打桩(mock)实例来模拟不同的屏幕状态。它使用“Koin”框架。

操作流程:

  • 生成黄金基准截图的命令
./gradlew :app:debugExecuteScreenshotTests -Precor

此命令将UI的当前状态保存为未来比较的参考点。

现在,让我们通过将平均每日情绪图表的标题从“平均每日情绪”更改为“每日情绪”来模拟真实场景,并进行验证:

  • 变更后验证命令
./gradlew :app:debugExecuteScreenshotTests

执行后,Shot框架会生成一份报告,在其中突出显示比较后的差异点

  • 执行效果演示

注:此图像包含缩放效果,仅用于演示目的。

这种视觉反馈比手动检查每个组件和屏幕要高效得多。要更好地了解不同类型的UI测试之间的区别以及它们如何相互补充,请查看“并非所有UI测试都是一样的”一文。

结论

视觉测试对于交付没有视觉缺陷的应用程序至关重要。虽然UI测试侧重于行为验证,但它们无法检查应用程序的像素完整性。

通过实施视觉测试,开发团队可以在发布应用程序之前发现视觉缺陷。这些测试将发现组件错位、各种主题的颜色不正确、不同设备上的视觉问题等等。

未经打磨的UI看起来总是不专业,这可能会损害对品牌的信任度。通过将UI测试纳入开发过程,不仅检查了像素的完美性,还保护了品牌形象和声誉。


声明:本文为本站编辑转载,文章版权归原作者所有。文章内容为作者个人观点,本站只提供转载参考(依行业惯例严格标明出处和作译者),目的在于传递更多专业信息,普惠测试相关从业者,开源分享,推动行业交流和进步。 如涉及作品内容、版权和其它问题,请原作者及时与本站联系(QQ:1017718740),我们将第一时间进行处理。本站拥有对此声明的最终解释权!欢迎大家通过新浪微博(@测试窝)或微信公众号(测试窝)关注我们,与我们的编辑和其他窝友交流。
/14 人阅读/0 条评论 发表评论

登录 后发表评论
最新文章