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