内存泄漏会导致性能问题和应用程序崩溃,因此要检测并修复。然而,如果项目架构设计良好或有经验的开发人员参与,泄漏通常很少发生。手动检测泄漏通常无法获得结果,并且需要大量的时间。我建议将内存泄漏检测集成到UI测试中以节省时间。
为什么要进行UI测试?
我们最初并没有计划使用UI测试,我们通过手动检测泄漏。我们和其他人一样使用LeakCanary库来检测泄漏。
我们最初的计划是在为开发人员和测试人员构建的版本中启用LeakCanary。每当他们发现泄漏时,就可以创建一个错误报告。
但随着时间的推移,我们发现了一些问题:
LeakCanary会对性能产生负面影响。手动测试本身就很耗时,我们不想让它变得更加冗长。
LeakCanary通常需要很长时间才能创建内存转储文件。
有时候,泄漏会被忽视或者故意忽略。
我们需要找到一个方案来解决这些问题。当手动方法行不通时,人们更倾向于使用自动化。因此,最直接、最合乎逻辑的解决方案就是将泄漏检测集成到UI测试中。
第一个版本
当我们想出将内存泄漏检测集成到UI测试中的解决方案时,我们的公司刚刚为Android引入了原生UI测试。因此,这类测试数量很少,仅覆盖了有限的用户场景。这促使我们做出了一个坚定的决定——编写专门用于内存泄漏检测的UI测试。
专业的测试
这种测试的步骤很简单:
打开这个应用程序。
打开一个屏幕。
然后是另一个和另一个。
总之,尽可能多地打开屏幕。
关闭屏幕。通常来说,测试框架会负责处理这项任务。
使用 LeakCanary 进行内存泄漏检测。
本测试的主要目的是尽可能多地打开屏幕,然后关闭所有屏幕并开始泄漏检测。
简单介绍一下LeakCanary
首先,必须在 LeakCanary 中添加依赖项、初始化并启用检测。这些步骤由 LeakCanary 集成自动处理。
但也有一些问题:
通过 LeakCanary 进行内存泄露检测通常仅在 UI 测试中启用。然而,开发人员可能希望在调试版本中启用它,以便验证是否已修复了内存泄露问题。
默认情况下最好禁用LeakCanary,但在必要时可以启用它。
系统中存在漏洞、第三方库以及其他一些你无法控制的因素。
我建议创建一个名为“Config”的文件,以便自由地开启和关闭内存泄漏检测。“Config”文件有助于避免由于供应商问题导致的频繁测试失败。例如,三星键盘管理器的泄漏问题。这个“Config”文件可以用来开启和关闭内存泄漏检测,并在需要时添加自定义的引用匹配器。
将 LeakCanary 集成到测试中
你可以通过在测试完成时运行特定代码的方式,轻松地将 LeakCanary 集成到 UI 测试中。
LeakAssertions.assertNoLeaks()
测试完成后,如果您正在使用 Espresso,请在标注了 After 注解的方法中使用 LeakCanary 进行内存泄漏检测。
@After
fun 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的泄漏检测。
@After
fun after() {
if (IS_LEAK_TEST) {
// Launch leak detection
LeakAssertions.assertNoLeaks()
}
}
在Kaspresso中实现这种逻辑并不太难。
after.after {
if (IS_LEAK_TEST) {
LeakAssertions.assertNoLeaks()
}
actions(this)
}
就是这样。这些改进可能很小,尤其是在不考虑之前编写的数百个测试的情况下。
到底是什么?
该方案的效果很好,因为我们在候选版本构建中实现了完全自动化的内存泄漏检测,从而能够识别并修复内存泄漏问题。除了报告检测到的内存泄漏外,开发人员的参与度非常低。
我们目前对它很满意。
实际上,两种选择都有各自的优点。
第一种方案容易集成,但难以维护。适用于 UI 测试覆盖率较低的情况。
第二种方案集成起来比较困难,但支持起来比较容易。当UI测试覆盖率较高时适用。但这并不是每个人都具备的,而且也不总是必需的。
往期系列文章