square / leakcanary

A memory leak detection library for Android.
https://square.github.io/leakcanary
Apache License 2.0
29.39k stars 3.97k forks source link

LeakCanary in Unit Test Artifact #143

Closed linggom closed 8 years ago

linggom commented 9 years ago

Hi.

I got NPE when i run test using Unit Test build variants. Do you have any best practice to do this ?

java.lang.NullPointerException
    at com.squareup.leakcanary.internal.LeakCanaryInternals.isInServiceProcess(LeakCanaryInternals.java:130)
    at com.squareup.leakcanary.LeakCanary.isInAnalyzerProcess(LeakCanary.java:146)
    at com.squareup.leakcanary.LeakCanary.install(LeakCanary.java:48)
    at com.squareup.leakcanary.LeakCanary.install(LeakCanary.java:37)
    at com.vidio.android.VidioApplication.onCreate(VidioApplication.java:18)
    at org.robolectric.internal.ParallelUniverse.setUpApplicationState(ParallelUniverse.java:131)
    at org.robolectric.RobolectricTestRunner.setUpApplicationState(RobolectricTestRunner.java:431)
    at org.robolectric.RobolectricTestRunner$2.evaluate(RobolectricTestRunner.java:224)
    at org.junit.runners.ParentRunner.runLeaf(ParentRunner.java:325)
    at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:78)
    at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:57)
    at org.junit.runners.ParentRunner$3.run(ParentRunner.java:290)
    at org.junit.runners.ParentRunner$1.schedule(ParentRunner.java:71)
    at org.junit.runners.ParentRunner.runChildren(ParentRunner.java:288)
    at org.junit.runners.ParentRunner.access$000(ParentRunner.java:58)
    at org.junit.runners.ParentRunner$2.evaluate(ParentRunner.java:268)
    at org.robolectric.RobolectricTestRunner$1.evaluate(RobolectricTestRunner.java:168)
    at org.junit.runners.ParentRunner.run(ParentRunner.java:363)
    at org.gradle.api.internal.tasks.testing.junit.JUnitTestClassExecuter.runTestClass(JUnitTestClassExecuter.java:86)
    at org.gradle.api.internal.tasks.testing.junit.JUnitTestClassExecuter.execute(JUnitTestClassExecuter.java:49)
    at org.gradle.api.internal.tasks.testing.junit.JUnitTestClassProcessor.processTestClass(JUnitTestClassProcessor.java:69)
    at org.gradle.api.internal.tasks.testing.SuiteTestClassProcessor.processTestClass(SuiteTestClassProcessor.java:48)
    at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
    at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
    at java.lang.reflect.Method.invoke(Method.java:497)
    at org.gradle.messaging.dispatch.ReflectionDispatch.dispatch(ReflectionDispatch.java:35)
    at org.gradle.messaging.dispatch.ReflectionDispatch.dispatch(ReflectionDispatch.java:24)
    at org.gradle.messaging.dispatch.ContextClassLoaderDispatch.dispatch(ContextClassLoaderDispatch.java:32)
    at org.gradle.messaging.dispatch.ProxyDispatchAdapter$DispatchingInvocationHandler.invoke(ProxyDispatchAdapter.java:93)
    at com.sun.proxy.$Proxy2.processTestClass(Unknown Source)
    at org.gradle.api.internal.tasks.testing.worker.TestWorker.processTestClass(TestWorker.java:105)
    at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
    at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
    at java.lang.reflect.Method.invoke(Method.java:497)
    at org.gradle.messaging.dispatch.ReflectionDispatch.dispatch(ReflectionDispatch.java:35)
    at org.gradle.messaging.dispatch.ReflectionDispatch.dispatch(ReflectionDispatch.java:24)
    at org.gradle.messaging.remote.internal.hub.MessageHub$Handler.run(MessageHub.java:360)
    at org.gradle.internal.concurrent.DefaultExecutorFactory$StoppableExecutorImpl$1.run(DefaultExecutorFactory.java:64)
    at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1142)
    at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:617)
    at java.lang.Thread.run(Thread.java:745)

