FragmentScenario 必须实现 OnFragmentInteractionListener

FragmentScenario must implement OnFragmentInteractionListener

我目前正在尝试使用 androidTest、mokito 和 espresso 测试 android 中的导航,如本教程中所建议的那样: https://developer.android.com/guide/navigation/navigation-testing 但是我系统地收到以下错误: E/MonitoringInstr:Thread[main,5,main] 遇到异常。将线程状态转储到峡湾的输出和固定。 java.lang.RuntimeException: androidx.fragment.app.testing.FragmentScenario$EmptyFragmentActivity@fa3e8be 必须实现 OnFragmentInteractionListener

这里是测试class:

package developer.android.com.enlightme

import androidx.fragment.app.testing.launchFragmentInContainer
import androidx.navigation.NavController
import androidx.navigation.Navigation
import androidx.test.espresso.Espresso.onView
import androidx.test.espresso.action.ViewActions
import androidx.test.espresso.matcher.ViewMatchers
import androidx.test.espresso.matcher.ViewMatchers.withId
import androidx.test.filters.MediumTest
import androidx.test.runner.AndroidJUnit4
import org.junit.Assert.*
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.Mockito.*


@MediumTest
@RunWith(AndroidJUnit4::class)
class MainFragmentTest{
    @Test
    fun createDebateTransition(){
        // Create a mock NavController
        val mockNavController = mock(NavController::class.java)
        // Create a graphical FragmentScenario for the TitleScreen
        var mainScenario = launchFragmentInContainer<MainFragment>()
        // Set the NavController property on the fragment
        mainScenario.onFragment { fragment ->  Navigation.setViewNavController(fragment.requireView(), mockNavController)
        }
        // Verify that performing a click "créer" prompt the correct Navigation action
        onView(ViewMatchers.withId(R.id.nav_button_creer)).perform(ViewActions.click())
        verify(mockNavController).navigate(R.id.action_mainFragment_to_create1Fragment)
    }
  }
}

这是我的片段

package developer.android.com.enlightme

import android.content.Context
import android.net.Uri
import android.os.Bundle
import androidx.fragment.app.Fragment
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.databinding.DataBindingUtil
import androidx.navigation.findNavController


/**
 * A simple [Fragment] subclass.
 * Activities that contain this fragment must implement the
 * [MainFragment.OnFragmentInteractionListener] interface
 * to handle interaction events.
 * Use the [MainFragment.newInstance] factory method to
 * create an instance of this fragment.
 *
 */
class MainFragment : Fragment() {
    private var listener: OnFragmentInteractionListener? = null

