问题
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