Possible to mock/test Android ViewBinding interactions? - android

currently trying to test the interactions with ViewBinding classes in Unit Tests
"Invalid Input" should {
"disable the LoginButton" {
val viewBinding: FrLoginBinding = mockk()
InvalidInputViewStateBinder.bind(InvalidInput, viewBinding)
verify { viewBinding.loginButton.isEnabled = false }
}
}
something like this is what i had in mind. The Views in the ViewBinding are public final Properties and cannot easily be mocked. At least i'm unable to. Passing a View mock to create the ViewBinding also doesn't work, as i'd have to mock findViewById for it.
Has anyone tried this out and got it to work?

I came across this same issue. Here is how I resolved it
#RunWith(PowerMockRunner::class)
#PrepareForTest(MyLayoutBinding::class)
class MyTestClass {
#Mock
lateinit var mMockViewBinding: MyLayoutBinding
#Mock
lateinit var mMockView: View
#Mock
lateinit var mMockTitleTv: TextView
#Mock
lateinit var mMockRootView: ConstraintLayout
#Before
fun setup() {
MockitoAnnotations.initMocks(this)
PowerMockito.mockStatic(MyLayoutBinding::class.java)
whenever(MyLayoutBinding.bind(mMockView)).thenReturn(mMockViewBinding)
// Use Whitebox for each view component in the layout.
Whitebox.setInternalState(mMockBinding, "title", mMockTitleTv)
// Because 'getRoot' is part of the ViewBinding interface, we just mock the method.
whenever(mMockBinding.root).thenReturn(mMockRootView)
}
}
Use Whitebox to set the properties (i.e. the views by id) and mock the getRoot() interface method to set the root to your mocked root view.

Related

Android - mockito stub simple variable (not method)

I have a class with multiple variables inside.
SomeViewModel has a boolean variable defaulted to false,
var booleanVariable = false
SomeViewModel depends on SomeDataModel,
var dataModel: SomeDataModel? = null
Test class
#RunWith(MockitoJUnitRunner::class)
class TestClass {
#Mock lateinit var someViewModel: SomeViewModel
#Mock lateinit var someDataModel: SomeDataModel
#Before
fun setup() {
when(someViewModel.booleanVariable).thenReturn(true)
when(someViewModel.dataModel).thenReturn(someDataModel)
}
#Test
fun shouldShowImportPolicyTest() {
someViewModel.booleanVariable // return FALSE, not true as stubbed
someViewModel.dataModel // always returns NULL
}
}
if I stub a method it works fine. what am I doing wrong here?
You can mock member variables if they are not final with Mockito.
You have 3 choices here:
make them open
use the dependency mockito-inline to allow the mocking on final classes/fields
use the Kotlin all-open compiler plugin to make the class and its fields open in tests (https://kotlinlang.org/docs/reference/compiler-plugins.html)

Android Presenter Testing IllegalStateException captor.capture() must not be null

I have simple application based on mvp. Write test for presenter. Used Mockito for mock data. I catch view callback data (ArrayList) with ArgumentCaptor. My Test class
#RunWith(MockitoJUnitRunner::class)
class MainPresenterTest{
#Mock
lateinit var view:MainView
#Mock
lateinit var context:Context
#InjectMocks
lateinit var presenter: MainPresenter
#Captor
lateinit var captor: ArgumentCaptor<ArrayList<News>>
#Before
fun init(){
MockitoAnnotations.initMocks(this)
}
#Test
fun success(){
presenter.loadNews()
Mockito.verify<MainView>(view).onSuccess(captor.capture())
var data = captor.value
Mockito.verify(view).onSuccess(data)
Mockito.verify(view,never()).onError("")
}
}
Main View
interface MainView{
fun onSuccess(n:ArrayList<News>)
fun onError(e:String)
}
But throw
java.lang.IllegalStateException: captor.capture() must not be null
Example of correct verification:
verify(mock).doSomething()
Also, this error might show up because you verify either of: final/private/equals()/hashCode() methods.
Those methods *cannot* be stubbed/verified.
Mocking methods declared on non-public parent classes is not supported.
You actually don't need to define it as a class lateinit variable. In your test method, define a method variable like this
#Test
fun success(){
presenter.loadNews()
val captor: ArgumentCaptor<ArrayList<*>> = ArgumentCaptor.forClass(ArrayList::class.java)
Mockito.verify<MainView>(view).onSuccess(captor.capture())
var data = captor.value
Mockito.verify(view).onSuccess(data)
Mockito.verify(view,never()).onError("")
}
}
Also, you should assert the data from the captor. Instead of this
Mockito.verify(view).onSuccess(data)
do something like this
assertEquals("x", data.size())

