1*d9e8da70SAndroid Build Coastguard Worker# Leak detection in UI tests 2*d9e8da70SAndroid Build Coastguard Worker 3*d9e8da70SAndroid Build Coastguard WorkerRunning leak detection in UI tests means you can detect memory leaks automatically in Continuous 4*d9e8da70SAndroid Build Coastguard WorkerIntegration prior to new leaks being merged into the codebase. 5*d9e8da70SAndroid Build Coastguard Worker 6*d9e8da70SAndroid Build Coastguard Worker!!! info "Test environment detection" 7*d9e8da70SAndroid Build Coastguard Worker In debug builds, LeakCanary looks for retained instances continuously, freezes the VM to take 8*d9e8da70SAndroid Build Coastguard Worker a heap dump after a watched object has been retained for 5 seconds, then performs the analysis 9*d9e8da70SAndroid Build Coastguard Worker in a background thread and reports the result using notifications. That behavior isn't well suited 10*d9e8da70SAndroid Build Coastguard Worker for UI tests, so LeakCanary is automatically disabled when JUnit is on the runtime classpath 11*d9e8da70SAndroid Build Coastguard Worker (see [test environment detection](recipes.md#leakcanary-test-environment-detection)). 12*d9e8da70SAndroid Build Coastguard Worker 13*d9e8da70SAndroid Build Coastguard Worker## Getting started 14*d9e8da70SAndroid Build Coastguard Worker 15*d9e8da70SAndroid Build Coastguard WorkerLeakCanary provides an artifact dedicated to detecting leaks in UI tests: 16*d9e8da70SAndroid Build Coastguard Worker 17*d9e8da70SAndroid Build Coastguard Worker``` 18*d9e8da70SAndroid Build Coastguard WorkerandroidTestImplementation "com.squareup.leakcanary:leakcanary-android-instrumentation:${leakCanaryVersion}" 19*d9e8da70SAndroid Build Coastguard Worker``` 20*d9e8da70SAndroid Build Coastguard Worker 21*d9e8da70SAndroid Build Coastguard WorkerYou can then call `LeakAssertions.assertNoLeak()` at any point in your tests to check for leaks: 22*d9e8da70SAndroid Build Coastguard Worker 23*d9e8da70SAndroid Build Coastguard Worker ```kotlin 24*d9e8da70SAndroid Build Coastguard Worker class CartTest { 25*d9e8da70SAndroid Build Coastguard Worker 26*d9e8da70SAndroid Build Coastguard Worker @Test 27*d9e8da70SAndroid Build Coastguard Worker fun addItemToCart() { 28*d9e8da70SAndroid Build Coastguard Worker // ... 29*d9e8da70SAndroid Build Coastguard Worker LeakAssertions.assertNoLeak() 30*d9e8da70SAndroid Build Coastguard Worker } 31*d9e8da70SAndroid Build Coastguard Worker } 32*d9e8da70SAndroid Build Coastguard Worker ``` 33*d9e8da70SAndroid Build Coastguard Worker 34*d9e8da70SAndroid Build Coastguard WorkerIf retained instances are detected, LeakCanary will dump and analyze the heap. If application leaks 35*d9e8da70SAndroid Build Coastguard Workerare found, `LeakAssertions.assertNoLeak()` will throw a `NoLeakAssertionFailedError`. 36*d9e8da70SAndroid Build Coastguard Worker 37*d9e8da70SAndroid Build Coastguard Worker``` 38*d9e8da70SAndroid Build Coastguard Workerleakcanary.NoLeakAssertionFailedError: Application memory leaks were detected: 39*d9e8da70SAndroid Build Coastguard Worker==================================== 40*d9e8da70SAndroid Build Coastguard WorkerHEAP ANALYSIS RESULT 41*d9e8da70SAndroid Build Coastguard Worker==================================== 42*d9e8da70SAndroid Build Coastguard Worker1 APPLICATION LEAKS 43*d9e8da70SAndroid Build Coastguard Worker 44*d9e8da70SAndroid Build Coastguard Worker┬─── 45*d9e8da70SAndroid Build Coastguard Worker│ GC Root: System class 46*d9e8da70SAndroid Build Coastguard Worker│ 47*d9e8da70SAndroid Build Coastguard Worker├─ com.example.MySingleton class 48*d9e8da70SAndroid Build Coastguard Worker│ Leaking: NO (a class is never leaking) 49*d9e8da70SAndroid Build Coastguard Worker│ ↓ static MySingleton.leakedView 50*d9e8da70SAndroid Build Coastguard Worker│ ~~~~~~~~~~ 51*d9e8da70SAndroid Build Coastguard Worker├─ android.widget.TextView instance 52*d9e8da70SAndroid Build Coastguard Worker│ Leaking: YES (View.mContext references a destroyed activity) 53*d9e8da70SAndroid Build Coastguard Worker│ ↓ TextView.mContext 54*d9e8da70SAndroid Build Coastguard Worker╰→ com.example.MainActivity instance 55*d9e8da70SAndroid Build Coastguard Worker Leaking: YES (Activity#mDestroyed is true) 56*d9e8da70SAndroid Build Coastguard Worker==================================== 57*d9e8da70SAndroid Build Coastguard Worker at leakcanary.AndroidDetectLeaksAssert.assertNoLeaks(AndroidDetectLeaksAssert.kt:34) 58*d9e8da70SAndroid Build Coastguard Worker at leakcanary.LeakAssertions.assertNoLeaks(LeakAssertions.kt:21) 59*d9e8da70SAndroid Build Coastguard Worker at com.example.CartTest.addItemToCart(TuPeuxPasTest.kt:41) 60*d9e8da70SAndroid Build Coastguard Worker``` 61*d9e8da70SAndroid Build Coastguard Worker 62*d9e8da70SAndroid Build Coastguard Worker!!! bug "Obfuscated instrumentation tests" 63*d9e8da70SAndroid Build Coastguard Worker When running instrumentation tests against obfuscated release builds, the LeakCanary classes end 64*d9e8da70SAndroid Build Coastguard Worker up spread over the test APK and the main APK. Unfortunately there is a 65*d9e8da70SAndroid Build Coastguard Worker [bug](https://issuetracker.google.com/issues/126429384) in the Android Gradle Plugin that leads 66*d9e8da70SAndroid Build Coastguard Worker to runtime crashes when running tests, because code from the main APK is changed without the 67*d9e8da70SAndroid Build Coastguard Worker using code in the test APK being updated accordingly. If you run into this issue, setting up the 68*d9e8da70SAndroid Build Coastguard Worker [Keeper plugin](https://slackhq.github.io/keeper/) should fix it. 69*d9e8da70SAndroid Build Coastguard Worker 70*d9e8da70SAndroid Build Coastguard Worker 71*d9e8da70SAndroid Build Coastguard Worker## Test rule 72*d9e8da70SAndroid Build Coastguard Worker 73*d9e8da70SAndroid Build Coastguard Worker You can use the `DetectLeaksAfterTestSuccess` test rule to automatically call 74*d9e8da70SAndroid Build Coastguard Worker `LeakAssertions.assertNoLeak()` at the end of a test: 75*d9e8da70SAndroid Build Coastguard Worker 76*d9e8da70SAndroid Build Coastguard Worker ```kotlin 77*d9e8da70SAndroid Build Coastguard Worker class CartTest { 78*d9e8da70SAndroid Build Coastguard Worker @get:Rule 79*d9e8da70SAndroid Build Coastguard Worker val rule = DetectLeaksAfterTestSuccess() 80*d9e8da70SAndroid Build Coastguard Worker 81*d9e8da70SAndroid Build Coastguard Worker @Test 82*d9e8da70SAndroid Build Coastguard Worker fun addItemToCart() { 83*d9e8da70SAndroid Build Coastguard Worker // ... 84*d9e8da70SAndroid Build Coastguard Worker } 85*d9e8da70SAndroid Build Coastguard Worker } 86*d9e8da70SAndroid Build Coastguard Worker ``` 87*d9e8da70SAndroid Build Coastguard Worker 88*d9e8da70SAndroid Build Coastguard Worker You can call also `LeakAssertions.assertNoLeak()` as many times as you want in a single test: 89*d9e8da70SAndroid Build Coastguard Worker 90*d9e8da70SAndroid Build Coastguard Worker ```kotlin 91*d9e8da70SAndroid Build Coastguard Worker class CartTest { 92*d9e8da70SAndroid Build Coastguard Worker @get:Rule 93*d9e8da70SAndroid Build Coastguard Worker val rule = DetectLeaksAfterTestSuccess() 94*d9e8da70SAndroid Build Coastguard Worker 95*d9e8da70SAndroid Build Coastguard Worker // This test has 3 leak assertions (2 in the test + 1 from the rule). 96*d9e8da70SAndroid Build Coastguard Worker @Test 97*d9e8da70SAndroid Build Coastguard Worker fun addItemToCart() { 98*d9e8da70SAndroid Build Coastguard Worker // ... 99*d9e8da70SAndroid Build Coastguard Worker LeakAssertions.assertNoLeak() 100*d9e8da70SAndroid Build Coastguard Worker // ... 101*d9e8da70SAndroid Build Coastguard Worker LeakAssertions.assertNoLeak() 102*d9e8da70SAndroid Build Coastguard Worker // ... 103*d9e8da70SAndroid Build Coastguard Worker } 104*d9e8da70SAndroid Build Coastguard Worker } 105*d9e8da70SAndroid Build Coastguard Worker ``` 106*d9e8da70SAndroid Build Coastguard Worker 107*d9e8da70SAndroid Build Coastguard Worker## Skipping leak detection 108*d9e8da70SAndroid Build Coastguard Worker 109*d9e8da70SAndroid Build Coastguard WorkerUse `@SkipLeakDetection` to disable `LeakAssertions.assertNoLeak()` calls: 110*d9e8da70SAndroid Build Coastguard Worker 111*d9e8da70SAndroid Build Coastguard Worker ```kotlin 112*d9e8da70SAndroid Build Coastguard Worker class CartTest { 113*d9e8da70SAndroid Build Coastguard Worker @get:Rule 114*d9e8da70SAndroid Build Coastguard Worker val rule = DetectLeaksAfterTestSuccess() 115*d9e8da70SAndroid Build Coastguard Worker 116*d9e8da70SAndroid Build Coastguard Worker // This test will not perform any leak assertion. 117*d9e8da70SAndroid Build Coastguard Worker @SkipLeakDetection("See issue #1234") 118*d9e8da70SAndroid Build Coastguard Worker @Test 119*d9e8da70SAndroid Build Coastguard Worker fun addItemToCart() { 120*d9e8da70SAndroid Build Coastguard Worker // ... 121*d9e8da70SAndroid Build Coastguard Worker LeakAssertions.assertNoLeak() 122*d9e8da70SAndroid Build Coastguard Worker // ... 123*d9e8da70SAndroid Build Coastguard Worker LeakAssertions.assertNoLeak() 124*d9e8da70SAndroid Build Coastguard Worker // ... 125*d9e8da70SAndroid Build Coastguard Worker } 126*d9e8da70SAndroid Build Coastguard Worker } 127*d9e8da70SAndroid Build Coastguard Worker ``` 128*d9e8da70SAndroid Build Coastguard Worker 129*d9e8da70SAndroid Build Coastguard WorkerYou can use **tags** to identify each `LeakAssertions.assertNoLeak()` call and disable only a subset of these calls: 130*d9e8da70SAndroid Build Coastguard Worker 131*d9e8da70SAndroid Build Coastguard Worker ```kotlin 132*d9e8da70SAndroid Build Coastguard Worker class CartTest { 133*d9e8da70SAndroid Build Coastguard Worker @get:Rule 134*d9e8da70SAndroid Build Coastguard Worker val rule = DetectLeaksAfterTestSuccess(tag = "EndOfTest") 135*d9e8da70SAndroid Build Coastguard Worker 136*d9e8da70SAndroid Build Coastguard Worker // This test will only perform the second leak assertion. 137*d9e8da70SAndroid Build Coastguard Worker @SkipLeakDetection("See issue #1234", "First Assertion", "EndOfTest") 138*d9e8da70SAndroid Build Coastguard Worker @Test 139*d9e8da70SAndroid Build Coastguard Worker fun addItemToCart() { 140*d9e8da70SAndroid Build Coastguard Worker // ... 141*d9e8da70SAndroid Build Coastguard Worker LeakAssertions.assertNoLeak(tag = "First Assertion") 142*d9e8da70SAndroid Build Coastguard Worker // ... 143*d9e8da70SAndroid Build Coastguard Worker LeakAssertions.assertNoLeak(tag = "Second Assertion") 144*d9e8da70SAndroid Build Coastguard Worker // ... 145*d9e8da70SAndroid Build Coastguard Worker } 146*d9e8da70SAndroid Build Coastguard Worker } 147*d9e8da70SAndroid Build Coastguard Worker ``` 148*d9e8da70SAndroid Build Coastguard Worker 149*d9e8da70SAndroid Build Coastguard WorkerTags can be retrieved by calling `HeapAnalysisSuccess.assertionTag` and are also reported in the 150*d9e8da70SAndroid Build Coastguard Workerheap analysis result metadata: 151*d9e8da70SAndroid Build Coastguard Worker 152*d9e8da70SAndroid Build Coastguard Worker``` 153*d9e8da70SAndroid Build Coastguard Worker==================================== 154*d9e8da70SAndroid Build Coastguard WorkerMETADATA 155*d9e8da70SAndroid Build Coastguard Worker 156*d9e8da70SAndroid Build Coastguard WorkerPlease include this in bug reports and Stack Overflow questions. 157*d9e8da70SAndroid Build Coastguard Worker 158*d9e8da70SAndroid Build Coastguard WorkerBuild.VERSION.SDK_INT: 23 159*d9e8da70SAndroid Build Coastguard Worker... 160*d9e8da70SAndroid Build Coastguard WorkerassertionTag: Second Assertion 161*d9e8da70SAndroid Build Coastguard Worker``` 162*d9e8da70SAndroid Build Coastguard Worker 163*d9e8da70SAndroid Build Coastguard Worker## Test rule chains 164*d9e8da70SAndroid Build Coastguard Worker 165*d9e8da70SAndroid Build Coastguard Worker```kotlin 166*d9e8da70SAndroid Build Coastguard Worker// Example test rule chain 167*d9e8da70SAndroid Build Coastguard Worker@get:Rule 168*d9e8da70SAndroid Build Coastguard Workerval rule = RuleChain.outerRule(LoginRule()) 169*d9e8da70SAndroid Build Coastguard Worker .around(ActivityScenarioRule(CartActivity::class.java)) 170*d9e8da70SAndroid Build Coastguard Worker .around(LoadingScreenRule()) 171*d9e8da70SAndroid Build Coastguard Worker 172*d9e8da70SAndroid Build Coastguard Worker``` 173*d9e8da70SAndroid Build Coastguard Worker 174*d9e8da70SAndroid Build Coastguard WorkerIf you use a test rule chain, the position of the `DetectLeaksAfterTestSuccess` rule in that chain 175*d9e8da70SAndroid Build Coastguard Workercould be significant. For example, if you use an `ActivityScenarioRule` that automatically 176*d9e8da70SAndroid Build Coastguard Workerfinishes the activity at the end of a test, having `DetectLeaksAfterTestSuccess` around 177*d9e8da70SAndroid Build Coastguard Worker`ActivityScenarioRule` will detect leaks after the activity is destroyed and therefore detect any 178*d9e8da70SAndroid Build Coastguard Workeractivity leak. But then `DetectLeaksAfterTestSuccess` will not detect fragment leaks that go away 179*d9e8da70SAndroid Build Coastguard Workerwhen the activity is destroyed. 180*d9e8da70SAndroid Build Coastguard Worker 181*d9e8da70SAndroid Build Coastguard Worker```kotlin 182*d9e8da70SAndroid Build Coastguard Worker@get:Rule 183*d9e8da70SAndroid Build Coastguard Workerval rule = RuleChain.outerRule(LoginRule()) 184*d9e8da70SAndroid Build Coastguard Worker // Detect leaks AFTER activity is destroyed 185*d9e8da70SAndroid Build Coastguard Worker .around(DetectLeaksAfterTestSuccess(tag = "AfterActivityDestroyed")) 186*d9e8da70SAndroid Build Coastguard Worker .around(ActivityScenarioRule()) 187*d9e8da70SAndroid Build Coastguard Worker .around(LoadingScreenRule()) 188*d9e8da70SAndroid Build Coastguard Worker``` 189*d9e8da70SAndroid Build Coastguard Worker 190*d9e8da70SAndroid Build Coastguard WorkerIf instead you set up `ActivityScenarioRule` around `DetectLeaksAfterTestSuccess`, destroyed 191*d9e8da70SAndroid Build Coastguard Workeractivity leaks will not be detected as the activity will still be created when the leak assertion 192*d9e8da70SAndroid Build Coastguard Workerrule runs, but more fragment leaks might be detected. 193*d9e8da70SAndroid Build Coastguard Worker 194*d9e8da70SAndroid Build Coastguard Worker```kotlin 195*d9e8da70SAndroid Build Coastguard Worker@get:Rule 196*d9e8da70SAndroid Build Coastguard Workerval rule = RuleChain.outerRule(LoginRule()) 197*d9e8da70SAndroid Build Coastguard Worker .around(ActivityScenarioRule(CartActivity::class.java)) 198*d9e8da70SAndroid Build Coastguard Worker // Detect leaks BEFORE activity is destroyed 199*d9e8da70SAndroid Build Coastguard Worker .around(DetectLeaksAfterTestSuccess(tag = "BeforeActivityDestroyed")) 200*d9e8da70SAndroid Build Coastguard Worker .around(LoadingScreenRule()) 201*d9e8da70SAndroid Build Coastguard Worker``` 202*d9e8da70SAndroid Build Coastguard Worker 203*d9e8da70SAndroid Build Coastguard WorkerTo detect all leaks, the best option is to 204*d9e8da70SAndroid Build Coastguard Workerset up the `DetectLeaksAfterTestSuccess` rule twice, before and after the `ActivityScenarioRule` 205*d9e8da70SAndroid Build Coastguard Workerrule. 206*d9e8da70SAndroid Build Coastguard Worker 207*d9e8da70SAndroid Build Coastguard Worker```kotlin 208*d9e8da70SAndroid Build Coastguard Worker// Detect leaks BEFORE and AFTER activity is destroyed 209*d9e8da70SAndroid Build Coastguard Worker@get:Rule 210*d9e8da70SAndroid Build Coastguard Workerval rule = RuleChain.outerRule(LoginRule()) 211*d9e8da70SAndroid Build Coastguard Worker .around(DetectLeaksAfterTestSuccess(tag = "AfterActivityDestroyed")) 212*d9e8da70SAndroid Build Coastguard Worker .around(ActivityScenarioRule(CartActivity::class.java)) 213*d9e8da70SAndroid Build Coastguard Worker .around(DetectLeaksAfterTestSuccess(tag = "BeforeActivityDestroyed")) 214*d9e8da70SAndroid Build Coastguard Worker .around(LoadingScreenRule()) 215*d9e8da70SAndroid Build Coastguard Worker``` 216*d9e8da70SAndroid Build Coastguard Worker 217*d9e8da70SAndroid Build Coastguard Worker`RuleChain.detectLeaksAfterTestSuccessWrapping()` is a helper for doing just that: 218*d9e8da70SAndroid Build Coastguard Worker 219*d9e8da70SAndroid Build Coastguard Worker```kotlin 220*d9e8da70SAndroid Build Coastguard Worker// Detect leaks BEFORE and AFTER activity is destroyed 221*d9e8da70SAndroid Build Coastguard Worker@get:Rule 222*d9e8da70SAndroid Build Coastguard Workerval rule = RuleChain.outerRule(LoginRule()) 223*d9e8da70SAndroid Build Coastguard Worker // The tag will be suffixed with "Before" and "After". 224*d9e8da70SAndroid Build Coastguard Worker .detectLeaksAfterTestSuccessWrapping(tag = "ActivitiesDestroyed") { 225*d9e8da70SAndroid Build Coastguard Worker around(ActivityScenarioRule(CartActivity::class.java)) 226*d9e8da70SAndroid Build Coastguard Worker } 227*d9e8da70SAndroid Build Coastguard Worker .around(LoadingScreenRule()) 228*d9e8da70SAndroid Build Coastguard Worker``` 229*d9e8da70SAndroid Build Coastguard Worker 230*d9e8da70SAndroid Build Coastguard Worker## Customizing `assertNoLeak()` 231*d9e8da70SAndroid Build Coastguard Worker 232*d9e8da70SAndroid Build Coastguard Worker`LeakAssertions.assertNoLeak()` delegates calls to a global `DetectLeaksAssert` implementation, 233*d9e8da70SAndroid Build Coastguard Workerwhich by default is an instance of `AndroidDetectLeaksAssert`. You can change the 234*d9e8da70SAndroid Build Coastguard Worker`DetectLeaksAssert` implementation by calling `DetectLeaksAssert.update(customLeaksAssert)`. 235*d9e8da70SAndroid Build Coastguard Worker 236*d9e8da70SAndroid Build Coastguard WorkerThe `AndroidDetectLeaksAssert` implementation performs a heap dump when retained instances are 237*d9e8da70SAndroid Build Coastguard Workerdetected, analyzes the heap, then passes the result to a `HeapAnalysisReporter`. The default 238*d9e8da70SAndroid Build Coastguard Worker`HeapAnalysisReporter` is `NoLeakAssertionFailedError.throwOnApplicationLeaks()` which throws a 239*d9e8da70SAndroid Build Coastguard Worker`NoLeakAssertionFailedError` if an application leak is detected. 240*d9e8da70SAndroid Build Coastguard Worker 241*d9e8da70SAndroid Build Coastguard WorkerYou could provide a custom implementation to also upload heap analysis results to a central place 242*d9e8da70SAndroid Build Coastguard Workerbefore failing the test: 243*d9e8da70SAndroid Build Coastguard Worker```kotlin 244*d9e8da70SAndroid Build Coastguard Workerval throwingReporter = NoLeakAssertionFailedError.throwOnApplicationLeaks() 245*d9e8da70SAndroid Build Coastguard Worker 246*d9e8da70SAndroid Build Coastguard WorkerDetectLeaksAssert.update(AndroidDetectLeaksAssert( 247*d9e8da70SAndroid Build Coastguard Worker heapAnalysisReporter = { heapAnalysis -> 248*d9e8da70SAndroid Build Coastguard Worker // Upload the heap analysis result 249*d9e8da70SAndroid Build Coastguard Worker heapAnalysisUploader.upload(heapAnalysis) 250*d9e8da70SAndroid Build Coastguard Worker // Fail the test if there are application leaks 251*d9e8da70SAndroid Build Coastguard Worker throwingReporter.reportHeapAnalysis(heapAnalysis) 252*d9e8da70SAndroid Build Coastguard Worker } 253*d9e8da70SAndroid Build Coastguard Worker)) 254*d9e8da70SAndroid Build Coastguard Worker``` 255