使用UI测试内存泄漏

文摘   2024-11-24 19:45   四川  

内存泄漏会导致性能问题和应用程序崩溃,因此要检测并修复。然而,如果项目架构设计良好或有经验的开发人员参与,泄漏通常很少发生。手动检测泄漏通常无法获得结果,并且需要大量的时间。我建议将内存泄漏检测集成到UI测试中以节省时间。

为什么要进行UI测试?

我们最初并没有计划使用UI测试,我们通过手动检测泄漏。我们和其他人一样使用LeakCanary库来检测泄漏。

我们最初的计划是在为开发人员和测试人员构建的版本中启用LeakCanary。每当他们发现泄漏时,就可以创建一个错误报告。

但随着时间的推移,我们发现了一些问题:

  • LeakCanary会对性能产生负面影响。手动测试本身就很耗时,我们不想让它变得更加冗长。

  • LeakCanary通常需要很长时间才能创建内存转储文件。

  • 有时候,泄漏会被忽视或者故意忽略。

我们需要找到一个方案来解决这些问题。当手动方法行不通时,人们更倾向于使用自动化。因此,最直接、最合乎逻辑的解决方案就是将泄漏检测集成到UI测试中。

第一个版本

当我们想出将内存泄漏检测集成到UI测试中的解决方案时,我们的公司刚刚为Android引入了原生UI测试。因此,这类测试数量很少,仅覆盖了有限的用户场景。这促使我们做出了一个坚定的决定——编写专门用于内存泄漏检测的UI测试。

专业的测试

这种测试的步骤很简单:

  1. 打开这个应用程序。

  2. 打开一个屏幕。

  3. 然后是另一个和另一个。

  4. 总之,尽可能多地打开屏幕。

  5. 关闭屏幕。通常来说,测试框架会负责处理这项任务。

  6. 使用 LeakCanary 进行内存泄漏检测。

本测试的主要目的是尽可能多地打开屏幕,然后关闭所有屏幕并开始泄漏检测。

简单介绍一下LeakCanary

首先,必须在 LeakCanary 中添加依赖项、初始化并启用检测。这些步骤由 LeakCanary 集成自动处理。

但也有一些问题:

  • 通过 LeakCanary 进行内存泄露检测通常仅在 UI 测试中启用。然而,开发人员可能希望在调试版本中启用它,以便验证是否已修复了内存泄露问题。

  • 默认情况下最好禁用LeakCanary,但在必要时可以启用它。

系统中存在漏洞、第三方库以及其他一些你无法控制的因素。

我建议创建一个名为“Config”的文件,以便自由地开启和关闭内存泄漏检测。“Config”文件有助于避免由于供应商问题导致的频繁测试失败。例如,三星键盘管理器的泄漏问题。这个“Config”文件可以用来开启和关闭内存泄漏检测,并在需要时添加自定义的引用匹配器。

将 LeakCanary 集成到测试中

你可以通过在测试完成时运行特定代码的方式,轻松地将 LeakCanary 集成到 UI 测试中。

LeakAssertions.assertNoLeaks()

测试完成后,如果您正在使用 Espresso,请在标注了 After 注解的方法中使用 LeakCanary 进行内存泄漏检测。

