我正在对Fragment是否显示进行仪器测试。当我开始执行测试时,我可以在物理设备中看到 FragmentLogin 视图,但测试失败,并显示 androidx.test.espresso.NoMatchingViewException: 在层次结构中找不到匹配的视图:view.getId() 是 <2131296460/com.nano.modularapp:id/fragmentLogin> 并且应用程序关闭.
我在代码中使用了 Hilt 和 Navigation Graph 进行测试。
主要活动::类
@AndroidEntryPoint
class MainActivity : AppCompatActivity() {
private lateinit var navController: NavController
private lateinit var toolbar: Toolbar
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
enableEdgeToEdge()
setContentView(R.layout.activity_main)
ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.main)) { v, insets ->
val systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars())
v.setPadding(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom)
insets
}
toolbar = findViewById(R.id.toolbar)
//https://medium.com/androiddevelopers/navigationui-d21fd4f5c318
val navHostFragment = supportFragmentManager.findFragmentById(R.id.navHostFragmentController) as NavHostFragment
navController = navHostFragment.navController
val appBarConfiguration = AppBarConfiguration(navController.graph)
setSupportActionBar(toolbar)
setupActionBarWithNavController(navController,appBarConfiguration)
}
}
MainActivity.xml
<androidx.constraintlayout.widget.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/main"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<androidx.appcompat.widget.Toolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintStart_toStartOf="parent"/>
<androidx.fragment.app.FragmentContainerView
android:id="@+id/navHostFragmentController"
android:layout_width="match_parent"
android:layout_height="0dp"
app:layout_constraintTop_toBottomOf="@id/toolbar"
app:layout_constraintBottom_toBottomOf="parent"
android:name="androidx.navigation.fragment.NavHostFragment"
app:navGraph="@navigation/onboard_graph"
app:defaultNavHost="true"/>
</androidx.constraintlayout.widget.ConstraintLayout>
导航图
<navigation xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/onboard_graph"
app:startDestination="@id/fragmentLogin">
<fragment
android:id="@+id/fragmentLogin"
android:name="com.nano.modularapp.login.FragmentLogin"
android:label="Login" >
<action
android:id="@+id/action_fragmentLogin_to_fragmentSignUp"
app:destination="@id/fragmentSignUp" />
</fragment>
<fragment
android:id="@+id/fragmentSignUp"
android:name="com.nano.modularapp.signup.FragmentSignUp"
android:label="Sign Up" />
</navigation>
FragmentLogin::类
@AndroidEntryPoint
class FragmentLogin : Fragment() {
private lateinit var binding: FragmentLoginBinding
private val viewModel by viewModels<LoginViewModel>()
var errorMsg: ObservableField<String> = ObservableField("")
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
}
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
binding = DataBindingUtil.inflate(inflater,R.layout.fragment_login,container,false)
binding.login = this
binding.lifecycleOwner = this
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
observeInputFieldsError()
setListener()
}
private fun observeInputFieldsError(){
viewModel.inputFieldError.observe(viewLifecycleOwner, Observer { state->
when(state){
is ValidationState.EmailError->{
errorMsg.set(getString(state.error))
}
is ValidationState.PasswordError->{
errorMsg.set(getString(state.error))
}
ValidationState.Success->{}
}
})
}
private fun setListener(){
binding.navigateToSignUp.setOnClickListener {
findNavController().navigate(R.id.action_fragmentLogin_to_fragmentSignUp)
}
binding.btnLogin.setOnClickListener {
viewModel.loginUser("","")
}
}
}
FragmentLogin.xml
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
xmlns:app="http://schemas.android.com/apk/res-auto">
<data>
<variable
name="login"
type="com.nano.modularapp.login.FragmentLogin" />
</data>
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".login.FragmentLogin">
<TextView
android:id="@+id/tvSignUpTitle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Welcome in ModularApp"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintStart_toStartOf="parent"
android:layout_marginTop="20dp"
android:layout_marginStart="20dp"
android:textColor="@color/black"
android:fontFamily="@font/mukta_bold"
android:textSize="26sp"
android:textStyle="bold"/>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintTop_toBottomOf="@id/tvSignUpTitle"
app:layout_constraintStart_toStartOf="parent"
android:text="Login"
android:textColor="@color/black"
android:layout_marginTop="20dp"
android:layout_marginStart="20dp"
android:fontFamily="@font/mukta_medium"
android:textSize="22sp" />
<LinearLayout
android:layout_width="0dp"
android:layout_height="wrap_content"
android:orientation="vertical"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
android:layout_margin="20dp">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Email"
android:textSize="20sp"
android:layout_marginTop="15dp"
android:textColor="@color/text_color"
android:fontFamily="@font/mukta_medium"/>
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/editTextEmail"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="Email"
android:layout_marginTop="5dp"
android:textColor="@color/text_color"
android:fontFamily="@font/mukta_regular"
android:textSize="18sp"/>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Password"
android:textSize="20sp"
android:layout_marginTop="20dp"
android:textColor="@color/text_color"
android:fontFamily="@font/mukta_medium"
android:inputType="textEmailAddress"/>
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/editTextPassword"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="Password"
android:layout_marginTop="5dp"
android:textColor="@color/text_color"
android:fontFamily="@font/mukta_regular"
android:textSize="18sp"
android:inputType="textPassword"/>
<TextView
android:id="@+id/tvError"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textSize="15sp"
android:layout_marginTop="15dp"
android:fontFamily="@font/mukta_regular"
android:text="@{login.errorMsg}"
android:textColor="@color/error"/>
<Button
android:id="@+id/btnLogin"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Login"
android:textSize="20sp"
android:paddingStart="20dp"
android:paddingEnd="20dp"
android:layout_marginTop="25dp"
android:layout_gravity="center"
android:fontFamily="@font/mukta_semi_bold"/>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:padding="8dp"
android:gravity="center">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textSize="15sp"
android:text="Don't have an account?"
android:textColor="@color/black"
android:fontFamily="@font/mukta_regular"/>
<TextView
android:id="@+id/navigateToSignUp"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textSize="18sp"
android:text=" Sign Up"
android:textColor="@color/info_blue"
android:fontFamily="@font/mukta_medium"/>
</LinearLayout>
</LinearLayout>
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>
FragmentLoginInstrumentedTest::类
@HiltAndroidTest
@RunWith(AndroidJUnit4::class)
class FragmentLoginInstrumentedTest {
@get:Rule
val hiltRule:HiltAndroidRule = HiltAndroidRule(this)
@get:Rule
val activityRule: ActivityScenarioRule<MainActivity> = ActivityScenarioRule(MainActivity::class.java)
@Before
fun init(){
hiltRule.inject()
}
@Test
fun test_blankField_expectedEmptyField(){
//Checking LoginFragment is attached and displayed in screen or not
onView(withId(R.id.fragmentLogin)).check(ViewAssertions.matches(isDisplayed()))
//Clicking on Sign Up text to navigate to Sign Up Fragment
//onView(withId(R.id.navigateToSignUp)).perform(ViewActions.click())
//Checking SignUpFragment is displayed or not
//onView(withId(R.id.fragmentSignUp)).check(ViewAssertions.matches(isDisplayed()))
//Clicking Sign Up Button
//onView(withId(R.id.btnSignUp)).perform(ViewActions.click())
//Checking empty email error message
//onView(withId(R.id.tvError)).check(ViewAssertions.matches(withText("Email field is empty")))
}
}
以上是我的完整代码。我不明白原因,登录片段显示然后测试用例失败。
我能够解决这个问题。一个很小的点需要看这里
当您使用 Hilt + Navigation 使用 NavController 加载片段时。 Android Instrumentation 测试过程中需要考虑的几点
运行仪器测试注释时 @RunwWith(AndroidJUnitRunner::class)
设置另一个规则来确定您将启动 Fragment 的 Activity。正如你 正在使用片段,您需要在调试主中创建 HiltTestActivity java目录中的文件夹。
List 编写时 Espresso.onView(withId(R.id.loginFragment)) --> 将其替换为 Espresso.onView(withId(R.id.loginFragmentView))。 “loginFragmentView”您需要将其添加到片段的根视图中 布局文件名。
请查看这个 github 解决方案供您参考 NoMatchingViewException 问题