# 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. !!! info "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](recipes.md#leakcanary-test-environment-detection)). ## Getting started LeakCanary provides an artifact dedicated to detecting leaks in UI tests: ``` androidTestImplementation "com.squareup.leakcanary:leakcanary-android-instrumentation:${leakCanaryVersion}" ``` You can then call `LeakAssertions.assertNoLeak()` at any point in your tests to check for leaks: ```kotlin class CartTest { @Test fun addItemToCart() { // ... LeakAssertions.assertNoLeak() } } ``` If retained instances are detected, LeakCanary will dump and analyze the heap. If application leaks are found, `LeakAssertions.assertNoLeak()` 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) ``` !!! bug "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](https://issuetracker.google.com/issues/126429384) 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](https://slackhq.github.io/keeper/) should fix it. ## Test rule You can use the `DetectLeaksAfterTestSuccess` test rule to automatically call `LeakAssertions.assertNoLeak()` at the end of a test: ```kotlin class CartTest { @get:Rule val rule = DetectLeaksAfterTestSuccess() @Test fun addItemToCart() { // ... } } ``` You can call also `LeakAssertions.assertNoLeak()` as many times as you want in a single test: ```kotlin 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.assertNoLeak() // ... LeakAssertions.assertNoLeak() // ... } } ``` ## Skipping leak detection Use `@SkipLeakDetection` to disable `LeakAssertions.assertNoLeak()` calls: ```kotlin class CartTest { @get:Rule val rule = DetectLeaksAfterTestSuccess() // This test will not perform any leak assertion. @SkipLeakDetection("See issue #1234") @Test fun addItemToCart() { // ... LeakAssertions.assertNoLeak() // ... LeakAssertions.assertNoLeak() // ... } } ``` You can use **tags** to identify each `LeakAssertions.assertNoLeak()` call and disable only a subset of these calls: ```kotlin 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 ```kotlin // 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. ```kotlin @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. ```kotlin @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. ```kotlin // 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: ```kotlin // 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 `assertNoLeak()` `LeakAssertions.assertNoLeak()` 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: ```kotlin 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) } )) ```