Fragment Unit Testing: launchFragment throws ClassCastException

半城伤御伤魂 提交于 2020-03-23 11:55:04

问题


I'm trying to call methods within my Fragment class in my unit test but I keep getting the error java.lang.ClassCastException: androidx.fragment.app.testing.FragmentScenario$EmptyFragmentActivity cannot be cast to com.nu.rms.inspections.ui.activity.InspectionActivity

I'm following Google's docs. I'm confused why the empty fragment activity is having a cast attempted to become my InspectionActivity (the parent activity the fragment resides in), perhaps this is expected?

What can I do to alleviate the CastClassException and use my fragment's methods within my unit test? (related question that doesn't solve my issue)

Test

@RunWith(AndroidJUnit4::class)
class ExampleUnitTest {
    @Test
    fun `inspection failure point to location mapping is correct`() {
        val scenario = launchFragment<ContentFragment>()
        scenario.onFragment { fragment ->
            //TODO: test logic
        }
    }
...
}

Fragment Class

import androidx.fragment.app.Fragment
...
class ContentFragment : Fragment() {...}

Stack Trace

java.lang.ClassCastException: androidx.fragment.app.testing.FragmentScenario$EmptyFragmentActivity cannot be cast to com.nu.rms.inspections.ui.activity.InspectionActivity
    at com.nu.rms.inspections.ui.fragment.ContentFragment.clearRFIDCache(ContentFragment.kt:565)
    at com.nu.rms.inspections.ui.fragment.ContentFragment.showStep1(ContentFragment.kt:222)
    at com.nu.rms.inspections.ui.fragment.ContentFragment.access$showStep1(ContentFragment.kt:37)
    at com.nu.rms.inspections.ui.fragment.ContentFragment$onViewCreated$1.onChanged(ContentFragment.kt:77)
    at com.nu.rms.inspections.ui.fragment.ContentFragment$onViewCreated$1.onChanged(ContentFragment.kt:37)
    at androidx.lifecycle.LiveData.considerNotify(LiveData.java:113)
    at androidx.lifecycle.LiveData.dispatchingValue(LiveData.java:131)
    at androidx.lifecycle.LiveData.setValue(LiveData.java:289)
    at androidx.lifecycle.MutableLiveData.setValue(MutableLiveData.java:33)
    at androidx.lifecycle.Transformations$2$1.onChanged(Transformations.java:153)
    at androidx.lifecycle.MediatorLiveData$Source.onChanged(MediatorLiveData.java:152)
    at androidx.lifecycle.LiveData.considerNotify(LiveData.java:113)
    at androidx.lifecycle.LiveData.dispatchingValue(LiveData.java:126)
    at androidx.lifecycle.LiveData$ObserverWrapper.activeStateChanged(LiveData.java:424)
    at androidx.lifecycle.LiveData.observeForever(LiveData.java:214)
    at androidx.lifecycle.MediatorLiveData$Source.plug(MediatorLiveData.java:141)
    at androidx.lifecycle.MediatorLiveData.addSource(MediatorLiveData.java:96)
    at androidx.lifecycle.Transformations$2.onChanged(Transformations.java:150)
    at androidx.lifecycle.MediatorLiveData$Source.onChanged(MediatorLiveData.java:152)
    at androidx.lifecycle.LiveData.considerNotify(LiveData.java:113)
    at androidx.lifecycle.LiveData.dispatchingValue(LiveData.java:126)
    at androidx.lifecycle.LiveData$ObserverWrapper.activeStateChanged(LiveData.java:424)
    at androidx.lifecycle.LiveData.observeForever(LiveData.java:214)
    at androidx.lifecycle.MediatorLiveData$Source.plug(MediatorLiveData.java:141)
    at androidx.lifecycle.MediatorLiveData.onActive(MediatorLiveData.java:118)
    at androidx.lifecycle.LiveData$ObserverWrapper.activeStateChanged(LiveData.java:418)
    at androidx.lifecycle.LiveData$LifecycleBoundObserver.onStateChanged(LiveData.java:376)
    at androidx.lifecycle.LifecycleRegistry$ObserverWithState.dispatchEvent(LifecycleRegistry.java:361)
    at androidx.lifecycle.LifecycleRegistry.forwardPass(LifecycleRegistry.java:300)
    at androidx.lifecycle.LifecycleRegistry.sync(LifecycleRegistry.java:339)
    at androidx.lifecycle.LifecycleRegistry.moveToState(LifecycleRegistry.java:145)
    at androidx.lifecycle.LifecycleRegistry.handleLifecycleEvent(LifecycleRegistry.java:131)
    at androidx.fragment.app.FragmentViewLifecycleOwner.handleLifecycleEvent(FragmentViewLifecycleOwner.java:51)
    at androidx.fragment.app.Fragment.performStart(Fragment.java:2639)
    at androidx.fragment.app.FragmentManagerImpl.moveToState(FragmentManagerImpl.java:915)
    at androidx.fragment.app.FragmentManagerImpl.moveFragmentToExpectedState(FragmentManagerImpl.java:1238)
    at androidx.fragment.app.FragmentManagerImpl.moveToState(FragmentManagerImpl.java:1303)
    at androidx.fragment.app.BackStackRecord.executeOps(BackStackRecord.java:439)
    at androidx.fragment.app.FragmentManagerImpl.executeOps(FragmentManagerImpl.java:2079)
    at androidx.fragment.app.FragmentManagerImpl.executeOpsTogether(FragmentManagerImpl.java:1869)
    at androidx.fragment.app.FragmentManagerImpl.removeRedundantOperationsAndExecute(FragmentManagerImpl.java:1824)
    at androidx.fragment.app.FragmentManagerImpl.execSingleAction(FragmentManagerImpl.java:1696)
    at androidx.fragment.app.BackStackRecord.commitNow(BackStackRecord.java:293)
    at androidx.fragment.app.testing.FragmentScenario$1.perform(FragmentScenario.java:312)
    at androidx.fragment.app.testing.FragmentScenario$1.perform(FragmentScenario.java:291)
    at androidx.test.core.app.ActivityScenario.lambda$onActivity$1$ActivityScenario(ActivityScenario.java:534)
    at androidx.test.core.app.ActivityScenario$$Lambda$0.run(Unknown Source)
    at org.robolectric.android.fakes.RoboMonitoringInstrumentation.runOnMainSync(RoboMonitoringInstrumentation.java:53)
    at androidx.test.core.app.ActivityScenario.onActivity(ActivityScenario.java:527)
    at androidx.fragment.app.testing.FragmentScenario.internalLaunch(FragmentScenario.java:290)
    at androidx.fragment.app.testing.FragmentScenario.launch(FragmentScenario.java:203)
    at com.nu.rms.inspections.ExampleUnitTest.inspection failure point to location mapping is correct(ExampleUnitTest.kt:56)
    at org.junit.runners.model.FrameworkMethod$1.runReflectiveCall(FrameworkMethod.java:50)
    at org.junit.internal.runners.model.ReflectiveCallable.run(ReflectiveCallable.java:12)
    at org.junit.runners.model.FrameworkMethod.invokeExplosively(FrameworkMethod.java:47)
    at org.junit.internal.runners.statements.InvokeMethod.evaluate(InvokeMethod.java:17)
    at org.robolectric.RobolectricTestRunner$HelperTestRunner$1.evaluate(RobolectricTestRunner.java:600)
    at org.robolectric.internal.SandboxTestRunner$2.evaluate(SandboxTestRunner.java:260)
    at org.robolectric.internal.SandboxTestRunner.runChild(SandboxTestRunner.java:130)
    at org.robolectric.internal.SandboxTestRunner.runChild(SandboxTestRunner.java:42)
    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.internal.SandboxTestRunner$1.evaluate(SandboxTestRunner.java:84)
    at org.junit.runners.ParentRunner.run(ParentRunner.java:363)
    at androidx.test.runner.AndroidJUnit4.run(AndroidJUnit4.java:104)
    at org.junit.runner.JUnitCore.run(JUnitCore.java:137)
    at com.intellij.junit4.JUnit4IdeaTestRunner.startRunnerWithArgs(JUnit4IdeaTestRunner.java:68)
    at com.intellij.rt.execution.junit.IdeaTestRunner$Repeater.startRunnerWithArgs(IdeaTestRunner.java:47)
    at com.intellij.rt.execution.junit.JUnitStarter.prepareStreamsAndStart(JUnitStarter.java:242)
    at com.intellij.rt.execution.junit.JUnitStarter.main(JUnitStarter.java:70)

