我目前正在尝试使用androidTest,mokito和espresso来测试android中的导航,如本教程中建议的那样: https://developer.android.com/guide/navigation/navigation-testing 但是我系统地得到以下错误: E / MonitoringInstr:线程[main,5,main]遇到异常。将线程状态转储到输出并固定到峡湾。 java.lang.RuntimeException:androidx.fragment.app.testing.FragmentScenario$EmptyFragmentActivity@fa3e8be必须实现OnFragmentInteractionListener
这是测试班:
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
}
}
和模块毕业文件:
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"
}
和项目毕业文件:
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。
所以基本上,当我在手机上运行应用程序(包括导航)时,一切都很好。问题来自于仪器测试本身。据我了解,FragmentScenario没有实现OnFragmentInteractionListener。我当然不能更改FragmentScenario类,也不知道应该如何管理。我使用错误的工具测试片段之间的相互作用吗?
谢谢大家!
答案 0 :(得分:2)
FragmentScenario使用默认的空活动作为主机,获取您的片段并将其在容器中启动。它不会启动您的活动。目的是隔离测试片段。
该空主机活动当然不会实现OnFragmentInteractionListener,因为它是您创建的接口。在onAttach回调中,您将强制主机活动实现此接口,并告诉它引发异常。那就是您在测试期间遇到的错误。
您可以在onAttach方法中删除else部分,错误将消失。但是您的监听器将为空,并且依赖于它的某些功能将无法正常工作。
也许您可能还会考虑更改此体系结构。也许您可以考虑使用共享的ViewModel?它比与听众进行交互容易。如果您不想更改当前状态,可以继续进行集成测试而不是孤立的片段测试。