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