xref: /aosp_15_r20/external/leakcanary2/docs/ui-tests.md (revision d9e8da70d8c9df9a41d7848ae506fb3115cae6e6)
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
4647├─ 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