airbnb / mavericks

Mavericks: Android on Autopilot
https://airbnb.io/mavericks/
Apache License 2.0
5.83k stars 500 forks source link

NullPointerException during unit test - BaseMvRxViewModel.execute - parameter $this$execute #513

Open eboudrant opened 3 years ago

eboudrant commented 3 years ago

Since we migrated from MvRx 1.5 to Mavericks 2.0 we observe intermittent failures in our unit test. We are still using the mavericks-rxjava library. I just wanted to open an issue here in case someone observed the same behavior.

We're using robolectric 4.5-alpha-3 and mockito 3.8.0.

java.lang.NullPointerException: Parameter specified as non-null is null: method com.airbnb.mvrx.BaseMvRxViewModel.execute, parameter $this$execute
    at com.airbnb.mvrx.BaseMvRxViewModel.execute(BaseMvRxViewModel.kt)
    at com.package.OurViewModel.submit(OurViewModel.kt:64)
    at com.pachage.OurViewModelTest.testMethod(OurViewModelTest.kt:183)
    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:498)
    at org.junit.runners.model.FrameworkMethod$1.runReflectiveCall(FrameworkMethod.java:59)
    at org.junit.internal.runners.model.ReflectiveCallable.run(ReflectiveCallable.java:12)
    at org.junit.runners.model.FrameworkMethod.invokeExplosively(FrameworkMethod.java:56)
    at org.junit.internal.runners.statements.InvokeMethod.evaluate(InvokeMethod.java:17)
    at org.junit.internal.runners.statements.RunBefores.evaluate(RunBefores.java:26)
    at org.junit.rules.ExternalResource$1.evaluate(ExternalResource.java:54)
    at dagger.hilt.android.internal.testing.MarkThatRulesRanRule$1.evaluate(MarkThatRulesRanRule.java:92)
    at org.junit.runners.ParentRunner$3.evaluate(ParentRunner.java:306)
    at org.robolectric.RobolectricTestRunner$HelperTestRunner$1.evaluate(RobolectricTestRunner.java:575)
    at org.robolectric.internal.SandboxTestRunner$2.lambda$evaluate$0(SandboxTestRunner.java:263)
    at org.robolectric.internal.bytecode.Sandbox.lambda$runOnMainThread$0(Sandbox.java:89)
    at java.util.concurrent.FutureTask.run(FutureTask.java:266)
    at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149)
    at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624)
    at java.lang.Thread.run(Thread.java:748)

The code where it happen : OurViewModel.kt

    fun submit() {
        agent
            .submitData()
L64         .execute {
                copy(shouldShowError = it is Fail, submission = it)
            }
    }

In the test class we use the MvRxTestRule but I suspect some processes are still running async causing theses issues in Mockito. Ex, some of the .execute { run on this thread during testing : Thread[pool-5-thread-2 @coroutine#7,5,SDK 21]

And we have theses warnings:

If you're unsure why you're getting above error read on.
Due to the nature of the syntax above problem might occur because:
1. This exception *might* occur in wrongly written multi-threaded tests.
   Please refer to Mockito FAQ on limitations of concurrency testing.
2. A spy is stubbed using when(spy.foo()).then() syntax. It is safer to stub spies - 
   - with doReturn|Throw() family of methods. More in javadocs for Mockito.spy() method.

We are still investigating...

elihart commented 3 years ago

Thanks for flagging, possibly the coroutines are not configured correctly for tests? Could you share how you are using MvRxTestRule?

eboudrant commented 3 years ago

The only thing we are doing with the test rule is declaring it at the top of the class :

@get:Rule
val mvRxTestRule = MvRxTestRule()
elihart commented 3 years ago

If you look in the src of MvRxTestRule you'll see it is partially a helper for initializing Mavericks

    private fun setupMocking() {
        val mocksEnabled = viewModelMockBehavior != null
        // Use a null context since we don't need mock printing during tests
        MockableMavericks.initialize(debugMode = debugMode, mocksEnabled = mocksEnabled, applicationContext = null)

        if (viewModelMockBehavior != null) {
            MockableMavericks.mockConfigFactory.mockBehavior = viewModelMockBehavior
        }
    }

However, this does not set any coroutine overrides for the options of subscriptionCoroutineContextOverride stateStoreCoroutineContext or viewModelCoroutineContext on MockableMavericks.initialize

You may want to play around with setting these to either the testDispatcher: CoroutineDispatcher = TestCoroutineDispatcher() in the test rule, or to Dispatchers.Unconfined.

Particularly stateStoreCoroutineContext may be needed to be set to a controlled dispatcher for your case, since otherwise it uses a private thread pool. The other contexts should default to the main thread.