Is it caused by multiple flavour on release ?

pyricau commented 9 years ago

No. You just need to read the code to figure it out. It's because you're using Robolectric, which doesn't set a processName on the serviceInfo returned by PackageManager.getServiceInfo().

Instead, you can let your application class have a method that can be overridden in unit tests:

public class VideoApplication extends Application {
  public void onCreate() {
    super.onCreate();
    if (!isInUnitTests()) {
      LeakCanary.install(this);
    }
  }

  protected boolean isInUnitTests() {
    return false;
  }
}

Then you just need to create a TestVideoApplication in your test code and the same package, and Robolectric will automatically use that instead:

public class TestVideoApplication extends VideoApplication {
    protected boolean isInUnitTests() {
    return true;
  }
}
pyricau commented 9 years ago

TODO: Update README to mention Robolectric

romainpiel commented 9 years ago

You can also disable leakCanary for unit tests:

testCompile "com.squareup.leakcanary:leakcanary-android-no-op:${leakCanaryVersion}"
ygnessin commented 9 years ago

If your tests run in debug mode like mine, to disable LeakCanary you'll need your testCompile declaration to be above debugCompile:

// WORKS
androidTestCompile 'com.squareup.leakcanary:leakcanary-android-no-op:1.3.1'
debugCompile 'com.squareup.leakcanary:leakcanary-android:1.3.1'

// DOESN'T WORK
debugCompile 'com.squareup.leakcanary:leakcanary-android:1.3.1'
androidTestCompile 'com.squareup.leakcanary:leakcanary-android-no-op:1.3.1'

Ran into this issue today so thought I'd share in case anyone else is confused

ylogx commented 8 years ago

Propose to close based on PR above

pyricau commented 8 years ago

@ygnessin @shubhamchaudhary I don't recall: which one works by default on a new Android project, androidTestCompile or testCompile? I'd like the README to mention the most standard one.

ygnessin commented 8 years ago

@pyricau I think testCompile is for unit tests, which is probably the closest thing to "default" there is. I use androidTestCompile for the test API that I wrote which runs on a device; probably less common. Sorry for the confusion!

pyricau commented 8 years ago

thx!

jaredsburrows commented 8 years ago

@ygnessin This works for me in all of my projects:

debugCompile 'com.squareup.leakcanary:leakcanary-android:1.3.1' // We only want leakcanary in debug builds

dogfoodCompile 'com.squareup.leakcanary:leakcanary-android-no-op:1.3.1' // no-op

releaseCompile 'com.squareup.leakcanary:leakcanary-android-no-op:1.3.1' // no-op

testCompile 'com.squareup.leakcanary:leakcanary-android-no-op:1.3.1' // no-op
pyricau commented 8 years ago

The readme now has this:

 dependencies {
   debugCompile 'com.squareup.leakcanary:leakcanary-android:1.4'
   releaseCompile 'com.squareup.leakcanary:leakcanary-android-no-op:1.4'
   testCompile 'com.squareup.leakcanary:leakcanary-android-no-op:1.4'
 }

So we can close this issue.

caarmen commented 6 years ago

I'm seeing this NPE now in unit tests, that I've updated my environment (android gradle plugin 3.0.0)

I have this in my gradle file:

    debugImplementation 'com.squareup.leakcanary:leakcanary-android:1.5.4'
    releaseImplementation 'com.squareup.leakcanary:leakcanary-android-no-op:1.5.4'
    testImplementation 'com.squareup.leakcanary:leakcanary-android-no-op:1.5.4'
    androidTestImplementation 'com.squareup.leakcanary:leakcanary-android-no-op:1.5.4'

I tried changing the order of these statements, but so far haven't had luck getting rid of the NPE in unit tests, by trying to use the no-op version of the lib for tests.

Note: the same issue occurs if i keep the old testCompile syntax instead of the new testImplementation.

Note: tests run fine from within Android Studio. The error occurs running tests on the command line with ./gradlew clean testDebugUnitTest

Update: ok, if I RTFM and create a test application class, and override the setupLeakCanary() as described, the tests now work on the command line. :)

