英文:
Android testing fragments and login action
问题
-
Your test is failing because it's not able to find a view with the ID
2131362204
which corresponds toR.id.email
. This means there might be an issue with the setup or the view hierarchy in your test environment. -
To test your login logic with Firebase Realtime Database, you can use Firebase's testing tools. Firebase provides a testing emulator suite that allows you to write unit tests and integration tests for Firebase features. You can find more information in the Firebase documentation on how to set up and use these emulators for testing.
-
For simple unit tests that don't involve UI interactions, you can create separate test classes and functions for your business logic. These tests can be written using standard testing frameworks like JUnit or Kotlin's built-in testing tools. You can write tests to validate different scenarios of your login logic, including cases where the user input is blank or invalid. This will help ensure the correctness of your code.
英文:
Hi guys i'm trying to test my android app.It is an app used for the restaurant industry similar to just eat for example.
I've done some testing on generic navigation and now I want to test the login action and see if the fragments are loaded.
This is my test class
@Test
fun test_Login_to_FragmentRistoranti(){
//Setup activity scenario
val activityScenario= ActivityScenario.launch(IntroActivity::class.java)
//Perform click action per andare a LogIn
Espresso.onView(ViewMatchers.withId(R.id.ConstraintLogin))
.check(ViewAssertions.matches(ViewMatchers.isDisplayed()))
Espresso.onView(ViewMatchers.withId(R.id.ConstraintLogin)).perform(ViewActions.click())
//login action user livello 1
val email:String="useruser@gmail.com"
val pwd:String="useruser"
Espresso.onView(ViewMatchers.withId(R.id.email)).perform(ViewActions.typeText(email))
closeSoftKeyboard()
Espresso.onView(ViewMatchers.withId(R.id.password)).perform(ViewActions.typeText(pwd))
closeSoftKeyboard()
Espresso.onView(ViewMatchers.withId(R.id.ConstraintLogin)).perform(ViewActions.click())
//Controllo dopo il login che si veda fragmentRistoranti
Espresso.onView(ViewMatchers.withId(R.id.swipe_refresh_ristoranti))
.check(ViewAssertions.matches(ViewMatchers.isDisplayed()))
//logout e controllo che si veda login
Espresso.onView(ViewMatchers.withId(R.id.ic_logoutU)).perform(ViewActions.click())
//Perform click action per andare a LogIn
Espresso.onView(ViewMatchers.withId(R.id.ConstraintLogin))
.check(ViewAssertions.matches(ViewMatchers.isDisplayed()))
}
And this is my FragmentLogin class
package com.example.progettoprogrammazione.intro
import android.content.Intent
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.Toast
import androidx.fragment.app.Fragment
import androidx.navigation.findNavController
import com.example.progettoprogrammazione.R
import com.example.progettoprogrammazione.activity.EmployeeActivity
import com.example.progettoprogrammazione.activity.RestaurateurActivity
import com.example.progettoprogrammazione.activity.UserActivity
import com.example.progettoprogrammazione.databinding.Fragment0LoginBinding
import com.example.progettoprogrammazione.firebase.FireBaseCallbackRestaurant
import com.example.progettoprogrammazione.firebase.FireBaseCallbackCart
import com.example.progettoprogrammazione.firebase.FireBaseCallbackUser
import com.example.progettoprogrammazione.utils.*
import com.google.firebase.auth.FirebaseAuth
import com.google.firebase.database.FirebaseDatabase
// Made by Alessandro Pieragostini, Matteo Sonaglioni & Stefano Marcucci
/* Chi utilizza questo fragment può accedere all'interno dell'applicazione, a seconda del livello
utente, oppure navigare alla pagina Registrati */
class FragmentLogin : Fragment(), UserUtils, DipendenteUtils, RestaurantUtils, ProductUtils,
CartUtils {
private lateinit var binding: Fragment0LoginBinding
override var firebaseAuth: FirebaseAuth = FirebaseAuth.getInstance()
override var firebaseDatabase: FirebaseDatabase = FirebaseDatabase.getInstance()
private var userlvl: String? = null
private lateinit var useremail: String
fun login(email:String, password:String){
if (email.isNotEmpty() && password.isNotEmpty()) {
firebaseAuth.signInWithEmailAndPassword(email, password).addOnCompleteListener {
if (it.isSuccessful) {
//LOGIN
getUserData(object : FireBaseCallbackUser {
override fun onResponse(responseU: ResponseUser) {
userlvl = responseU.user!!.Livello
Toast.makeText(
context,
"Login effettuato con successo!",
Toast.LENGTH_LONG
)
.show()
getQRData(
FirebaseAuth.getInstance().uid,
object : FireBaseCallbackCart {
override fun onResponse(responseC: ResponseCart) {
getRestaurantData(object : FireBaseCallbackRestaurant {
override fun onResponse(responseR: ResponseRistorante) {
when (userlvl) {
"1" -> {
val intent =
Intent(
context,
UserActivity::class.java
).apply {
putExtra("user", responseU.user)
putParcelableArrayListExtra(
"ristoranti",
responseR.ristoranti
)
putExtra(
"cart",
responseC.cart
)
}
startActivity(intent)
activity?.finish()
}
"2" -> {
val intent =
Intent(
context,
EmployeeActivity::class.java
).apply {
putExtra("user", responseU.user)
putParcelableArrayListExtra(
"ristoranti",
responseR.ristoranti
)
putExtra(
"cart",
responseC.cart
)
}
startActivity(intent)
activity?.finish()
}
"3" -> {
val intent =
Intent(
context,
RestaurateurActivity::class.java
).apply {
putExtra("user", responseU.user)
putParcelableArrayListExtra(
"ristoranti",
responseR.ristoranti
)
putExtra(
"cart",
responseC.cart
)
}
startActivity(intent)
activity?.finish()
}
else -> {
Toast.makeText(
context,
"Errore durante il caricamento.",
Toast.LENGTH_LONG
).show()
}
}
}
}, context)
}
},
context
)
}
}, context)
} else Toast.makeText(
context,
"Email e password non corrispondono!",
Toast.LENGTH_LONG
).show()
}
} else {
Toast.makeText(context, "Nessun campo può essere vuoto!", Toast.LENGTH_LONG)
.show()
}
}
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
binding = Fragment0LoginBinding.inflate(layoutInflater)
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
firebaseAuth = FirebaseAuth.getInstance()
// Questa funzione permette di effettuare il login quando i campi di email e password corrispondono
// ad un account già creato e, a seconda del livello, si effettuerà la navigazione in una delle Activity
binding.ConstraintLogin.setOnClickListener {
login( binding.email.text.toString(), binding.password.text.toString())
}
// Cliccando in questo bottone, verrà mandata una mail di recupero password
binding.recuperapassword.setOnClickListener {
useremail = binding.email.text.toString()
if (useremail.isNotEmpty()) {
recoverUserPassword(context, useremail)
} else Toast.makeText(context, "Inserisci un'email.", Toast.LENGTH_SHORT).show()
}
// Cliccando sul bottone, la navigazione porterà alla pagina "Registrati"
binding.noaccount.setOnClickListener {
view.findNavController().navigate(R.id.LoginToRegister)
}
}
}
The error of the test class is reported below
androidx.test.espresso.NoMatchingViewException: No views in hierarchy found matching:
view.getId() is <2131362204/com.example.progettoprogrammazione:id/email>
View Hierarchy:
+>DecorView{id=-1, visibility=VISIBLE, width=1080, height=2280, has-focus=false, has-focusable=true, has-window-focus=true, is-clickable=false, is-enabled=true, is-focused=false, is-focusable=false, is-layout-requested=false, is-selected=false, layout-params={(0,0)(fillxfill) ty=BASE_APPLICATION wanim=0x10302fe
fl=LAYOUT_IN_SCREEN LAYOUT_INSET_DECOR SPLIT_TOUCH HARDWARE_ACCELERATED DRAWS_SYSTEM_BAR_BACKGROUNDS
pfl=NO_MOVE_ANIMATION FORCE_DRAW_STATUS_BAR_BACKGROUND FIT_INSETS_CONTROLLED
bhv=DEFAULT
fitSides=}, tag=null, root-is-layout-requested=false, has-input-connection=false, x=0.0, y=0.0, child-count=3}
|
+->LinearLayout{id=-1, visibility=VISIBLE, width=1080, height=2214, has-focus=false, has-focusable=true, has-window-focus=true, is-clickable=false, is-enabled=true, is-focused=false, is-focusable=false, is-layout-requested=false, is-selected=false, layout-params=android.widget.FrameLayout$LayoutParams@YYYYYY, tag=null, root-is-layout-requested=false, has-input-connection=false, x=0.0, y=0.0, child-count=2}
|
+-->ViewStub{id=16908746, res-name=action_mode_bar_stub, visibility=GONE, width=0, height=0, has-focus=false, has-focusable=false, has-window-focus=true, is-clickable=false, is-enabled=true, is-focused=false, is-focusable=false, is-layout-requested=true, is-selected=false, layout-params=android.widget.LinearLayout$LayoutParams@YYYYYY, tag=null, root-is-layout-requested=false, has-input-connection=false, x=0.0, y=0.0}
|
+-->FrameLayout{id=-1, visibility=VISIBLE, width=1080, height=2148, has-focus=false, has-focusable=true, has-window-focus=true, is-clickable=false, is-enabled=true, is-focused=false, is-focusable=false, is-layout-requested=false, is-selected=false, layout-params=android.widget.LinearLayout$LayoutParams@YYYYYY, tag=null, root-is-layout-requested=false, has-input-connection=false, x=0.0, y=66.0, child-count=1}
|
+--->ActionBarOverlayLayout{id=2131362158, res-name=decor_content_parent, visibility=VISIBLE, width=1080, height=2148, has-focus=false, has-focusable=true, has-window-focus=true, is-clickable=false, is-enabled=true, is-focused=false, is-focusable=false, is-layout-requested=false, is-selected=false, layout-params=android.widget.FrameLayout$LayoutParams@YYYYYY, tag=null, root-is-layout-requested=false, has-input-connection=false, x=0.0, y=0.0, child-count=2}
|
+---->ContentFrameLayout{id=16908290, res-name=content, visibility=VISIBLE, width=1080, height=1994, has-focus=false, has-focusable=true, has-window-focus=true, is-clickable=false, is-enabled=true, is-focused=false, is-focusable=false, is-layout-requested=false, is-selected=false, layout-params=androidx.appcompat.widget.ActionBarOverlayLayout$LayoutParams@YYYYYY, tag=null, root-is-layout-requested=false, has-input-connection=false, x=0.0, y=154.0, child-count=1}
|
+----->ConstraintLayout{id=2131362320, res-name=intro_activity, visibility=VISIBLE, width=1080, height=1994, has-focus=false, has-focusable=true, has-window-focus=true, is-clickable=false, is-enabled=true, is-focused=false, is-focusable=false, is-layout-requested=false, is-selected=false, layout-params=android.widget.FrameLayout$LayoutParams@YYYYYY, tag=null, root-is-layout-requested=false, has-input-connection=false, x=0.0, y=0.0, child-count=1}
|
+------>FragmentContainerView{id=2131362417, res-name=nav_host, visibility=VISIBLE, width=1080, height=1994, has-focus=false, has-focusable=true, has-window-focus=true, is-clickable=false, is-enabled=true, is-focused=false, is-focusable=false, is-layout-requested=false, is-selected=false, layout-params=androidx.constraintlayout.widget.ConstraintLayout$LayoutParams@YYYYYY, tag=null, root-is-layout-requested=false, has-input-connection=false, x=0.0, y=0.0, child-count=1}
|
+------->FragmentContainerView{id=2131362417, res-name=nav_host, visibility=VISIBLE, width=1080, height=1994, has-focus=false, has-focusable=true, has-window-focus=true, is-clickable=false, is-enabled=true, is-focused=false, is-focusable=false, is-layout-requested=false, is-selected=false, layout-params=android.widget.FrameLayout$LayoutParams@YYYYYY, tag=null, root-is-layout-requested=false, has-input-connection=false, x=0.0, y=0.0, child-count=1}
|
+-------->ConstraintLayout{id=-1, visibility=VISIBLE, width=1080, height=1994, has-focus=false, has-focusable=true, has-window-focus=true, is-clickable=false, is-enabled=true, is-focused=false, is-focusable=false, is-layout-requested=false, is-selected=false, layout-params=android.widget.FrameLayout$LayoutParams@YYYYYY, tag=null, root-is-layout-requested=false, has-input-connection=false, x=0.0, y=0.0, child-count=1}
|
+--------->LinearLayout{id=-1, visibility=VISIBLE, width=1080, height=1994, has-focus=false, has-focusable=true, has-window-focus=true, is-clickable=false, is-enabled=true, is-focused=false, is-focusable=false, is-layout-requested=false, is-selected=false, layout-params=androidx.constraintlayout.widget.ConstraintLayout$LayoutParams@YYYYYY, tag=null, root-is-layout-requested=false, has-input-connection=false, x=0.0, y=0.0, child-count=4}
|
+---------->AppCompatImageView{id=2131362307, res-name=image_intro, visibility=VISIBLE, width=871, height=871, has-focus=false, has-focusable=false, has-window-focus=true, is-clickable=false, is-enabled=true, is-focused=false, is-focusable=false, is-layout-requested=false, is-selected=false, layout-params=android.widget.LinearLayout$LayoutParams@YYYYYY, tag=null, root-is-layout-requested=false, has-input-connection=false, x=104.0, y=55.0}
|
+---------->MaterialTextView{id=2131361945, res-name=benvenuto_text, visibility=VISIBLE, width=720, height=88, has-focus=false, has-focusable=false, has-window-focus=true, is-clickable=false, is-enabled=true, is-focused=false, is-focusable=false, is-layout-requested=false, is-selected=false, layout-params=android.widget.LinearLayout$LayoutParams@YYYYYY, tag=null, root-is-layout-requested=false, has-input-connection=false, x=180.0, y=926.0, text=Benvenuto su Cookade!, input-type=0, ime-target=false, has-links=false}
|
+---------->LinearLayout{id=-1, visibility=VISIBLE, width=970, height=279, has-focus=false, has-focusable=true, has-window-focus=true, is-clickable=false, is-enabled=true, is-focused=false, is-focusable=false, is-layout-requested=false, is-selected=false, layout-params=android.widget.LinearLayout$LayoutParams@YYYYYY, tag=null, root-is-layout-requested=false, has-input-connection=false, x=55.0, y=1152.0, child-count=1}
|
+----------->LinearLayout{id=-1, visibility=VISIBLE, width=936, height=279, has-focus=false, has-focusable=true, has-window-focus=true, is-clickable=false, is-enabled=true, is-focused=false, is-focusable=false, is-layout-requested=false, is-selected=false, layout-params=android.widget.LinearLayout$LayoutParams@YYYYYY, tag=null, root-is-layout-requested=false, has-input-connection=false, x=17.0, y=0.0, child-count=2}
|
+------------>ConstraintLayout{id=2131361801, res-name=ConstraintLogin, visibility=VISIBLE, width=550, height=138, has-focus=false, has-focusable=true, has-window-focus=true, is-clickable=true, is-enabled=true, is-focused=false, is-focusable=true, is-layout-requested=false, is-selected=false, layout-params=android.widget.LinearLayout$LayoutParams@YYYYYY, tag=null, root-is-layout-requested=false, has-input-connection=false, x=193.0, y=55.0, child-count=1}
|
+------------->MaterialTextView{id=2131362346, res-name=login, visibility=VISIBLE, width=163, height=71, has-focus=false, has-focusable=false, has-window-focus=true, is-clickable=false, is-enabled=true, is-focused=false, is-focusable=false, is-layout-requested=false, is-selected=false, layout-params=androidx.constraintlayout.widget.ConstraintLayout$LayoutParams@YYYYYY, tag=null, root-is-layout-requested=false, has-input-connection=false, x=194.0, y=34.0, text=LOGIN, input-type=0, ime-target=false, has-links=false}
|
+------------>MaterialTextView{id=-1, visibility=VISIBLE, width=826, height=3, has-focus=false, has-focusable=false, has-window-focus=true, is-clickable=false, is-enabled=true, is-focused=false, is-focusable=false, is-layout-requested=false, is-selected=false, layout-params=android.widget.LinearLayout$LayoutParams@YYYYYY, tag=null, root-is-layout-requested=false, has-input-connection=false, x=55.0, y=221.0, text=, input-type=0, ime-target=false, has-links=false}
|
+---------->LinearLayout{id=-1, visibility=VISIBLE, width=970, height=127, has-focus=false, has-focusable=true, has-window-focus=true, is-clickable=false, is-enabled=true, is-focused=false, is-focusable=false, is-layout-requested=false, is-selected=false, layout-params=android.widget.LinearLayout$LayoutParams@YYYYYY, tag=null, root-is-layout-requested=false, has-input-connection=false, x=55.0, y=1431.0, child-count=1}
|
+----------->MaterialTextView{id=2131362547, res-name=registrati, visibility=VISIBLE, width=914, height=71, has-focus=false, has-focusable=true, has-window-focus=true, is-clickable=true, is-enabled=true, is-focused=false, is-focusable=true, is-layout-requested=false, is-selected=false, layout-params=android.widget.LinearLayout$LayoutParams@YYYYYY, tag=null, root-is-layout-requested=false, has-input-connection=false, x=28.0, y=28.0, text=Non sei registrato? Clicca qui!, input-type=0, ime-target=false, has-links=false}
|
+---->ActionBarContainer{id=2131361899, res-name=action_bar_container, visibility=VISIBLE, width=1080, height=154, has-focus=false, has-focusable=false, has-window-focus=true, is-clickable=false, is-enabled=true, is-focused=false, is-focusable=false, is-layout-requested=false, is-selected=false, layout-params=androidx.appcompat.widget.ActionBarOverlayLayout$LayoutParams@YYYYYY, tag=null, root-is-layout-requested=false, has-input-connection=false, x=0.0, y=0.0, child-count=2}
|
+----->Toolbar{id=2131361897, res-name=action_bar, visibility=VISIBLE, width=1080, height=154, has-focus=false, has-focusable=false, has-window-focus=true, is-clickable=false, is-enabled=true, is-focused=false, is-focusable=false, is-layout-requested=false, is-selected=false, layout-params=android.widget.FrameLayout$LayoutParams@YYYYYY, tag=null, root-is-layout-requested=false, has-input-connection=false, x=0.0, y=0.0, child-count=2}
|
+------>AppCompatTextView{id=-1, visibility=VISIBLE, width=269, height=71, has-focus=false, has-focusable=false, has-window-focus=true, is-clickable=false, is-enabled=true, is-focused=false, is-focusable=false, is-layout-requested=false, is-selected=false, layout-params=androidx.appcompat.widget.Toolbar$LayoutParams@YYYYYY, tag=null, root-is-layout-requested=false, has-input-connection=false, x=44.0, y=41.0, text=COOKADE, input-type=0, ime-target=false, has-links=false}
|
+------>ActionMenuView{id=-1, visibility=VISIBLE, width=0, height=154, has-focus=false, has-focusable=false, has-window-focus=true, is-clickable=false, is-enabled=true, is-focused=false, is-focusable=false, is-layout-requested=false, is-selected=false, layout-params=androidx.appcompat.widget.Toolbar$LayoutParams@YYYYYY, tag=null, root-is-layout-requested=false, has-input-connection=false, x=1080.0, y=0.0, child-count=0}
|
+----->ActionBarContextView{id=2131361905, res-name=action_context_bar, visibility=GONE, width=0, height=0, has-focus=false, has-focusable=false, has-window-focus=true, is-clickable=false, is-enabled=true, is-focused=false, is-focusable=false, is-layout-requested=true, is-selected=false, layout-params=android.widget.FrameLayout$LayoutParams@YYYYYY, tag=null, root-is-layout-requested=false, has-input-connection=false, x=0.0, y=0.0, child-count=0}
|
+->View{id=16908336, res-name=navigationBarBackground, visibility=VISIBLE, width=1080, height=66, has-focus=false, has-focusable=false, has-window-focus=true, is-clickable=false, is-enabled=true, is-focused=false, is-focusable=false, is-layout-requested=false, is-selected=false, layout-params=android.widget.FrameLayout$LayoutParams@YYYYYY, tag=null, root-is-layout-requested=false, has-input-connection=false, x=0.0, y=2214.0}
|
+->View{id=16908335, res-name=statusBarBackground, visibility=VISIBLE, width=1080, height=66, has-focus=false, has-focusable=false, has-window-focus=true, is-clickable=false, is-enabled=true, is-focused=false, is-focusable=false, is-layout-requested=false, is-selected=false, layout-params=android.widget.FrameLayout$LayoutParams@YYYYYY, tag=null, root-is-layout-requested=false, has-input-connection=false, x=0.0, y=0.0}
at androidx.test.espresso.NoMatchingViewException$Builder.build(NoMatchingViewException.java:5)
at androidx.test.espresso.base.DefaultFailureHandler.lambda$getNoMatchingViewExceptionTruncater$0(DefaultFailureHandler.java:5)
at androidx.test.espresso.base.DefaultFailureHandler$$ExternalSyntheticLambda1.truncateExceptionMessage(Unknown Source:2)
at androidx.test.espresso.base.ViewHierarchyExceptionHandler.handleSafely(ViewHierarchyExceptionHandler.java:5)
at androidx.test.espresso.base.ViewHierarchyExceptionHandler.handleSafely(ViewHierarchyExceptionHandler.java:1)
at androidx.test.espresso.base.DefaultFailureHandler$TypedFailureHandler.handle(DefaultFailureHandler.java:4)
at androidx.test.espresso.base.DefaultFailureHandler.handle(DefaultFailureHandler.java:5)
at androidx.test.espresso.ViewInteraction.waitForAndHandleInteractionResults(ViewInteraction.java:8)
at androidx.test.espresso.ViewInteraction.desugaredPerform(ViewInteraction.java:11)
at androidx.test.espresso.ViewInteraction.perform(ViewInteraction.java:8)
at com.example.progettoprogrammazione.UserNavigationTesting.test_Login_to_FragmentRistoranti(UserNavigationTesting.kt:46)
Now I want to ask a couple of questions:
1)Why my test returns failed
2)How can i test my login logic in a better way. We use in our project firebase realtimedb and i do not know how to implement testing (like i want to test if i let the user text view blank , ecc)
3)Where i can do some simple testing and not UI tests
答案1
得分: 1
你的测试失败是因为在点击 R.id.ConstraintLogin 后,预期的视图未显示在屏幕上。这可能是由于多种原因,例如片段/活动加载时间比测试时间长,或者视图不包含点击监听器。您需要分享更多的代码,以便我能够准确定位问题。如果是由于时间问题,我还建议查看空闲资源。
如果您想更好地测试自己的逻辑,那么您需要将您自己的逻辑代码部分提取到单独的函数中进行单元测试。针对数据库进行测试并不是一个好主意,而是应该模拟数据并使数据访问层用于测试目的。简单的测试是通过单元测试完成的,您可以在这里测试数据类和函数,但请注意,如果不使用框架(如 mockjay 或 mockito),您将无法获得 Android 上下文。
英文:
Your test is failing because after the click action on R.id.ConstraintLogin, the expected view is not showing on the screen. This could be due to a number of reasons, such as due to the fragment/activity loading is taking longer than the test or that the view does not contain the click listener. You would have to share a little more code for me to be able to pinpoint the problem. I would also suggest looking into Idling resources if its due to a timing issue.
If you want to test your own logic better, then you will need to extract the code parts that are your own logic into separate functions for unit testing. Testing against a DB is not a good idea, instead you should mock the data and make the data access layer abstract for testing purposes.
Simple testing is done with unit tests, here you can test data classes and functions, but beware, you have no android context without using a framework such as mockjay or mockito.
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论