    override fun onCreateView(
        inflater: LayoutInflater, container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View? {
        // Inflate the layout for this fragment
        val binding = DataBindingUtil.inflate<developer.android.com.enlightme.databinding.FragmentMainBinding>(inflater, R.layout.fragment_main, container, false)
        setHasOptionsMenu(true)
        //Click listener to create fragment
        binding.navButtonCreer.setOnClickListener { view : View ->
            view.findNavController().navigate(R.id.action_mainFragment_to_create1Fragment)
        }
        binding.navButtonRejoindre.setOnClickListener{view : View ->
            view.findNavController().navigate(R.id.action_mainFragment_to_joinDebateFragment)
        }
        return binding.root
    }
    fun onButtonPressed(uri: Uri) {
        listener?.onFragmentInteraction(uri)
    }

    override fun onAttach(context: Context) {
        super.onAttach(context)
        if (context is OnFragmentInteractionListener) {
            listener = context
        } else {
            throw RuntimeException(context.toString() + " must implement OnFragmentInteractionListener")
        }
    }

    override fun onDetach() {
        super.onDetach()
        listener = null
    }

    /**
     * This interface must be implemented by activities that contain this
     * fragment to allow an interaction in this fragment to be communicated
     * to the activity and potentially other fragments contained in that
     * activity.
     *
     *
     * See the Android Training lesson [Communicating with Other Fragments]
     * (http://developer.android.com/training/basics/fragments/communicating.html)
     * for more information.
     */
    interface OnFragmentInteractionListener {
        fun onFragmentInteraction(uri: Uri)
    }

    companion object {
        /**
         * Use this factory method to create a new instance of
         * this fragment using the provided parameters.
         *
         * @return A new instance of fragment MainFragment.
         */
        @JvmStatic
        fun newInstance(param1: String, param2: String) =
            MainFragment().apply {
                arguments = Bundle().apply {
                }
            }
    }
}

和 mainAcrivity 文件:

package developer.android.com.enlightme

import android.net.Uri
import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import androidx.databinding.DataBindingUtil
import androidx.fragment.app.DialogFragment
import androidx.lifecycle.ViewModelProviders
import androidx.navigation.findNavController
import androidx.navigation.ui.NavigationUI
import developer.android.com.enlightme.Debate.*
import developer.android.com.enlightme.Debate.ConcurentOp.InsertArg
import developer.android.com.enlightme.databinding.ActivityMainBinding
import developer.android.com.enlightme.objects.DebateEntity

class MainActivity : AppCompatActivity(), MainFragment.OnFragmentInteractionListener,
    Create1Fragment.OnFragmentInteractionListener,
    Create2Fragment.OnFragmentInteractionListener,
    DebateFragment.OnFragmentInteractionListener,
    ArgumentPlusSide1Fragment.OnFragmentInteractionListener,
    ArgumentPlusSide2Fragment.OnFragmentInteractionListener,
    ArgumentSide1Fragment.OnFragmentInteractionListener,
    ArgumentSide2Fragment.OnFragmentInteractionListener,
    NewArgDialogFragment.OnFragmentInteractionListener,
    JoinDebateFragment.OnFragmentInteractionListener,
    ItemBtListFragment.OnFragmentInteractionListener,
    ProvideUserNameFragment.OnFragmentInteractionListener,
    NewArgDialogFragment.NoticeDialogListener,
    ProvideUserNameFragment.NoticeDialogListener{

    private lateinit var binding: ActivityMainBinding
    private lateinit var debateViewModel: DebateViewModel
    private lateinit var joinDebateViewModel: JoinDebateViewModel
    lateinit var debateFragment: DebateFragment

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        binding = DataBindingUtil.setContentView(this, R.layout.activity_main)
        val navController = this.findNavController(R.id.myNavHostFragment)
        NavigationUI.setupActionBarWithNavController(this, navController)
        debateViewModel = this.run {
            ViewModelProviders.of(this).get(DebateViewModel::class.java)
        }
        joinDebateViewModel = this.run {
            ViewModelProviders.of(this).get(JoinDebateViewModel::class.java)
        }
    }
    override fun onSupportNavigateUp(): Boolean {
        val navController = this.findNavController(R.id.myNavHostFragment)
        return navController.navigateUp()
    }
    override fun onFragmentInteraction(uri: Uri) {
    }
    // The dialog fragment receives a reference to this Activity through the
    // Fragment.onAttach() callback, which it uses to call the following methods
    // defined by the NoticeDialogFragment.NoticeDialogListener interface
    override fun onDialogPositiveClick(dialog: DialogFragment) {
        if(debateViewModel.edit_arg_pos >= 0){
            debateFragment.modArgument(debateViewModel.temp_side,
                debateViewModel.temp_debate_entity, debateViewModel.edit_arg_pos)
        }else{
            val place : Int
            if(debateViewModel.temp_side == 1){
                place = debateViewModel.debate.value?.get_debate_entity()?.side_1_entity?.size ?: -1
            }else{
                place = debateViewModel.debate.value?.get_debate_entity()?.side_2_entity?.size ?: -1
            }
            val currDebate = this.debateViewModel.debate.value?.get_debate_entity()
            if (currDebate != null){
                val operation = InsertArg(debateViewModel.temp_debate_entity, place, debateViewModel.temp_side)
                debateViewModel.debate.value?.manageUserUpdate(listOf(operation), this,
                    joinDebateViewModel.listEndpointId, joinDebateViewModel.myEndpointId, currDebate.path_to_root)
            }

        }
        debateViewModel.temp_side = 0
        debateViewModel.temp_debate_entity = DebateEntity()
    }
    override fun onDialogNegativeClick(dialog: DialogFragment) {
        // User touched the dialog's negative button
    }
}

和模块 gradle 文件:

apply plugin: 'com.android.application'

apply plugin: 'kotlin-android'
apply plugin: 'maven'
apply plugin: 'kotlin-android-extensions'
apply plugin: "androidx.navigation.safeargs"
apply plugin: "kotlinx-serialization"

android {
    compileSdkVersion 28
    dataBinding {
        enabled = true
    }
    defaultConfig {
        applicationId "developer.android.com.enlightme"
        minSdkVersion 18
        targetSdkVersion 28
        versionCode 1
        versionName "1.0"
        testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
        vectorDrawables.useSupportLibrary = true
        android.defaultConfig.vectorDrawables.useSupportLibrary = true
    }
    buildTypes {
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
        }
    }
    compileOptions {
        sourceCompatibility JavaVersion.VERSION_1_8
        targetCompatibility JavaVersion.VERSION_1_8
    }
    buildToolsVersion = '28.0.3'
    kotlinOptions {
        jvmTarget = JavaVersion.VERSION_1_8.toString()
    }
}