回答1:


FragmentScenario adds your Fragment to an empty activity class - the androidx.fragment.app.testing.FragmentScenario$EmptyFragmentActivity mentioned in the stack trace.

That means that your Fragment is not in an instance of your InspectionActivity class. You're getting a crash because your clearRFIDCache() method is casting the activity to InspectionActivity.

If you want to need to test your Fragment within a specific instance of Activity and have a strong coupling between the two, you need to use ActivityScenario and manually add your Fragment to that Activity, rather than using FragmentScenario that gives you no control over the activity class you're using.

Ideally, you shouldn't tightly couple your Fragment to your Activity. For example, you can provide a FragmentFactory that uses constructor injection to add the interface your Fragment requires, rather than your Fragment reaching up to the Activity to call methods directly as discussed in the Fragments: Past, Present, and Future talk:

// Create an interface for what methods you want to expose
interface Inspector {
  // whatever methods you want
}

// Change your Fragment to take in that interface
class ContentFragment(val inspector: Inspector) : Fragment() {
    fun clearRFIDCache() {
        // Now you can call methods on inspector here
        // without casting your Activity
    }
}

private class InspectionActivityFactory(
    inspector: Inspector
) : FragmentFactory() {
    override fun instantiate(
        classLoader: ClassLoader,
        className: String
    ) = when (className) {
        ContentFragment::class.java.name -> ContentFragment(inspector)
        else -> super.instantiate(classLoader, className)
    }
}

// Now update your InspectionActivity to implement the interface
// and pass itself into an instance of the FragmentFactory you created
class InspectionActivity : AppCompatActivity(), Inspector {
    override fun onCreate(savedInstanceState: Bundle?) {
        supportFragmentManager.fragmentFactory =
            InspectionActivityFactory(this)
        super.onCreate(savedInstanceState)
        ...
    }
}

launchFragment takes a factory parameter that allows you to inject a test interface, ensuring that you can check that you get the callbacks you expect without relying on a specific subclass of your activity. When using Kotlin, you can also use the trailing lambda syntax to construct the Fragment and skip manually creating a Factory at all:

@RunWith(AndroidJUnit4::class)
class ExampleUnitTest {
    @Test
    fun `inspection failure point to location mapping is correct`() {
        val inspector = mock(Inspector::class.java)
        val scenario = launchFragment {
            ContentFragment(inspector)
        }
        scenario.onFragment { fragment ->
            //TODO: test logic
        }
    }
...
}


来源:https://stackoverflow.com/questions/58651075/fragment-unit-testing-launchfragment-throws-classcastexception

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!