Leak detection in UI tests¶
Running leak detection in UI tests means you can detect memory leaks automatically in Continuous Integration prior to new leaks being merged into the codebase.
Test environment detection
In debug builds, LeakCanary looks for retained instances continuously, freezes the VM to take a heap dump after a watched object has been retained for 5 seconds, then performs the analysis in a background thread and reports the result using notifications. That behavior isn’t well suited for UI tests, so LeakCanary is automatically disabled when JUnit is on the runtime classpath (see test environment detection).
Getting started¶
LeakCanary provides an artifact dedicated to detecting leaks in UI tests:
androidTestImplementation 'com.squareup.leakcanary:leakcanary-android-instrumentation:2.14'
// You still need to include the LeakCanary artifact in your app:
debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.14'
You can then call LeakAssertions.assertNoLeaks()
at any point in your tests to check for leaks:
class CartTest {
@Test
fun addItemToCart() {
// ...
LeakAssertions.assertNoLeaks()
}
}
If retained instances are detected, LeakCanary will dump and analyze the heap. If application leaks
are found, LeakAssertions.assertNoLeaks()
will throw a NoLeakAssertionFailedError
.
leakcanary.NoLeakAssertionFailedError: Application memory leaks were detected:
====================================
HEAP ANALYSIS RESULT
====================================
1 APPLICATION LEAKS
┬───
│ GC Root: System class
│
├─ com.example.MySingleton class
│ Leaking: NO (a class is never leaking)
│ ↓ static MySingleton.leakedView
│ ~~~~~~~~~~
├─ android.widget.TextView instance
│ Leaking: YES (View.mContext references a destroyed activity)
│ ↓ TextView.mContext
╰→ com.example.MainActivity instance
Leaking: YES (Activity#mDestroyed is true)
====================================
at leakcanary.AndroidDetectLeaksAssert.assertNoLeaks(AndroidDetectLeaksAssert.kt:34)
at leakcanary.LeakAssertions.assertNoLeaks(LeakAssertions.kt:21)
at com.example.CartTest.addItemToCart(TuPeuxPasTest.kt:41)
Obfuscated instrumentation tests
When running instrumentation tests against obfuscated release builds, the LeakCanary classes end up spread over the test APK and the main APK. Unfortunately there is a bug in the Android Gradle Plugin that leads to runtime crashes when running tests, because code from the main APK is changed without the using code in the test APK being updated accordingly. If you run into this issue, setting up the Keeper plugin should fix it.
Test rule¶
You can use the DetectLeaksAfterTestSuccess
test rule to automatically call
LeakAssertions.assertNoLeaks()
at the end of a test:
class CartTest {
@get:Rule
val rule = DetectLeaksAfterTestSuccess()
@Test
fun addItemToCart() {
// ...
}
}
You can call also LeakAssertions.assertNoLeaks()
as many times as you want in a single test:
class CartTest {
@get:Rule
val rule = DetectLeaksAfterTestSuccess()
// This test has 3 leak assertions (2 in the test + 1 from the rule).
@Test
fun addItemToCart() {
// ...
LeakAssertions.assertNoLeaks()
// ...
LeakAssertions.assertNoLeaks()
// ...
}
}
Skipping leak detection¶
Use @SkipLeakDetection
to disable LeakAssertions.assertNoLeaks()
calls:
class CartTest {
@get:Rule
val rule = DetectLeaksAfterTestSuccess()
// This test will not perform any leak assertion.
@SkipLeakDetection("See issue #1234")
@Test
fun addItemToCart() {
// ...
LeakAssertions.assertNoLeaks()
// ...
LeakAssertions.assertNoLeaks()
// ...
}
}
You can use tags to identify each LeakAssertions.assertNoLeaks()
call and disable only a subset of these calls:
class CartTest {
@get:Rule
val rule = DetectLeaksAfterTestSuccess(tag = "EndOfTest")
// This test will only perform the second leak assertion.
@SkipLeakDetection("See issue #1234", "First Assertion", "EndOfTest")
@Test
fun addItemToCart() {
// ...
LeakAssertions.assertNoLeak(tag = "First Assertion")
// ...
LeakAssertions.assertNoLeak(tag = "Second Assertion")
// ...
}
}
Tags can be retrieved by calling HeapAnalysisSuccess.assertionTag
and are also reported in the
heap analysis result metadata:
====================================
METADATA
Please include this in bug reports and Stack Overflow questions.
Build.VERSION.SDK_INT: 23
...
assertionTag: Second Assertion
Test rule chains¶
// Example test rule chain
@get:Rule
val rule = RuleChain.outerRule(LoginRule())
.around(ActivityScenarioRule(CartActivity::class.java))
.around(LoadingScreenRule())
If you use a test rule chain, the position of the DetectLeaksAfterTestSuccess
rule in that chain
could be significant. For example, if you use an ActivityScenarioRule
that automatically
finishes the activity at the end of a test, having DetectLeaksAfterTestSuccess
around
ActivityScenarioRule
will detect leaks after the activity is destroyed and therefore detect any
activity leak. But then DetectLeaksAfterTestSuccess
will not detect fragment leaks that go away
when the activity is destroyed.
@get:Rule
val rule = RuleChain.outerRule(LoginRule())
// Detect leaks AFTER activity is destroyed
.around(DetectLeaksAfterTestSuccess(tag = "AfterActivityDestroyed"))
.around(ActivityScenarioRule())
.around(LoadingScreenRule())
If instead you set up ActivityScenarioRule
around DetectLeaksAfterTestSuccess
, destroyed
activity leaks will not be detected as the activity will still be created when the leak assertion
rule runs, but more fragment leaks might be detected.
@get:Rule
val rule = RuleChain.outerRule(LoginRule())
.around(ActivityScenarioRule(CartActivity::class.java))
// Detect leaks BEFORE activity is destroyed
.around(DetectLeaksAfterTestSuccess(tag = "BeforeActivityDestroyed"))
.around(LoadingScreenRule())
To detect all leaks, the best option is to
set up the DetectLeaksAfterTestSuccess
rule twice, before and after the ActivityScenarioRule
rule.
// Detect leaks BEFORE and AFTER activity is destroyed
@get:Rule
val rule = RuleChain.outerRule(LoginRule())
.around(DetectLeaksAfterTestSuccess(tag = "AfterActivityDestroyed"))
.around(ActivityScenarioRule(CartActivity::class.java))
.around(DetectLeaksAfterTestSuccess(tag = "BeforeActivityDestroyed"))
.around(LoadingScreenRule())
RuleChain.detectLeaksAfterTestSuccessWrapping()
is a helper for doing just that:
// Detect leaks BEFORE and AFTER activity is destroyed
@get:Rule
val rule = RuleChain.outerRule(LoginRule())
// The tag will be suffixed with "Before" and "After".
.detectLeaksAfterTestSuccessWrapping(tag = "ActivitiesDestroyed") {
around(ActivityScenarioRule(CartActivity::class.java))
}
.around(LoadingScreenRule())
Customizing assertNoLeaks()
¶
LeakAssertions.assertNoLeaks()
delegates calls to a global DetectLeaksAssert
implementation,
which by default is an instance of AndroidDetectLeaksAssert
. You can change the
DetectLeaksAssert
implementation by calling DetectLeaksAssert.update(customLeaksAssert)
.
The AndroidDetectLeaksAssert
implementation performs a heap dump when retained instances are
detected, analyzes the heap, then passes the result to a HeapAnalysisReporter
. The default
HeapAnalysisReporter
is NoLeakAssertionFailedError.throwOnApplicationLeaks()
which throws a
NoLeakAssertionFailedError
if an application leak is detected.
You could provide a custom implementation to also upload heap analysis results to a central place before failing the test:
val throwingReporter = NoLeakAssertionFailedError.throwOnApplicationLeaks()
DetectLeaksAssert.update(AndroidDetectLeaksAssert(
heapAnalysisReporter = { heapAnalysis ->
// Upload the heap analysis result
heapAnalysisUploader.upload(heapAnalysis)
// Fail the test if there are application leaks
throwingReporter.reportHeapAnalysis(heapAnalysis)
}
))