However, it would be nice to just not use leak canary in tests by using the no-op version of the lib.

Kisty commented 6 years ago

As per @JakeWharton's #907 PR, it suggests to do the following without subclassing Application (win!):

Taken from README.md

To disable LeakCanary in unit tests, add the following to your build.gradle:

// Ensure the no-op dependency is always used in JVM tests.
configurations.all { config ->
  if (config.name.contains('UnitTest')) {
    config.resolutionStrategy.eachDependency { details ->
      if (details.requested.group == 'com.squareup.leakcanary' && details.requested.name == 'leakcanary-android') {
        details.useTarget(group: details.requested.group, name: 'leakcanary-android-no-op', version: details.requested.version)
      }
    }
  }
}
AndreSand commented 6 years ago

@Kisty Thanks for the answer! it works :)

Gloix commented 6 years ago

Adding @JakeWharton's suggestion still causes NPEs in my setup. I added the config lines after the dependencies block in my app module's build.gradle, as well as adding the no-op version directly using testImplementation. Using Gradle 4.6, Leak Canary 1.5.4, Android Studio 3.1.2 on High Sierra and Robolectric 3.8.

java.lang.NullPointerException
    at com.squareup.leakcanary.internal.LeakCanaryInternals.isInServiceProcess(LeakCanaryInternals.java:112)
    at com.squareup.leakcanary.LeakCanary.isInAnalyzerProcess(LeakCanary.java:145)
    ...
Kisty commented 6 years ago

@Gloix can you post your dependency configuration with just the leak canary dependencies + the snippet Jake posted? Perhaps somethings in the wrong order.

Gloix commented 6 years ago

@Kisty Nevermind. After a couple of rebuilds it is working now :D, but thanks anyway.

Gloix commented 6 years ago

@Kisty I don't know what happened yesterday but my setup failed again, but this time I went deeper and pinpointed the set of possible configurations my test is running on:

prodDebugCompile, prodDebugApk, prodDebugProvided, prodDebugApi, prodDebugImplementation, prodDebugRuntimeOnly, prodDebugCompileOnly, prodDebugWearApp, prodDebugAnnotationProcessor, prodDebugCompileClasspath, prodDebugAnnotationProcessorClasspath, prodDebugRuntimeClasspath, prodDebugWearBundling, prodDebugBundleElements, prodDebugRuntimeElements, prodDebugApiElements, prodDebugMetadataElements

none of them contains the substring AndroidTest or UnitTest, but all of them contain the substring prodDebug. It seems my tests are not running with a test-like gradle config and I'm kind of lost as to what I can do to tell Android Studio to run my tests using a test-like gradle config.

kschults commented 6 years ago

@Gloix Were you ever able to resolve this? Adding the configurations block fixed it for me, but only for running from the command line. If I run tests from within the IDE, it still doesn't use the no-op artifact, and the tests fail.

Gloix commented 6 years ago

@kschults I never tried running it through the command line. I fixed it by killing leakcanary until I need it again.

kschults commented 6 years ago

@Kisty Here's a shortened version of my build.gradle. This version works fine when running from the command line ./gradlew :app:test<build type + flavor>UnitTest, but not when running from the buttons in the IDE.

buildscript { ... }
configurations.all { config ->
    // Ensure the no-op leak canary dependency is always used in tests.
    if (config.name.contains('UnitTest') || config.name.contains("AndroidTest")) {
        config.resolutionStrategy.eachDependency { details ->
            if (details.requested.group == 'com.squareup.leakcanary' && details.requested.name == 'leakcanary-android') {
                details.useTarget(group: details.requested.group, name: 'leakcanary-android-no-op', version: details.requested.version)
            }
        }
    }
}

...

android { ... }
dependencies {
...
    testImplementation 'com.squareup.leakcanary:leakcanary-android-no-op:1.5.4'
    debugImplementation 'com.squareup.leakcanary:leakcanary-android:1.5.4'
    qaImplementation 'com.squareup.leakcanary:leakcanary-android-no-op:1.5.4'
    releaseImplementation 'com.squareup.leakcanary:leakcanary-android-no-op:1.5.4'
}

