Blog Infos
Author
Published
Topics
,
Published
Topics
,

Leak Canary logo from https://square.github.io/leakcanary/faq/

 

Memory leaks can cause performance issues and application crashes, making them important to detect and fix. However, if the project architecture is well-designed or experienced developers are involved, leaks will appear rarely. Even very rarely. Manual detection of leaks often yields no results and also requires a significant amount of time. I suggest integrating memory leak detection into UI tests to save time.

Let me introduce myself — my name is Danil Perevalov, and now let’s get to the topic.

Why UI tests?

We didn’t plan to use UI tests initially. Leaks were detected manually.

We use the LeakCanary library, like everyone else, to detect leaks.

Our initial plan was to enable LeakCanary in builds for developers and testers, they can create a bug report whenever they notice a leak.

But over time, we have discovered some problems:

  • LeakCanary negatively affects performance. Manual testing is already time-consuming, and we don’t want to make it even longer.
  • LeakCanary often takes too much time to create memory dumps.
  • Leaks are sometimes overlooked or intentionally ignored.

We needed to find a solution to address these issues. And when the manual approach doesn’t work, people prefer to use the automation. So, the most straightforward and logical solution was to integrate leak detection into UI tests.

First version

But… When we came up with the solution to integrate leak detection into UI tests, our company had just introduced native UI tests for Android. As a result, there were only a few of them and they only covered a limited number of user scenarios. This led us to make a determined decision — to write specialized UI tests specifically for leaks detection.

Specialized tests

The sequence of steps in such a test is simple:

  1. Open the application.
  2. Open a screen.
  3. Then another one.
  4. And another.
  5. Open as many screens as possible, in short.
  6. Close the screens. The test framework itself typically handles this task.
  7. Run a leak detection with LeakCanary.

The main objective of this test is to maximize the number of screens opened, after which all screens are closed and leak detection is started.

A little bit about LeakCanary

To begin, you must add dependency, initialize, and enable detection in LeakCanary. These steps are automatically handled by the integration of LeakCanary.

But there are problems:

  • Leak detection via LeakCanary is typically enabled only in UI tests. However, developers may want to enable it in the debug build. This is done to verify if a leak has been fixed.
  • It is preferable to have LeakCanary disabled by default, but it can be enabled when necessary.

There are factors such as leaks in the system, third-party libraries, and other elements that are beyond your control.

I recommend creating a Config to freely enable and disable leak detection. Also Config helps avoid constant test crashes caused by vendor issues. Such as Samsung’s keyboard manager leaking. This Config can be used to enable and disable leak detection and add a custom reference matcher if needed.

Integrating LeakCanary into tests

You can easily integrate LeakCanary into UI tests by simply including a specific code that runs when the test is completed.

LeakAssertions.assertNoLeaks()

After the test is completed, if you are using Espresso, you should start leak detection with LeakCanary in the method annotated with After.

@After
fun after() {
   // Launch leak detection
   LeakAssertions.assertNoLeaks()
}

Our company uses Kaspresso, and we have created a wrapper around its Rule to incorporate our own code in the init and after sections for all tests. Here is an example of how we have implemented this:

class LeakKaspressoRule(
   testClassName: String
) : TestRule {

   val kaspressoRule = KaspressoRule(testClassName)

   override fun apply(base: Statement, description: Description): Statement {
       return kaspressoRule.apply(base, description)
   }

   fun before(actions: BaseTestContext.() -> Unit) = After(kaspressoRule.before {
       // own code
       actions(this)
   })

   class After(
       private val after: AfterTestSection<Unit, Unit>
   ) {

       fun after(actions: BaseTestContext.() -> Unit) = Init(after.after {
           // own code
           actions(this)
       })
   }

   class Init(
       private val init: InitSection<Unit, Unit>
   ) {

       fun run(steps: TestContext<Unit>.() -> Unit) = init.run {
           steps(this)
           // own code
       }
   }
}

Add leak detection to the After Kaspresso class.

after.after {
   LeakAssertions.assertNoLeaks()
   actions(this)
}

And overall, that’s it. Let’s look at an example of such a test.

class LeakAuthUiTest {

    @get:Rule
    val leakKaspressoRule = LeakKaspressoRule(javaClass.simpleName)

    @Test
    fun testLeakOnAuth() {
        leakKaspressoRule.before {
        }.after {
        }.run {
            step("Open user profile") { ... }
            step("Open auth")  { ... }
            step("Open registration")  { ... }
            step("Open restore password")  { ... }
        }
    }
}

