Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support AppComponentFactory or ignore BroadcastReceivers with custom constructors #7581

Closed
ZacSweers opened this issue Sep 7, 2022 · 11 comments · Fixed by #8004
Closed

Support AppComponentFactory or ignore BroadcastReceivers with custom constructors #7581

ZacSweers opened this issue Sep 7, 2022 · 11 comments · Fixed by #8004

Comments

@ZacSweers
Copy link
Contributor

Description

When using Android 28+'s AppComponentFactory, it becomes possible to constructor-inject BroadcastReceiver instance. However, Robolectric seems to assume that these must all have empty constructors and tries to instantiate them all at test init.

I think Robolectric should ignore these or offer a way to handle them.

Steps to Reproduce

Add a custom receiver with a non-empty constructor on the classpath, then try to run any test using RobolectricTestRunner. Observe a trace like this

java.lang.NoSuchMethodException: slack.browser.chrome.ChromeTabBroadcastReceiver.<init>()
java.lang.RuntimeException: java.lang.NoSuchMethodException: slack.browser.chrome.ChromeTabBroadcastReceiver.<init>()
	at org.robolectric.util.ReflectionHelpers.callConstructor(ReflectionHelpers.java:434)
	at org.robolectric.internal.bytecode.ShadowImpl.newInstanceOf(ShadowImpl.java:18)
	at org.robolectric.shadow.api.Shadow.newInstanceOf(Shadow.java:35)
	at org.robolectric.android.internal.AndroidTestEnvironment.registerBroadcastReceivers(AndroidTestEnvironment.java:675)
	at org.robolectric.android.internal.AndroidTestEnvironment.installAndCreateApplication(AndroidTestEnvironment.java:339)
	at org.robolectric.android.internal.AndroidTestEnvironment.lambda$createApplicationSupplier$0(AndroidTestEnvironment.java:229)
	at org.robolectric.util.PerfStatsCollector.measure(PerfStatsCollector.java:53)
	at org.robolectric.android.internal.AndroidTestEnvironment.lambda$createApplicationSupplier$1(AndroidTestEnvironment.java:226)
	at com.google.common.base.Suppliers$NonSerializableMemoizingSupplier.get(Suppliers.java:183)
	at org.robolectric.RuntimeEnvironment.getApplication(RuntimeEnvironment.java:71)
	at org.robolectric.android.internal.AndroidTestEnvironment.setUpApplicationState(AndroidTestEnvironment.java:194)
	at org.robolectric.RobolectricTestRunner.beforeTest(RobolectricTestRunner.java:325)
	at org.robolectric.internal.SandboxTestRunner$2.lambda$evaluate$0(SandboxTestRunner.java:265)
	at org.robolectric.internal.bytecode.Sandbox.lambda$runOnMainThread$0(Sandbox.java:88)
	at java.base/java.util.concurrent.FutureTask.run(FutureTask.java:264)
	at java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1136)
	at java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:635)
	at java.base/java.lang.Thread.run(Thread.java:833)
Caused by: java.lang.NoSuchMethodException: slack.browser.chrome.ChromeTabBroadcastReceiver.<init>()
	at java.base/java.lang.Class.getConstructor0(Class.java:3585)
	at java.base/java.lang.Class.getDeclaredConstructor(Class.java:2754)
	at org.robolectric.util.ReflectionHelpers.callConstructor(ReflectionHelpers.java:420)
	... 17 more

Robolectric & Android Version

4.8.2, android API 28

Link to a public git repo demonstrating the problem:

@ZacSweers
Copy link
Contributor Author

Note that trying to set @Config(manifest = Config.NONE) doesn't appear to work around it either

@erhiteshkumar
Copy link

I am also facing the same issue.

@utzcoz
Copy link
Member

utzcoz commented Dec 10, 2022

@ZacSweers @erhiteshkumar Thanks for reporting this issue. Although Robolectric can get parameter types list of constructor, there are some problems can not be processed easily:

  1. Which constructor should be used when creating instance for a class if it has multiple constructors?
  2. Which value should be passed for specific type when calling the constructor with input parameters?
  3. It's more complicated for question 2 if input class type is a custom class.

I don't have a great idea how to fix this issue or provide potential method from Robolectric perspective, but I can share what I did before when integrating Robolectric and encountering a similar problem: using annotation processor to add empty constructor for testing code. There is an example that I wrote to try to fix a similar issue: https://github.com/utzcoz/robolectric-with-junitparams/blob/master/junitparamsprocessor/src/main/java/com/utzcoz/robolectric/junitparamsprocessor/RobolectricJUnitParamsProcessor.java#L74-L85.

@ZacSweers
Copy link
Contributor Author

Is there any reason it couldn't just use appcomponentfactory if available and otherwise refuse to load any receivers that don't have no-arg constructors available?