Overriding my application in test works, but it would definitely be better if I could just use the no-op version. I've looked at the dependency graph generated by ./gradlew :app:dependences, and it looks like it should be using the no-op version, but when I run it, it still uses the full artifact.

Kisty commented 6 years ago

Try swapping the dependency line for test and debug so that the test dependency comes after debug. I wonder if the debug line is overwriting the test dependency. It's worth a try.

kschults commented 6 years ago

No luck. Same issue

Kisty commented 6 years ago

Ok, how about trying the Canary build of Android Studio (download and install separately http://d.android.com/studio/preview). I know the team are aware of unit test issues in Android Studio 3.1 with Robolectric. Do you have Robolectric in unit tests?

kschults commented 6 years ago

Yeah, it's Robolectric. I'll give that a try (probably tomorrow) and get back to you

kschults commented 6 years ago

@Kisty Unfortunately, I'm getting the same results on the Canary build (3.2 Beta 1)

IgorGanapolsky commented 6 years ago

It is still the same issue in Android Studio 3.3 Canary 5

mikehc commented 5 years ago

@Kisty Here's a shortened version of my build.gradle. This version works fine when running from the command line ./gradlew :app:test<build type + flavor>UnitTest, but not when running from the buttons in the IDE.

buildscript { ... }
configurations.all { config ->
    // Ensure the no-op leak canary dependency is always used in tests.
    if (config.name.contains('UnitTest') || config.name.contains("AndroidTest")) {
        config.resolutionStrategy.eachDependency { details ->
            if (details.requested.group == 'com.squareup.leakcanary' && details.requested.name == 'leakcanary-android') {
                details.useTarget(group: details.requested.group, name: 'leakcanary-android-no-op', version: details.requested.version)
            }
        }
    }
}

...

android { ... }
dependencies {
...
    testImplementation 'com.squareup.leakcanary:leakcanary-android-no-op:1.5.4'
    debugImplementation 'com.squareup.leakcanary:leakcanary-android:1.5.4'
    qaImplementation 'com.squareup.leakcanary:leakcanary-android-no-op:1.5.4'
    releaseImplementation 'com.squareup.leakcanary:leakcanary-android-no-op:1.5.4'
}

Overriding my application in test works, but it would definitely be better if I could just use the no-op version. I've looked at the dependency graph generated by ./gradlew :app:dependences, and it looks like it should be using the no-op version, but when I run it, it still uses the full artifact.

Having the same issues when upgrading from version 1.6.1. When I use 1.6.2 or 1.6.3 I get the NPE, and when debugging seems to be using the full artifact and not the no-op ones. This happens using Robolectric 4.0.1 and 4.2.1 (latests available to date).

Kisty commented 5 years ago

OK, perhaps it's favouring the debug artefact over the test one for testDebug. Have you tried:

testDebugImplementation 'com.squareup.leakcanary:leakcanary-android-no-op:1.5.4'

mikehc commented 5 years ago

@Kisty same result.

I ran the test in console with ./gradlew test and they run just fine but not on Android Studio. If I downgrade to version 1.6.1 they can run on Android Studio too.

Kisty commented 5 years ago

@mikehc Sounds like the classpath resolution is different on AS from Gradle. Tried editing the run config so it uses JAR manifest or something else?

Another few things to try from Android Studio:

  1. Build->Clean Project then Build->Make project (try doing two separate actions because I've had files locked and thus doesn't do a complete clean)
  2. File->Invalidate cache and restart
mikehc commented 5 years ago

Tried editing the run config so it uses JAR manifest

@Kisty Can you give me some pointers of how to do that?

Kisty commented 5 years ago

@mikehc

  1. Click dropdown for run list and select Edit configurations run-config-1
  2. Find unit test run task in popup
  3. Ensure that JRE is selected to Java 1.8 JRE not Android API...
  4. Select an option from Shorten command line and try running again. run-config-2
renetik commented 2 years ago

I was wrong probably :)