I've been facing an issue when trying to run instrumented tests for a fragment.
With the code below, the test starts but looks like it never launches the fragment, gets stuck on loading. I need to cancel it in order to stop running.
It appears to be something related to the Adapter, but I'm not sure.
class HomeFragment(
private val viewModel: HomeViewModel
) : BaseFragment<FragmentHomeBinding>(R.layout.fragment_home) {
override val shouldShowToolbar = false
override val shouldShowBottomNavigationView = true
private val authorAdapter = AuthorAdapter {
findNavController().navigate(
R.id.action_homeFragment_to_booksFragment,
bundleOf(ARG_SEARCH_TERM to it)
)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
initAdapter()
initObservers()
viewModel.fetchGreatestAuthors()
}
private fun initAdapter() {
with(binding.fragmentHomeAuthorsRecyclerView) {
addItemDecoration(DividerItemDecoration(context, LinearLayout.VERTICAL))
addItemDecoration(DividerItemDecoration(context, LinearLayout.HORIZONTAL))
adapter = authorAdapter
}
}
private fun initObservers() {
viewModel.getGreatestAuthorsLiveData().observe(viewLifecycleOwner) {
authorAdapter.setList(it)
}
viewModel.getLoadingLiveData().observe(viewLifecycleOwner) {
binding.loadingProgressBar.isVisible = it
}
}
}
BaseFragment and the adapter:
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
return DataBindingUtil.inflate<VB>(
inflater,
layoutRes,
container,
false
).run {
binding = this
root
}
}
class AuthorAdapter(
private val onItemClick: (id: String) -> Unit
) : RecyclerView.Adapter<ViewHolder>() {
private var list: List<Author> = emptyList()
fun setList(list: List<Author>) {
this.list = list
notifyDataSetChanged()
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): AuthorViewHolder {
return DataBindingUtil.inflate<ItemAuthorBinding>(
LayoutInflater.from(parent.context),
R.layout.item_author,
parent,
false
).run {
AuthorViewHolder(this)
}
}
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
(holder as? AuthorViewHolder)?.bind(list[position])
}
override fun getItemCount() = list.size
inner class AuthorViewHolder(private val binding: ItemAuthorBinding) :
ViewHolder(binding.root) {
fun bind(item: Author) {
binding.author = item
binding.root.setOnClickListener { onItemClick(item.fullName) }
}
}
}
The test class that is running into the issue:
@RunWith(AndroidJUnit4::class)
class HomeFragmentTest {
private lateinit var viewModelMock: HomeViewModel
@get:Rule
val instantTestExecutorSchedule = InstantTaskExecutorRule()
private val authorsLiveDataMock = MutableLiveData<List<Author>>()
private val loadingLiveDataMock = MutableLiveData<Boolean>()
@Before
fun setup() {
setupDataBindingMocks()
viewModelMock = mockk<HomeViewModel> {
every { getGreatestAuthorsLiveData() } returns authorsLiveDataMock
every { fetchGreatestAuthors() } returns run { authorsLiveDataMock.postValue(listOf()) }
every { getLoadingLiveData() } returns loadingLiveDataMock
}
}
private fun setupDataBindingMocks() {
mockkStatic(DataBindingUtil::class)
every {
DataBindingUtil.inflate<ItemAuthorBinding>(
any(),
any(),
any(),
any()
)
} returns mockk<ItemAuthorBinding>().apply {
every { root } returns mockk()
}
}
@Test
fun whenViewCreated_shouldDisplayExpectedViews() {
launchFragmentInContainer<HomeFragment>(
factory = MainFragmentFactory(viewModelMock)
)
assertViewDisplayed(
R.id.fragmentHomeAuthorsTitle,
)
}
If mockk() is replaced with spyk(), then it runs but throws the following error:
io.mockk.MockKException: Can't instantiate proxy via default constructor for class ItemAuthorBinding
It turns out the issue was a missing parameter (themeResId) in the launchFragmentInContainer method.
Note: Your fragment might require a theme that the test activity doesn't use by default. You can provide your own theme as an additional argument to launch() and launchInContainer().