@utzcoz
Copy link
Member

utzcoz commented Dec 11, 2022

if available and otherwise refuse to load any receivers that don't have no-arg constructors available?

@ZacSweers I sent a PR(#7860) to skip initialization for instances that don't have empty constructor. Could you help to check whether it can fix your problem with local maven snapshot building? Thanks.

@ZacSweers
Copy link
Contributor Author

I'm on vacation until Jan 8th but I can check again that week 👍

@utzcoz
Copy link
Member

utzcoz commented Feb 11, 2023

Hi @ZacSweers , I try a custom receiver with a non-empty constructor, and the AppComponentFactory also throws an exception to me when running it on Emulator:

Process: com.demo.myapplication, PID: 6868
java.lang.RuntimeException: Unable to instantiate receiver com.demo.myapplication.CustomConstructorReceiver: java.lang.InstantiationException: java.lang.Class<com.demo.myapplication.CustomConstructorReceiver> has no zero argument constructor
    at android.app.ActivityThread.handleReceiver(ActivityThread.java:4292)
    at android.app.ActivityThread.-$$Nest$mhandleReceiver(Unknown Source:0)
    at android.app.ActivityThread$H.handleMessage(ActivityThread.java:2153)
    at android.os.Handler.dispatchMessage(Handler.java:106)
    at android.os.Looper.loopOnce(Looper.java:201)
    at android.os.Looper.loop(Looper.java:288)
    at android.app.ActivityThread.main(ActivityThread.java:7872)
    at java.lang.reflect.Method.invoke(Native Method)
    at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:548)
    at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:936)
Caused by: java.lang.InstantiationException: java.lang.Class<com.demo.myapplication.CustomConstructorReceiver> has no zero argument constructor
    at java.lang.Class.newInstance(Native Method)
    at android.app.AppComponentFactory.instantiateReceiver(AppComponentFactory.java:110)
    at androidx.core.app.CoreComponentFactory.instantiateReceiver(CoreComponentFactory.java:60)
    at android.app.ActivityThread.handleReceiver(ActivityThread.java:4285)
    at android.app.ActivityThread.-$$Nest$mhandleReceiver(Unknown Source:0) 
    at android.app.ActivityThread$H.handleMessage(ActivityThread.java:2153) 
    at android.os.Handler.dispatchMessage(Handler.java:106) 
    at android.os.Looper.loopOnce(Looper.java:201) 
    at android.os.Looper.loop(Looper.java:288) 
    at android.app.ActivityThread.main(ActivityThread.java:7872) 
    at java.lang.reflect.Method.invoke(Native Method) 
    at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:548) 
    at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:936) 

I also check AppComponentFactory code, and it also assumes that the receiver instance should have a constructor without parameters.

When using Android 28+'s AppComponentFactory, it becomes possible to constructor-inject BroadcastReceiver instance.

Could you provide a sample with this scenario? I can validate its behavior and check whether there are something that Robolectric can do. The previous solution doesn't look like great.

If you use injection tool likes hilt, what about using these tools in Robolectric test environment too, e.g. https://dagger.dev/hilt/robolectric-testing.html?

@ZacSweers
Copy link
Contributor Author

ZacSweers commented Feb 12, 2023

You should be implementing your own AppComponentFactory that can instantiate those classes and pointing your manifest's android:appComponentFactory at it. The default behavior assumes no-arg constructors, but custom ones can provide ways of instantiating ones with arguments.

Not a broadcast receiver, but similar use with activities: https://github.com/slackhq/circuit/blob/main/samples/star/src/main/kotlin/com/slack/circuit/star/di/StarAppComponentFactory.kt

@utzcoz
Copy link
Member

utzcoz commented Feb 12, 2023

@ZacSweers Thanks for providing this information. Looks like the ideal solution is to support AppComponentFactory in Robolectric likes: https://cs.android.com/android/platform/superproject/+/master:frameworks/base/core/java/android/app/ActivityThread.java;l=4284-4285?q=ActivityThread&ss=android.

copybara-service bot pushed a commit that referenced this issue Apr 6, 2023
It does not appear to be necessary any more. The receiver name can be used
diretly.

Related to #7581

PiperOrigin-RevId: 522107957
copybara-service bot pushed a commit that referenced this issue Apr 6, 2023
It does not appear to be necessary any more. The receiver name can be used
diretly.

Related to #7581

PiperOrigin-RevId: 522204982
@utzcoz
Copy link
Member

utzcoz commented Apr 8, 2023

@ZacSweers The fix will be released in 4.10 stable release. Please help to try it and send feedback to us when it is released. Thanks.

@ZacSweers
Copy link
Contributor Author

Awesome!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging a pull request may close this issue.

3 participants