Issue
I am currently trying to test the navigation in android with androidTest, mokito and espresso like suggested in this tutorial: https://developer.android.com/guide/navigation/navigation-testing But I systematically get the following error: E/MonitoringInstr: Exception encountered by: Thread[main,5,main]. Dumping thread state to outputs and pining for the fjords. java.lang.RuntimeException: androidx.fragment.app.testing.FragmentScenario$EmptyFragmentActivity@fa3e8be must implement OnFragmentInteractionListener
Here is the test 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)
}
}
}
Here is my fragment
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 {
}
}
}
}
And the mainAcrivity file:
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
}
}
And the module graddle file:
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"
}
And the project graddle file:
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
}
I am using Android studio 3.5.3.
So basically, everything is fine when I just run the app on my phone including navigation. The problem steams from the instrumented test itself. For what I understand, FragmentScenario does not implement OnFragmentInteractionListener. I cannot change FragmentScenario class of course and I don't know how this thing is supposed to be managed. Am I using the wrong tool to test the fragments interactions?
Thank you all!
Solution
FragmentScenario takes your fragment and launches it in a container, using a default empty activity as the host. It doesn't launch your activity. The goal is to test the fragment in isolation.
That empty host activity doesn't implement OnFragmentInteractionListener, of course, as it is an interface created by you. In your onAttach callback, you are forcing the host activity to implement this interface and tell it to throw an exception otherwise. And that's the error you get during tests.
You can remove the else part in your onAttach method and the error will be gone. But your listener will be null and some functionalities that depend on it won't work properly.
May be you might also consider changing this architecture. May be you can consider using a shared viewmodel? It is easier than interacting with listeners. If you don't want to change current state, may be you can go on with integration tests instead of isolated fragment tests.
Answered By - Oya Canli
0 comments:
Post a Comment
Note: Only a member of this blog may post a comment.