dependencies {
    implementation fileTree(dir: 'libs', include: ['*.jar'])
    implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
    implementation 'androidx.appcompat:appcompat:1.2.0-alpha01'
    implementation 'androidx.core:core-ktx:1.2.0-rc01'
    implementation 'androidx.constraintlayout:constraintlayout:2.0.0-beta4'
    implementation 'androidx.legacy:legacy-support-v4:1.0.0'

    //ViewModel
    implementation 'androidx.lifecycle:lifecycle-extensions:2.2.0-rc03'
    //MaterialIO
    implementation 'com.google.android.material:material:1.2.0-alpha03'

    // Navigation

    // Java
    implementation "androidx.navigation:navigation-fragment:$navigationVersion"
    implementation "androidx.navigation:navigation-ui:$navigationVersion"

    // Kotlin
    implementation "androidx.navigation:navigation-fragment-ktx:$navigationVersion"
    implementation "androidx.navigation:navigation-ui-ktx:$navigationVersion"

    //P2P with Nearby
    implementation "com.google.android.gms:play-services-nearby:17.0.0"

    //Serialization
    implementation "org.jetbrains.kotlinx:kotlinx-serialization-runtime:0.13.0" // JVM dependency

    //Testing
    // Required -- JUnit 4 framework
    testImplementation 'junit:junit:4.13-rc-2'
    // Optional -- Robolectric environment
    testImplementation 'androidx.test:core:1.2.0'
    // Optional -- Mockito framework
    //testImplementation 'org.mockito:mockito-core:1.10.19'
    //androidTestImplementation "org.mockito:mockito-core:${var}"
    //espresso
    androidTestImplementation 'androidx.test.espresso:espresso-core:3.1.0'
    androidTestImplementation 'androidx.test:runner:1.1.0'
    androidTestImplementation 'androidx.test:rules:1.1.0'
    //androidTestImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-test:$coroutinesVersion"
    debugImplementation "androidx.fragment:fragment-testing:$fragmentVersion"
    androidTestImplementation "org.mockito:mockito-core:${var}"
    androidTestImplementation "com.google.dexmaker:dexmaker:1.2"
    androidTestImplementation "com.google.dexmaker:dexmaker-mockito:1.2"


}

以及项目 gradle 文件:

ext {
    espressoVersion = '3.3.0-alpha03'
    coroutinesVersion = '1.2.1'
    fragmentVersion = '1.1.0'
    var = '1.10.19'
    var1 = '1.3.60'
    kotlin_version = '1.3.60'
}// Top-level build file where you can add configuration options common to all sub-projects/modules.

buildscript {
    ext {
    kotlin_version = '1.3.40'
        navigationVersion = '2.2.0-rc04'
    }
    repositories {
        google()
        jcenter()
    }
    dependencies {
        classpath 'com.android.tools.build:gradle:3.5.3'
        classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
        classpath "org.jetbrains.kotlin:kotlin-serialization:$kotlin_version"
        // NOTE: Do not place your application dependencies here; they belong
        // in the individual module build.gradle files
        //navigation
        def nav_version = "2.1.0-alpha05"
        classpath "androidx.navigation:navigation-safe-args-gradle-plugin:$nav_version"
    }
}

allprojects {
    repositories {
        google()
        jcenter()
    }
}

task clean(type: Delete) {
    delete rootProject.buildDir
}

我正在使用 Android studio 3.5.3。

所以基本上,当我 运行 我的 phone 上的应用程序(包括导航)时,一切都很好。问题源于仪器化测试本身。据我了解,FragmentScenario 没有实现 OnFragmentInteractionListener。我当然不能更改 FragmentScenario class,而且我不知道应该如何管理这个东西。我是否使用了错误的工具来测试片段交互?

谢谢大家!

FragmentScenario 获取您的片段并在容器中启动它,使用默认的空 activity 作为宿主。它不会启动您的 activity。目标是单独测试片段。

那个空主机 activity 当然没有实现 OnFragmentInteractionListener,因为它是您创建的接口。在您的 onAttach 回调中,您强制主机 activity 实现此接口并告诉它否则抛出异常。这就是您在测试期间遇到的错误。

您可以删除 onAttach 方法中的 else 部分,错误就会消失。但是您的侦听器将为空,并且依赖于它的某些功能将无法正常工作。

也许您也可以考虑更改此架构。也许您可以考虑使用共享视图模型?这比与听众互动更容易。如果您不想更改当前状态,您可以继续进行集成测试而不是孤立的片段测试。