LiveData unit testing error when using postValue in init block

I'm trying to write a unit test for a view model using live data.
LoginViewModel.kt
class LoginViewModel #Inject constructor(
val context: Context
): ViewModel() {
val username = MutableLiveData<String>()
val password = MutableLiveData<String>()
val isLoginButtonEnabled = MediatorLiveData<Boolean>().apply {
fun combineLatest(): Boolean {
return !(username.value.isNullOrEmpty() || password.value.isNullOrEmpty())
}
addSource(username) { this.value = combineLatest() }
addSource(password) { this.value = combineLatest() }
}
init {
username.postValue("test")
password.postValue("test")
}
}
LoginViewModelTest.kt
#RunWith(MockitoJUnitRunner::class)
class LoginViewModelTest {
#Rule
#JvmField
val instantTaskExecutorRole = InstantTaskExecutorRule()
private val context = mock(Context::class.java)
private val loginViewModel = LoginViewModel(context)
#Test
fun loginButtonDisabledOnEmptyUsername() {
val observer = mock<Observer<Boolean>>()
loginViewModel.isLoginButtonEnabled.observeForever(observer)
loginViewModel.username.postValue("")
verify(observer).onChanged(false)
}
}
My unit test throws the following exception at the line username.postValue("test"):
java.lang.RuntimeException: Method getMainLooper in android.os.Looper not mocked. See http://g.co/androidstudio/not-mocked for details.
The InstantTaskExecutorRule should provide an execution context when using live data, however it doesn't work when initializing live data in the init-block. When omitting the init-block it works as desired, but i need the possibility to initialize live data variables.
Is there any way to make the live data initialization work when unit testing view models?
I managed to unit test my ViewModel that was using LiveData using mentioned rula - InstantTaskExecutorRule. But in my case the rule val declaration was a bit different:
#Suppress("unused")
#get:Rule
val instantTaskExecutorRule: InstantTaskExecutorRule = InstantTaskExecutorRule()
Edit:
#Before
#Throws(Exception::class)
fun prepare() {
MockitoAnnotations.initMocks(this)
}
Edit2:
For some weird reason I cannot reproduce this :)
Also, I think that the problem could be because of the way you're initializing your ViewModel -
private val loginViewModel = LoginViewModel(context)
I assume that it initializes too early, thus it's init block gets called too early too. Maybe it's reasonable to create it in the #Before method ? Like:
private lateinit var viewModel: LoginViewModel
#Before
#Throws(Exception::class)
fun prepare() {
loginViewModel = LoginViewModel(context)
}
I was seeing a similar issue when setting a LiveData value during the ViewModel's init. Demigod's solution pointed me in the right direction, but I wanted to explain a bit about what was going on and why in the lifecycle of the testing process.
When you have a ViewModel that sets the LiveData during init, it will be run as soon as the view model is initialized. When you initialize the view model in your unit test using val viewModel = MyViewModel(), that view model is instantiated at the same time as the test class is initialized. The problem there is any rules you may have are initialized at the same time, but are not actually run until after the class is completely initialized, so your ViewModel.init() is happening before the rules actually take effect. This means your live data isn't working on an instant executor, any Rx observables aren't being run on replaced schedulers, etc. So ultimately there are two ways of solving for this:
Define the view model as a lateinit var and initialize the view model as a in the #Before method of your test, which runs after rules are applied, or
Define the view model as a val viewModel by lazy { MyViewModel() }, which won't be run until you actually start calling it in your tests.
I prefer option 2 because it also allows me to set up any test-case-specific preconditions before my view model is ever initialized, and I don't have to do repetitive initialization code (which could be quite verbose) inside every test that requires it.
I had a similar issue and the answer provided by Demigod was not solving it. I finally found out where the devil was hiding so I share it here : my init block was set before the liveData initialization, which works fine when running the app, but not when running tests !
class MyViewModel : ViewModel() {
// init { // <-- Do not put the init block before the liveData
// _myLiveData.postValue("First")
// }
private val _myLiveData: MutableLiveData<String> = MutableLiveData()
val myLiveData: LiveData<String>
get() = _myLiveData
init {
_myLiveData.postValue("First")
}
}

