I'm trying to test a fragment that uses viewBinding to show the views, and a viewModel to fetch that data.
I want to write a UI test to see if certain data is visible or not, but so fare I've had no luck and I'm hoping somebody can help me as I'm still very new to UI testing.
So far I tried mocking the viewModel, the liveData and the viewBinding, but to no success
Here are a few example classes of what I have tried to do
My ViewModel:
private val bike: MutableLiveData<Bike> = MutableLiveData()
val getBike: LiveData<BikeType>
get() = bikeType
fun getBike() {
BikeService().getBike(bikeId)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe({
bike.postValue(it)
}, {
Timber.e(it)
}).let { disposeBag.add(it) }
}
}
}
My Fragment
class BikeFragment : AppAbstractBaseFragment<Any>(), BikeView,
OnBackPressedListener {
var mBinding: FragmentBikeBinding? = null
var viewModel: BikeViewModel? = null
var cancellable: Boolean = false
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
mBinding = FragmentBikeBinding.inflate(inflater, container, false)
return mBinding?.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
if (viewModel == null) {
viewModel = ViewModelProvider(this).get(BikeViewModel::class.java)
}
mBinding?.apply {
this.view = this#BikeDashboardFragment
viewModel?.getBike?.observe(viewLifecycleOwner, { newBike ->
bike = newBike
})
}
viewModel?.getBike()
}
The Test
class BikeTest {
#get:Rule
val screenshotTestRule = ScreenshotTestRule()
#Rule
#JvmField
val dataBindingIdlingResourceRule = DataBindingIdlingResourceRule()
#Rule
#JvmField
var instantExecutor = InstantTaskExecutorRule()
private var scenario: FragmentScenario<BikeFragment>? = null
private var bikeData = mockk<MutableLiveData<Bike>>()
private var viewModel = mockk<BikeViewModel>(relaxed = true)
private val lifecycleOwner: LifecycleOwner = mockk()
private var fakeBike =
Bike(id = 1, cancellable = true, linkable = true, ownerName = "Joske")
var context: Context? = null
#Before
fun setUp() {
val app = ApplicationProvider.getApplicationContext<BaseApplication>()
context = app.applicationContext
}
#Test
fun loadBikeSeeTheRightBikeData() {
scenario = launchFragmentInContainer(
themeResId = R.style.Theme_Brand
)
dataBindingIdlingResourceRule.monitorFragment(scenario!!)
scenario?.onFragment {
it.mBinding?.bike = fakeBike
onView(withId(R.id.ownerNameTextView)).check(matches(withText("Klaartje")))
}
}
}
Related
I have a fragment which named FavoriteScreen.
FavoriteScreen has a favoriteScreenviewModel.
FavoriteScreen lists favorite movies and favorite movies are coming from Room Database.
Also, when pressed a film from recycleView, sets movie value in detailViewModel, detailScreen opens and detailScreen observing the detailScreenViewModel movie value.
When I make a test in FavoriteScreen, given error. What can I do it?
My favorite screen like this:
#AndroidEntryPoint
class FavoriteScreen #Inject constructor(var favoriteMovieAdapter: FavoriteMovieAdapter) :
Fragment() {
private var _binding: FragmentFavoriteScreenBinding? = null
private val fragmentFavoriteScreenBinding get() = _binding!!
lateinit var viewModel: FavoriteScreenViewModel
lateinit var detailViewModel: DetailScreenViewModel
lateinit var moviesRecyclerView: RecyclerView
var favoriteMovieList: List<FavoriteMovie> = listOf()
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View {
_binding = FragmentFavoriteScreenBinding.inflate(inflater, container, false)
return fragmentFavoriteScreenBinding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
viewModel = ViewModelProvider(requireActivity())[FavoriteScreenViewModel::class.java]
detailViewModel = ViewModelProvider(requireActivity())[DetailScreenViewModel::class.java]
subscribeToObservers()
moviesRecyclerView = fragmentFavoriteScreenBinding.moviesListRecyclerFavorite
moviesRecyclerView.adapter = favoriteMovieAdapter
moviesRecyclerView.layoutManager =
LinearLayoutManager(requireContext())
favoriteMovieAdapter.setOnItemClickListener {
val selectedMovie =
MovieResult(
it.backdrop_path,
it.poster_path,
it.api_id,
it.original_language,
it.original_title,
it.title,
it.release_date,
it.overview,
it.popularity.toFloat(),
it.vote_average.toFloat(),
it.vote_count
)
detailViewModel.setItem(selectedMovie)
findNavController().navigate(R.id.action_favoriteScreen_to_detailScreen)
}
}
private fun subscribeToObservers() {
viewModel.favoriteMovies.observe(viewLifecycleOwner) {
favoriteMovieAdapter.movieList = it
setVisibility(it)
}
}
private fun setVisibility(list: List<FavoriteMovie>) {
if (list.isNotEmpty()) {
fragmentFavoriteScreenBinding.favoritesBackground.visibility = View.GONE
} else {
fragmentFavoriteScreenBinding.favoritesBackground.visibility = View.VISIBLE
}
}
override fun onDestroyView() {
super.onDestroyView()
_binding = null
}}
My FavoriteScreenViewModel like this:
#HiltViewModel
class FavoriteScreenViewModel #Inject constructor(private val repository: MovieRepositoryInterface) :
ViewModel() {
val favoriteMovies = repository.getFavoriteMovies()
}
My FakeRepository like this:
class FakeMovieRepositoryTest : MovieRepositoryInterface {
private val favoriteMovies = mutableListOf<FavoriteMovie>()
private val favoriteMoviesLiveData = MutableLiveData<List<FavoriteMovie>>(favoriteMovies)
private val watchListMovies = mutableListOf<WatchListMovie>()
private val watchListMoviesLiveData = MutableLiveData<List<WatchListMovie>>(watchListMovies)
override suspend fun insertFavoriteMovie(movie: FavoriteMovie) {
favoriteMovies.add(movie)
refreshFavoriteMovieData()
}
override suspend fun deleteFavoriteMovie(api_id: Int) {
val item = favoriteMovies.find { it.api_id == api_id }
favoriteMovies.remove(item)
refreshFavoriteMovieData()
}
override fun getFavoriteMovies(): LiveData<List<FavoriteMovie>> {
return favoriteMoviesLiveData
}
override suspend fun checkIsInFavorites(api_id: Int): Boolean {
return favoriteMovies.find { it.api_id == api_id } != null
}
private fun refreshFavoriteMovieData() {
favoriteMoviesLiveData.postValue(favoriteMovies)
}
}
And my test class like this:
#MediumTest
#HiltAndroidTest
#ExperimentalCoroutinesApi
class FavoriteScreenTest {
#get:Rule
var hiltRule = HiltAndroidRule(this)
#get:Rule
var instantTaskExecutorRule = InstantTaskExecutorRule()
#Inject
lateinit var fragmentFactory: FragmentFactory
lateinit var favoriteMovieResult: FavoriteMovie
lateinit var testViewModel: FavoriteScreenViewModel
lateinit var testDetailViewModel: DetailScreenViewModel
#Before
fun setup() {
hiltRule.inject()
favoriteMovieResult = FavoriteMovie(
"a",
"a",
1,
"a",
"a",
"a",
"2023-01-01",
"a",
"1",
"1",
1,
1
)
testViewModel = FavoriteScreenViewModel(FakeMovieRepositoryTest())
testDetailViewModel = DetailScreenViewModel(FakeMovieRepositoryTest())
testDetailViewModel.insertFavoriteMovie(favoriteMovieResult)
}
#Test
fun testWatchListVisibilityTrue() {
val navController = Mockito.mock(NavController::class.java)
launchFragmentInHiltContainer<FavoriteScreen>(factory = fragmentFactory) {
Navigation.setViewNavController(requireView(), navController)
viewModel = testViewModel
detailViewModel = testDetailViewModel
}
}
}
Hi I am trying to link my sharedpreference manager via dagger and inject it into my fragment but It keeps saying it has not been initalized yet how would I go around to fix this?
Code snippet:
lass HomeFragment : Fragment() {
private lateinit var binding: FragmentHomeBinding
private lateinit var viewModel: HomeFragmentVM
#Inject
lateinit var spManager: SPManager
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
binding = FragmentHomeBinding.inflate(inflater, container, false)
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
val repository = Repository()
val viewModelFactory = HomeFragmentVMFactory(repository)
viewModel = ViewModelProvider(this, viewModelFactory)[HomeFragmentVM::class.java]
getUserList()
}
private fun getUserList() {
var retrofit = RetrofitClient.getInstance()
var apiInterface = retrofit.create(SimpleApi::class.java)
lifecycleScope.launchWhenCreated {
try {
val response = apiInterface.getPost()
if (response.isSuccessful) {
val products = response.body()!!.data.toString()
if (response.body()?.data?.size!! <= 0) {
} else {
if (spManager.getProductID().isEmpty())
{
spManager.saveProductID(response.body()!!.productid.toLong())
}
txtData.text = response.body()?.data.toString()
}
} else {
}
}catch (Ex: Exception){
Log.e("Error",Ex.localizedMessage)
}
}
}
}
The error does not cause a crash to the app but it comes up in my logs as lateinit property spManager has not been initalized.
I have a problem because I am trying to add an onReceiptsAdd () function to onStart () but I don't know how to make it work. how to add onReceiptsAdd () to onStart ()
here is my code:
class ScanFragment : Fragment(), OnReceiptsItemAdd {
private var _binding: FragmentScanBinding? = null
private val binding get() = _binding!!
private val scanVm by viewModels<ScanViewModel>()
private val adapter = ReceiptsAdapter(this)
private val SCAN_DEBUG = "SCAN_DEBUG"
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?,
): View {
_binding = FragmentScanBinding.inflate(inflater, container, false)
// Inflate the layout for this fragment
return binding.root
}
override fun onStart() {
super.onStart()
val value = binding.etBlank.text?.trim().toString()
val from = binding.etBlank.text?.trim().toString()
val image = binding.etBlank.text?.trim().toString()
val rootRef = FirebaseFirestore.getInstance()
val usersRef = rootRef.collection("receipts")
val auth = FirebaseAuth.getInstance()
auth.currentUser?.apply {
usersRef.document(uid).set(mapOf(
"id" to uid,
"from" to from,
"value" to value,
"image" to image,
)).addOnCompleteListener { task ->
if (task.isSuccessful) {
Log.d("TAG", "User successfully added.")
} else {
Log.d("TAG", task.exception!!.message!!)
}
}
}
}
override fun onReceiptsAdd(receipts: dataReceipts, position: Int) {
scanVm.addFavReceipt(receipts)
}
}
I'm trying to use paging with retrofit. But i have to make more than one calls for filtering or changing the rover(it is a nasa pictures app). But my app making Api call every second and that crashs it. I tried it with livedata also with flow but it didnt work.
There are three fragment execute same prosesses in tab layout with View Pager these are curiosityFragment, SpiritFragment, OpportunityFragment
object RetrofitInstance {
private val moshi = Moshi.Builder()
.add(KotlinJsonAdapterFactory())
.build()
private val retrofit by lazy {
Retrofit.Builder()
.baseUrl(BASE_URL)
.addConverterFactory(MoshiConverterFactory.create(moshi)).baseUrl(BASE_URL)
.addCallAdapterFactory(RxJava2CallAdapterFactory.create())
.build()
}
val api : SimpleApi by lazy {
retrofit.create(SimpleApi::class.java)
}}
interface SimpleApi {
#GET("mars-photos/api/v1/rovers/{name}/photos?sol=1000&api_key=DEMO_KEY")
fun getPhotos(#Path("name") name : String,
#Query("per_page")per_page:Int?,
#Query("page") page: Int):
Single<AllPhotos>
class DataSource(name:String) : RxPagingSource<Int,Photo>(){
var name :String
init{
this.name = name
}
override fun getRefreshKey(state: PagingState<Int, Photo>): Int? {
return null
}
override fun loadSingle(params: LoadParams<Int>): Single<LoadResult<Int, Photo>> {
val page = params.key ?:1
return RetrofitInstance.api.getPhotos(name,params.loadSize,page)
.subscribeOn(Schedulers.io())
.map {toLoadResult(it,page)}
.onErrorReturn { LoadResult.Error(it) }
}
private fun toLoadResult(data: AllPhotos,page : Int):LoadResult<Int,Photo>{
return LoadResult.Page(
data = data.photos,
prevKey = if(page==1) null else page-1,
nextKey = page.plus(1)
)
}
}
class ViewModelDataSource (application: Application):AndroidViewModel(application) {
fun getPhotosLiveData(name : String):LiveData<PagingData<Photo>>{
return Pager(
config = PagingConfig(pageSize = 10),
pagingSourceFactory = {
DataSource(name)
},
).liveData
}
}
class CuriosityFragment : Fragment() {
private lateinit var binding:FragmentCuriosityBinding
lateinit var mApiViewModel : ViewModelDataSource
private lateinit var curiosityAdapter: AdapterCuriosity
var hasLoadedOnce = false
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
binding = FragmentCuriosityBinding.inflate(inflater,container,false)
mApiViewModel = ViewModelProvider(this).get(ViewModelDataSource::class.java)
setupRecyclerView()
loaddata()
return binding.root
}
fun setupRecyclerView(){
curiosityAdapter = AdapterCuriosity()
binding.recyclerViewCriosity.apply {
adapter = curiosityAdapter
layoutManager = LinearLayoutManager(requireContext())
}
}
fun loaddata(){
mApiViewModel.getPhotosLiveData("curiosity").observe(viewLifecycleOwner, Observer {
curiosityAdapter.submitData(this.lifecycle,it)})
}
class OpportunityFragment : Fragment() {
private lateinit var binding: FragmentOppurtunityBinding
private lateinit var mApiViewModel: ViewModelDataSource
private lateinit var recyclerView: RecyclerView
private lateinit var opportunityAdapter:AdapterOpportunity
var hasLoadedOnce = false
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
binding = FragmentOppurtunityBinding.inflate(inflater, container, false)
recyclerView = binding.recyclerViewOpportunity
mApiViewModel = ViewModelProvider(this).get(ViewModelDataSource::class.java)
setupRecyclerView()
loaddata()
setHasOptionsMenu(true)
return binding.root
}
fun setupRecyclerView(){
opportunityAdapter = AdapterOpportunity()
binding.recyclerViewOpportunity.apply {
adapter = opportunityAdapter
layoutManager = LinearLayoutManager(requireContext())
}
}
fun loaddata(){
mApiViewModel.getPhotosLiveData("opportunity").observe(viewLifecycleOwner, Observer {
opportunityAdapter.submitData(this.lifecycle,it)})
}
class SpiritFragment : Fragment() {
private lateinit var binding: FragmentSpiritBinding
private lateinit var mApiViewModel : ViewModelDataSource
private lateinit var spiritAdapter:AdapterSpirit
lateinit var recyclerView: RecyclerView
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
binding = FragmentSpiritBinding.inflate(inflater, container, false)
recyclerView = binding.recyclerViewSpirit
mApiViewModel = ViewModelProvider(this).get(ViewModelDataSource::class.java)
setupRecyclerView()
loaddata()
setHasOptionsMenu(true)
return binding.root
}
fun setupRecyclerView(){
spiritAdapter = AdapterSpirit()
binding.recyclerViewSpirit.apply {
adapter = spiritAdapter
layoutManager = LinearLayoutManager(requireContext())
}
}
fun loaddata(){
mApiViewModel.getPhotosLiveData("spirit").observe(viewLifecycleOwner, Observer {
spiritAdapter.submitData(this.lifecycle,it)})
}
this is my Retrofit retrieve code :
the error came from this line ==>> val respon = response.body()!!
( java.lang.NullPointerException
at com.xfath.hormart.fragments.homechildfragments.ChildHomeFragment$getProducts$1.onResponse(ChildHomeFragment.kt:124 )
class ChildHomeFragment : Fragment(){
private lateinit var s: SharedPreference
private var listImageSliderHomes: ArrayList<ImageSliderHome> = ArrayList()
private var listProducts: ArrayList<Products> = ArrayList()
private lateinit var sliderView: SliderView
private lateinit var rvProductBaru: RecyclerView
private lateinit var rvElektronik: RecyclerView
private lateinit var rvPribadi: RecyclerView
private lateinit var ivNotif: ImageButton
private lateinit var uidhome: TextView
private lateinit var svhome: NestedScrollView
private lateinit var mSearchView: SearchView
private lateinit var mSwipeRefreshLayout: SwipeRefreshLayout
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
getSlideImage()
}
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
val view = inflater.inflate(R.layout.fragment_child_home, container, false)
init(view)
getProducts()
swipeRefresh()
s = SharedPreference(activity!!)
return view
}
private fun getProducts() {
ApiConfig.instanceRetrofit.getproduct().enqueue(object : Callback<ResponseModel> {
override fun onResponse(call: Call<ResponseModel>, response: Response<ResponseModel>) {
val respon = response.body()!!
if (respon.success == 1) {
listProducts = respon.products
displayProduct()
}
}
override fun onFailure(call: Call<ResponseModel>, t: Throwable) {
Log.v(TAG, "Error:" + t.message)
}
})
}
private fun displayProduct() {
val layoutManager = LinearLayoutManager(activity)
layoutManager.orientation = LinearLayoutManager.HORIZONTAL
rvProductBaru.adapter = ProductAdapter(listProducts)
rvProductBaru.layoutManager = layoutManager
}
}
thanks in advance..
This is meant that the response.body() returns null.
You should always do null checking for it or you can use let to avoid NPE.
PS: Avoid using !! in kotlin as it may throw NPE when the variable is null.
response.body()?.let { respon ->
if (respon.success == 1) {
listProducts = respon.products
displayProduct()
}
}