@Afterfun after() {   // Launch leak detection   LeakAssertions.assertNoLeaks()}

我们公司使用 Kaspresso,并为其编写了一个外壳,以便在所有测试的初始化和之后部分中包含我们自己的代码。以下是我们实现这一功能的示例:

class LeakKaspressoRule(   testClassName: String) : TestRule {val kaspressoRule = KaspressoRule(testClassName)   override fun apply(base: Statement, description: Description): Statement {       return kaspressoRule.apply(base, description)   }   fun before(actions: BaseTestContext.() -> Unit) = After(kaspressoRule.before {       // own code       actions(this)   })   class After(       private val after: AfterTestSection<Unit, Unit>   ) {       fun after(actions: BaseTestContext.() -> Unit) = Init(after.after {           // own code           actions(this)       })   }   class Init(       private val init: InitSection<Unit, Unit>   ) {       fun run(steps: TestContext<Unit>.() -> Unit) = init.run {           steps(this)           // own code       }   }}

在After Kaspresso类中添加泄漏检测功能。

after.after {   LeakAssertions.assertNoLeaks()   actions(this)}

就是这样。让我们来看一个这样的测试的例子。

class LeakAuthUiTest {

@get:Rule    val leakKaspressoRule = LeakKaspressoRule(javaClass.simpleName)    @Test    fun testLeakOnAuth() {        leakKaspressoRule.before {        }.after {        }.run {            step("Open user profile") { ... }            step("Open auth")  { ... }            step("Open registration")  { ... }            step("Open restore password")  { ... }        }    }}

正如上面所描述的,所有操作都是通过打开屏幕并运行LeakKaspressoRule中的内存泄漏检测来完成的,一旦测试完成。

现在让我们来讨论一下如何启动这些测试并为其提供支持。

启动和支持

我们的内存泄漏测试是在我们需要的时候进行的,这种情况并不常见。通常来说,每个月最多只会进行一次。

然而,这种方法通常很有效。我们会编写基于特定场景的测试用例,以打开一系列屏幕。运行这些专门的测试后,我们定期会发现内存泄露问题。因此,我们会启动相应的任务来解决这些问题,而最好的一点是,这并不需要开发人员或测试人员花费过多的时间。

但是……随着时间的推移,支持此类测试的问题出现了。这是因为应用程序不断变化,需要相应地调整测试。尤其是当它们影响到多个屏幕时。

开发人员在无意中破坏了检测内存泄漏的测试,而自己却并未意识到。由于这些测试没有持续运行,因此我们不得不定期创建任务来修复这些测试。编写此类新测试对任何人来说都不是理想的选择,因为这些测试需要进行痛苦的维护。这种情况持续了一段时间。

值得注意的是,与这种方法相比,手动检测泄漏需要投入更多的精力。

我们觉得是时候做出改变了。

第二个版本

到那时,常规的UI测试已经超过了100个。这些测试覆盖了更多的屏幕和用户场景,比专门用于漏洞检测的测试要多得多。

我们决定尝试用不同的方式做事,因为我们不再需要那些专业的测试了。

新概念

我们不会在项目中加入专门的泄漏检测测试,而是会在每次测试结束后添加一个运行泄漏检测的选项。

重要的是,这只是一种选择。将泄漏检测添加到测试中会显著增加测试时间。对于这些测试来说,快速运行非常重要。因为,我们在将特征分支合并到主分支之前会在Git中运行UI测试。

在我们的持续集成(CI)中,我们实现了以下逻辑:

  • 当UI测试在合并前进行时,它们会像平常一样运行,不会进行泄漏检测。只会执行与Git分支中已更改代码相关的测试。

  • 如果这些UI测试在发布候选版本构建之前执行,则会运行所有带有内存泄漏检测的测试。

它是如何工作的

要轻松实现此行为,可以添加一个标志并将其作为参数传递给 TestRunner。如果 `isLeakTest` 标志设置为 true,则表示应以泄漏检测模式运行测试。

其值是从测试运行器参数中读取的,然后写入一个静态变量中。

class CianUiTestRunner : AllureAndroidJUnitRunner() {      override fun onCreate(arguments: Bundle) {       IS_LEAK_TEST = arguments.getString("isLeakTest") == "true"   }}

在Espresso的After注解方法中,我们仅在`IS_LEAK_TEST`标志为true时运行LeakCanary的泄漏检测。

@Afterfun after() {   if (IS_LEAK_TEST) {       // Launch leak detection       LeakAssertions.assertNoLeaks()   }}

在Kaspresso中实现这种逻辑并不太难。

after.after {   if (IS_LEAK_TEST) {       LeakAssertions.assertNoLeaks()   }   actions(this)}

就是这样。这些改进可能很小,尤其是在不考虑之前编写的数百个测试的情况下。

到底是什么?

该方案的效果很好,因为我们在候选版本构建中实现了完全自动化的内存泄漏检测,从而能够识别并修复内存泄漏问题。除了报告检测到的内存泄漏外,开发人员的参与度非常低。

我们目前对它很满意。

实际上,两种选择都有各自的优点。

  • 第一种方案容易集成,但难以维护。适用于 UI 测试覆盖率较低的情况。

  • 第二种方案集成起来比较困难,但支持起来比较容易。当UI测试覆盖率较高时适用。但这并不是每个人都具备的,而且也不总是必需的。


往期系列文章

阿里微服务质量保障系列:异步通信模式以及测试分析

阿里微服务质量保障系列:微服务知多少

阿里微服务质量保障系列:研发流程知多少

阿里微服务质量保障系列:研发环境知多少

阿里微服务质量保障系列:阿里变更三板斧

阿里微服务质量保障系列:故障演练

阿里微服务质量保障系列:研发模式&发布策略

阿里微服务质量保障系列:性能监控

阿里微服务质量保障系列:性能监控最佳实践

阿里微服务质量保障系列:基于全链路的测试分析实践

阿里微服务质量保障系列 服务虚拟化技术

软件质量保障
所寫即所思|一个阿里质量人对测试技术的思考。
 最新文章