How to correctly mock ViewModel on androidTest

I'm currently writing some UI unit tests for a fragment, and one of these #Test is to see if a list of objects is correctly displayed, this is not an integration test, therefore I wish to mock the ViewModel.
The fragment's vars:
class FavoritesFragment : Fragment() {
private lateinit var adapter: FavoritesAdapter
private lateinit var viewModel: FavoritesViewModel
#Inject lateinit var viewModelFactory: FavoritesViewModelFactory
(...)
Here's the code:
#MediumTest
#RunWith(AndroidJUnit4::class)
class FavoritesFragmentTest {
#Rule #JvmField val activityRule = ActivityTestRule(TestFragmentActivity::class.java, true, true)
#Rule #JvmField val instantTaskExecutorRule = InstantTaskExecutorRule()
private val results = MutableLiveData<Resource<List<FavoriteView>>>()
private val viewModel = mock(FavoritesViewModel::class.java)
private lateinit var favoritesFragment: FavoritesFragment
#Before
fun setup() {
favoritesFragment = FavoritesFragment.newInstance()
activityRule.activity.addFragment(favoritesFragment)
`when`(viewModel.getFavourites()).thenReturn(results)
}
(...)
// This is the initial part of the test where I intend to push to the view
#Test
fun whenDataComesInItIsCorrectlyDisplayedOnTheList() {
val resultsList = TestFactoryFavoriteView.generateFavoriteViewList()
results.postValue(Resource.success(resultsList))
(...)
}
I was able to mock the ViewModel but of course, that's not the same ViewModel created inside the Fragment.
So my question really, has someone done this successfully or has some pointers/references that might help me out?
Also, I've tried looking into the google-samples but with no luck.
For reference, the project can be found here: https://github.com/JoaquimLey/transport-eta/
Within your test setup you'll need to provide a test version of the FavoritesViewModelFactory which is being injected in the Fragment.
You could do something like the following, where the Module will need to be added to your TestAppComponent:
#Module
object TestFavoritesViewModelModule {
val viewModelFactory: FavoritesViewModelFactory = mock()
#JvmStatic
#Provides
fun provideFavoritesViewModelFactory(): FavoritesViewModelFactory {
return viewModelFactory
}
}
You'd then be able to provide your Mock viewModel in the test.
fun setupViewModelFactory() {
whenever(TestFavoritesViewModelModule.viewModelFactory.create(FavoritesViewModel::class.java)).thenReturn(viewModel)
}
I have solved this problem using an extra object injected by Dagger, you can find the full example here: https://github.com/fabioCollini/ArchitectureComponentsDemo
In the fragment I am not using directly the ViewModelFactory, I have defined a custom factory defined as a Dagger singleton:
https://github.com/fabioCollini/ArchitectureComponentsDemo/blob/master/uisearch/src/main/java/it/codingjam/github/ui/search/SearchFragment.kt
Then in the test I replace using DaggerMock this custom factory using a factory that always returns a mock instead of the real viewModel:
https://github.com/fabioCollini/ArchitectureComponentsDemo/blob/master/uisearchTest/src/androidTest/java/it/codingjam/github/ui/repo/SearchFragmentTest.kt
Look like, you use kotlin and koin(1.0-beta).
It is my decision for mocking
#RunWith(AndroidJUnit4::class)
class DashboardFragmentTest : KoinTest {
#Rule
#JvmField
val activityRule = ActivityTestRule(SingleFragmentActivity::class.java, true, true)
#Rule
#JvmField
val executorRule = TaskExecutorWithIdlingResourceRule()
#Rule
#JvmField
val countingAppExecutors = CountingAppExecutorsRule()
private val testFragment = DashboardFragment()
private lateinit var dashboardViewModel: DashboardViewModel
private lateinit var router: Router
private val devicesSuccess = MutableLiveData<List<Device>>()
private val devicesFailure = MutableLiveData<String>()
#Before
fun setUp() {
dashboardViewModel = Mockito.mock(DashboardViewModel::class.java)
Mockito.`when`(dashboardViewModel.devicesSuccess).thenReturn(devicesSuccess)
Mockito.`when`(dashboardViewModel.devicesFailure).thenReturn(devicesFailure)
Mockito.`when`(dashboardViewModel.getDevices()).thenAnswer { _ -> Any() }
router = Mockito.mock(Router::class.java)
Mockito.`when`(router.loginActivity(activityRule.activity)).thenAnswer { _ -> Any() }
StandAloneContext.loadKoinModules(hsApp + hsViewModel + api + listOf(module {
single(override = true) { router }
factory(override = true) { dashboardViewModel } bind ViewModel::class
}))
activityRule.activity.setFragment(testFragment)
EspressoTestUtil.disableProgressBarAnimations(activityRule)
}
#After
fun tearDown() {
activityRule.finishActivity()
StandAloneContext.closeKoin()
}
#Test
fun devicesSuccess(){
val list = listOf(Device(deviceName = "name1Item"), Device(deviceName = "name2"), Device(deviceName = "name3"))
devicesSuccess.postValue(list)
onView(withId(R.id.rv_devices)).check(ViewAssertions.matches(ViewMatchers.isCompletelyDisplayed()))
onView(withId(R.id.rv_devices)).check(matches(hasDescendant(withText("name1Item"))))
onView(withId(R.id.rv_devices)).check(matches(hasDescendant(withText("name2"))))
onView(withId(R.id.rv_devices)).check(matches(hasDescendant(withText("name3"))))
}
#Test
fun devicesFailure(){
devicesFailure.postValue("error")
onView(withId(R.id.rv_devices)).check(ViewAssertions.matches(ViewMatchers.isCompletelyDisplayed()))
Mockito.verify(router, times(1)).loginActivity(testFragment.activity!!)
}
#Test
fun devicesCall() {
onView(withId(R.id.rv_devices)).check(ViewAssertions.matches(ViewMatchers.isCompletelyDisplayed()))
Mockito.verify(dashboardViewModel, Mockito.times(1)).getDevices()
}
}
In the example you provided, you are using mockito to return a mock for a specific instance of your view model, and not for every instance.
In order to make this work, you will have to have your fragment use the exact view model mock that you have created.
Most likely this would come from a store or a repository, so you could put your mock there? It really depends on how you setup the acquisition of the view model in your Fragments logic.
Recommendations:
1) Mock the data sources the view model is constructed from or
2) add a fragment.setViewModel() and Mark it as only for use in tests. This is a little ugly, but if you don't want to mock data sources, it is pretty easy this way.
One could easily mock a ViewModel and other objects without Dagger simply by:
Create a wrapper class that can re-route calls to the ViewModelProvider. Below is the production version of the wrapper class that simply passes the calls to the real ViewModelProvider which is passed in as a parameter.
class VMProviderInterceptorImpl : VMProviderInterceptor { override fun get(viewModelProvider: ViewModelProvider, x: Class<out ViewModel>): ViewModel {
return viewModelProvider.get(x)
}
}
Adding getters and setters for this wrapper object to the Application class.
In the Activity rule, before an activity is launched, swap out the real wrapper with a mocked wrapper that does not route the get ViewModel call to the real viewModelProvider and instead provides a mocked object.
I realize this is not as powerful as dagger but the simplicity is attractive.

