[当必须使用父级活动参考时,如何使用`FragmentScenario`隔离测试片段

问题描述 投票:2回答:1

我计划使用FragmentScenario隔离测试片段。在此片段中,我正在访问父活动以调用某些方法。为此,我正在使用CommonActivityOperations界面。我在此应用程序中使用导航体系结构组件。

interface CommonActivityOperations {
    fun closeSearchBar()
    fun navigateBackWithResult(resultFor: String, result: Bundle)
}

[当我尝试在AttractionDestinationFragment上运行测试时,出现以下错误。

java.lang.ClassCastException: androidx.fragment.app.testing.FragmentScenario$EmptyFragmentActivity cannot be cast to .interfaces.CommonActivityOperations

AttractionDestinationFragment

const val DESTINATION_DATA_EXTRA = "destination_data_result"

class AttractionDestinationFragment : BaseFragment(), ItemClickListener, ToolbarSearchChangeListener{

    private lateinit var navigationController: NavController
    private lateinit var defaultAttractionList: MutableList<AttractionDestinations>
    private lateinit var attractionDestinationRecyclerViewAdapter: AttractionDestinationRecyclerViewAdapter

    private val viewModel by lazy { getViewModel<AttractionDestinationViewModel>() }

    override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?,
                              savedInstanceState: Bundle?): View? {
        return inflater.inflate(R.layout.fragment_attraction_destination, container, false)
    }

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {

        initialization(view)
        observeAttractionDestinations()
    }

    private fun initialization(view: View) {

        navigationController = Navigation.findNavController(view)

        defaultAttractionList = mutableListOf(...)

        attractionDestinationRecyclerViewAdapter =
                AttractionDestinationRecyclerViewAdapter(defaultAttractionList)
        attractionDestinationRecyclerViewAdapter.itemClickListener = this
        attractionDestinationRecyclerView.addItemDecoration(SimpleLineDividerItemDecoration(context))
        attractionDestinationRecyclerView.adapter = attractionDestinationRecyclerViewAdapter

        activity?.invalidateOptionsMenu()

    }

    private fun observeAttractionDestinations() {

        viewModel.attractionDestinationsLiveData.observe(viewLifecycleOwner, Observer {

            it.getContentIfNotHandled()?.also { resource ->

                when (resource.status) {

                    DataRetrieveStatus.SUCCESS -> {
                        // apply new data
                    }

                    DataRetrieveStatus.RESPONSE_ERROR -> {
                        //Show error
                    }

                    DataRetrieveStatus.CONNECTION_ERROR,
                    DataRetrieveStatus.PROCESSING_ERROR -> {
                        //error handling
                    }
                }
            }
        })
    }

    override fun onItemClick(position: Int) {

        val bundle = Bundle()
        bundle.putParcelable(DESTINATION_DATA_EXTRA, attractionDestinationRecyclerViewAdapter.data[position])

        activity?.invalidateOptionsMenu()

        commonActivityOperations.navigateBackWithResult(DESTINATION_DATA_RESULT, bundle)
        commonActivityOperations.closeSearchBar()
    }

    override fun onQueryChange(text: String) {

        if(networkConnectivity.isOnline){

            viewModel.getAttractionDestinations(text)

        }else{
            // Error handling
        }
    }

    override fun onSearchBarClose() {
        // Reset List
    }

BaseFragment

abstract class BaseFragment : Fragment() {

    private lateinit var windowFocusChangeReceiver: BroadcastReceiver

    protected var dialogToDisplayOnWindowIsFocus: MaterialDialog.Builder? = null

    lateinit var commonActivityOperations: CommonActivityOperations

    val networkConnectivity: NetworkStatus by lazy { NetworkStatus(context) }

    override fun onAttach(context: Context) {
        commonActivityOperations = activity as CommonActivityOperations
        super.onAttach(context)
    }
}

AttractionDestinationFragmentTest

class AttractionDestinationFragmentTest {

    private val mockNavController by lazy {
        Mockito.mock(NavController::class.java)
    }

    @Before
    fun setUp() {

        MockitoAnnotations.initMocks(this)

        val fragment = AttractionDestinationFragment()

        launchFragmentInContainer(themeResId = R.style.AppTheme){
            fragment.also { fragment ->
                fragment.viewLifecycleOwnerLiveData.observeForever { viewLifecycleOwner ->
                    if (viewLifecycleOwner != null) {
                        Navigation.setViewNavController(fragment.requireView(), mockNavController)
                    }
                }
            }
        }

    }

    @Test
    fun destinationRecyclerViewLoadTest(){

        onView(withId(R.id.attractionDestinationRecyclerView))
                .check(RecyclerViewItemCountAssertion(15))
                .perform(actionOnItemAtPosition<AttractionDestinationRecyclerViewAdapter.ViewHolder>(0, click()))
    }

    @After
    fun tearDown() {
    }

如您所见,我已经在CommonActivityOperations内部设置了onAttach(context: Context)接口在BaseFragment中。我收到此错误消息是因为FragmentScenario创建了未实现EmptyFragmentActivityCommonActivityOperations,并且我正在尝试启动它。

有没有其他方法可以启动此界面?如何使此片段可测试?

谢谢。

编辑:我经历了以下类似的question

此问题未能解决我遇到的问题,因为我正在使用导航体系结构组件。使用导航体系结构组件时,我不会手动创建片段来将接口传递给片段,如本问题所示。

AttractionFlowActivity

private fun initialization() {

        navigationController = Navigation.findNavController(this, R.id.hostFragment)
        setSupportActionBar(toolbar)

        val appBarConfiguration = AppBarConfiguration
                .Builder()
                .setFallbackOnNavigateUpListener(this)
                .build()

        NavigationUI.setupActionBarWithNavController(this, navigationController, appBarConfiguration)

    }

上面的代码显示了父活动内部的初始化函数,该初始化函数负责设置导航体系结构组件并使用工具栏对其进行设置。

android android-fragments android-espresso android-testing
1个回答
0
投票

我无法找到一种方法来使用此接口,而无需引用导航体系结构组件的活动。我最终要做的是使用ReactiveX。我创建了一个类,该类在值更改时使用PublishSubject通知活动。

CommonActivityTrigger

class CommonActivityTrigger private constructor() {

    val navigateBackResultPublishSubject: PublishSubject<Pair<String,Bundle>>
            = PublishSubject.create()
    val closeSearchBarPublishSubject: PublishSubject<Boolean>
            = PublishSubject.create()

    private object HOLDER {
        val INSTANCE = CommonActivityTrigger()
    }

    companion object {
        val INSTANCE: CommonActivityTrigger by lazy { HOLDER.INSTANCE }
    }

    fun navigateBackWithResult(resultFor: String, result: Bundle){
        navigateBackResultPublishSubject.onNext(Pair(resultFor,result))
    }

    fun closeSearchBar(){
        closeSearchBarPublishSubject.onNext(true)
    }
}

AttractionDestinationFragment

class AttractionDestinationFragment : BaseFragment(), ItemClickListener, ToolbarSearchChangeListener{

    private lateinit var defaultAttractionList: MutableList<AttractionDestinations>
    private lateinit var attractionDestinationRecyclerViewAdapter: AttractionDestinationRecyclerViewAdapter

    private val viewModel by lazy { getViewModel<AttractionDestinationViewModel>() }
    private val commonActivityTrigger by lazy { CommonActivityTrigger.INSTANCE }

    override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?,
                              savedInstanceState: Bundle?): View? {
        return inflater.inflate(R.layout.fragment_attraction_destination, container, false)
    }

    override fun onItemClick(position: Int) {

        val bundle = Bundle()
        bundle.putParcelable(DESTINATION_DATA_EXTRA, attractionDestinationRecyclerViewAdapter.data[position])

        activity?.invalidateOptionsMenu()

        commonActivityTrigger.navigateBackWithResult(DESTINATION_DATA_RESULT, bundle)
        commonActivityTrigger.closeSearchBar()
    }
}

AttractionFlow1Activity

class AttractionFlow1Activity : AppCompatActivity(), AppBarConfiguration.OnNavigateUpListener {

    private lateinit var navigationController: NavController

    private val navHostFragment by lazy { supportFragmentManager.findFragmentById(R.id.hostFragment) }
    private val commonActivityTrigger by lazy { CommonActivityTrigger.INSTANCE }

    private val compositeDisposable = CompositeDisposable()

    private fun navigateBackWithResult() {

        val navigationBlackResultPublishDisposable = commonActivityTrigger
                .navigateBackResultPublishSubject
                .subscribe {

                    val (resultFor, result) = it

                    val childFragmentManager = supportFragmentManager
                            .findFragmentById(R.id.hostFragment)?.childFragmentManager
                    var backStackListener: FragmentManager.OnBackStackChangedListener by Delegates.notNull()
                    backStackListener = FragmentManager.OnBackStackChangedListener {
                        (childFragmentManager?.fragments?.get(0) as NavigationResult).onNavigationResult(resultFor, result)
                        childFragmentManager.removeOnBackStackChangedListener(backStackListener)
                    }
                    childFragmentManager?.addOnBackStackChangedListener(backStackListener)
                    navigationController.popBackStack()
                }

        compositeDisposable.add(navigationBlackResultPublishDisposable)

    }

    override fun onDestroy() {
        compositeDisposable.dispose()
        super.onDestroy()
    }
}
© www.soinside.com 2019 - 2024. All rights reserved.