As described above, everything occurs by simply opening screens and running a memory leak detection inside LeakKaspressoRule once the test is finished.

Let’s now discuss launching and providing support for these tests.

Launch and support

Our memory leak tests were launched when we needed it, which was not often. Usually at most once a month.

However, this approach generally proved effective. We would write tests that would open a stack of screens based on a specific scenario. We run the specialized tests, and periodically discover leaks. Consequently, tasks were initiated to address these leaks, and the best part was that it did not demand excessive time from developers or testers.

But… The problem of supporting such tests arose over time. This was due to the constant changes in the application and the need to adjust the tests accordingly. Especially when they affect multiple screens.

Job Offers

Job Offers

There are currently no vacancies.

OUR VIDEO RECOMMENDATION

,

Blast Off_ Managing Hundreds of UI Updates for Emoji Cannons

Managing a state might be a challenge. Managing the state with hundreds of updates and constant recomposition of floating emojis is a challenge indeed.
Watch Video

Blast Off_ Managing Hundreds of UI Updates for Emoji Cannons

Piotr Prus
Android developer

Blast Off_ Managing Hundreds of UI Updates for Emoji Cannons

Piotr Prus
Android developer

Blast Off_ Managing Hundreds of UI Updates for Emoji Cannons

Piotr Prus
Android developer

Jobs

Developers unintentionally broke the tests for detecting memory leaks without realizing it. Since the tests were not consistently launched. As a result, we had to periodically create tasks to fix these tests. Writing new tests of this nature was not desirable for anyone, as they required painful maintenance. This situation persisted for a while.

It is important to note that manual detection of leaks required significantly more effort compared to this method.

We decided it was time for a change.

Second version

The number of regular UI-tests had exceeded a hundred by that time. Covering a much larger number of screens and user scenarios than those specialized for leak detection.

We decided to try doing things differently because we didn’t need the specialized tests anymore.

New concept

Instead of including specialized tests for the leak detection in the project, we will add an option to run leak detection at the end of each test.

Importantly, this is only an option. Adding the leak detection to the test significantly increases the testing time. It is important for these tests to be able to run quickly. Because, we run UI tests before merging a feature branch with the main branch in Git.

On our CI, we implemented the following logic:

  • When UI tests are run before the merge, they are run normally without leak detection. Only the tests related to the changed code in the Git branch are executed.
  • All UI tests with leak detection are run if they are executed before the Release Candidate build.

How it works

To implement this behavior easily, one can add a flag and pass it as an argument to the TestRunner. If the flag `isLeakTest` is set to true, it indicates that the tests should be run in the leak detection mode.

The value of the flag is read from the TestRunner arguments and then written to a static variable.

class CianUiTestRunner : AllureAndroidJUnitRunner() {

   override fun onCreate(arguments: Bundle) {
       IS_LEAK_TEST = arguments.getString("isLeakTest") == "true"
   }
}

In the After annotated method for Espresso, we only run a leak detection from LeakCanary if the `IS_LEAK_TEST` flag is true.

@After
fun after() {
   if (IS_LEAK_TEST) {
       // Launch leak detection
       LeakAssertions.assertNoLeaks()
   }
}

Implementing such logic in Kaspresso is not too difficult.

after.after {
   if (IS_LEAK_TEST) {
       LeakAssertions.assertNoLeaks()
   }
   actions(this)
}

That’s it. The improvements may be small, especially when not considering the hundreds of tests written before.

What in the end?

The scheme works as we have achieved fully automated leak detection during Release Candidate builds, resulting in the identification of bugs to fix leaks. Minimal developer involvement is necessary for leak detection, except for the task of reporting the detected leaks.

We are currently satisfied with it.

Both options have their own benefits, in fact.

  • The first option is easy to integrate, but difficult to support. Is suitable when there is a low coverage of UI-tests.
  • The second option is difficult to integrate, but easy to support. Is suitable when there is a high coverage of UI-tests. Which not everyone possesses, and it is not always essential.

This article was previously published on proandroiddev.com

YOU MAY BE INTERESTED IN

YOU MAY BE INTERESTED IN

blog
Automation is a key point of Software Testing once it make possible to reproduce…
READ MORE
blog
Every good Android application should be well tested to minimize the risk of error…
READ MORE
blog
In this part of the series, we will plan our first screen in Jetpack…
READ MORE
blog
We’ll be selecting a time whenever a user presses a number key. Following points…
READ MORE

Leave a Reply

Your email address will not be published. Required fields are marked *

Fill out this field
Fill out this field
Please enter a valid email address.

Menu