Testing Android Kotlin app - Mockito with Dagger injects null

I'm learning testing on Android with Mockito and Robolectric. I created very simple app in Kotlin with RxJava and Dagger2, using Clean Architecture. Everything works well on device, but I can't make my test pass. Here is my LoginPresenterTest:
#RunWith(RobolectricGradleTestRunner::class)
#Config(constants = BuildConfig::class)
public class LoginPresenterTest {
private lateinit var loginPresenter: LoginPresenter
#Rule #JvmField
public val mockitoRule: MockitoRule = MockitoJUnit.rule()
#Mock
private lateinit var mockContext: Context
#Mock
private lateinit var mockLoginUseCase: LoginUseCase
#Mock
private lateinit var mockLoginView: LoginView
#Mock
private lateinit var mockCredentialsUseCase: GetCredentials
#Before
public fun setUp() {
loginPresenter = LoginPresenter(mockCredentialsUseCase, mockLoginUseCase)
loginPresenter.view = mockLoginView
}
#Test
public fun testLoginPresenterResume(){
given(mockLoginView.context()).willReturn(mockContext)
loginPresenter.resume();
}
}
LoginPresenter contructor:
class LoginPresenter #Inject constructor(#Named("getCredentials") val getCredentials: UseCase,
#Named("loginUseCase") val loginUseCase: LoginUseCase) : Presenter<LoginView>
in loginPresenter.resume() i have:
override fun resume() {
getCredentials.execute(GetCredentialsSubscriber() as DefaultSubscriber<in Any>)
}
And, finally, GetCredentials:
open class GetCredentials #Inject constructor(var userRepository: UserRepository,
threadExecutor: Executor,
postExecutionThread: PostExecutionThread):
UseCase(threadExecutor, postExecutionThread) {
override fun buildUseCaseObservable(): Observable<Credentials> = userRepository.credentials()
}
The problem is, that every field in GetCredentials is null. I think I miss something (I took pattern from this project: https://github.com/android10/Android-CleanArchitecture), but I can't find what is it. Does anyone know what may cause this?
You're using a mock instance of GetCredentials (#Mock var mockCredentialsUseCase: GetCredentials) that's why you have nulls in its fields. It's rarely a good idea to mock everything apart from the main class under test (LoginPresenter). One way to think of this is to divide the dependencies into peers and internals. I would rewrite the test to something like:
inline fun <reified T:Any> mock() : T = Mockito.mock(T::class.java)
#RunWith(RobolectricGradleTestRunner::class)
#Config(constants = BuildConfig::class)
public class LoginPresenterTest {
val mockContext:Context = mock()
val mockLoginView:LoginView = mock().apply {
given(this.context()).willReturn(mockContext)
}
val userRepository:UserRepository = mock() // or some in memory implementation
val credentials = GetCredentials(userRepository, testThreadExecutor, testPostThreadExecutor) // yes, let's use real GetCredentials implementation
val loginUseCase = LoginUseCase() // and a real LoginUseCase if possible
val loginPresenter = LoginPresenter(credentials, loginUseCase).apply {
view = mockLoginView
}
#Test
public fun testLoginPresenterResume(){
given(mockLoginView.context()).willReturn(mockContext)
loginPresenter.resume();
// do actual assertions as what should happen
}
}
As usual you need to think about what you're testing. The scope of the test does not have to be limited to a single class. It's often easier to think of features you're testing instead of classes (like in BDD). Above all try to avoid tests like this - which in my opinion adds very little value as a regression test but still impedes refactoring.
PS. Roboelectric add helper functions for context

Categories

Resources