Android test Fragmen with mock ViewModel using Hilt - android

I´m developing an app using Hilt, all works fine but when I try to run some Espresso test on a device running on below Android P I have encountered an issue.
The problem comes when I try to mock (using Mockk) the ViewModel so I can unit test my Fragment. When the Fragment will try to instanciate te ViewModel I got a NullPointerException when the ViewModel is being created. The NPE is thrown on the method setTagIfAbsent. The problem is that this method is package private as you can see on ViewModel source code, so it can not be mocked on Android < P.
I have tried by using the Kotlin All-Open plugin, it has helped on mocking the ViewModel and stubing it public methods. I try to stub the setTagIfAbsent by using the mockk private stubbing, like this:
every{
myViewModelMock["setTagIfAbsent"](any<String>,any())
} answers {secondArg()}
But when setTagIfAbsent is called, the real method is invoked, throwing the NPE because the ViewModel.mBagOfTags is null because the class is a mock.
The rest of the code is the following:
ViewModel:
#OpenForTesting
#HiltViewModel
class MyViewModel #Inject constructor MyViewModel(private val dependency: Dependency): ViewModel(){
//Rest of the code
}
Fragment:
#AndroidEntryPoint
class MyFragment: Fragment(){
private val viewModel: MyViewModel by viewModels()
//Rest of the code
}
Test class:
#HiltAndroidTest
#RunWith(AndroidJUnit4::class)
class MyFragmentTest {
#Bind
#MockK
lateinit var viewModel: MyViewModel
#get:Rule
var hiltRule = HiltAndroidRule(this)
#Before
fun prepareTest(){
MockkAnnotations.init(this)
hiltRule.inject()
launchFragmentInHiltContainer<MyFragment>()
}
#Test
fun testThatWillMakeAViewModelInvokation(){
onView(withId(R.id.button)).perform(click())
//Assume that button will make the ViewModel be called and created by the delegate
//When this happens the NPE is thrown
}
}
The method launchFragmentInHiltContainer comes from here (Hilt sample app).
If you look at the Mockk Android documentation it is said that < Android P the private methods can not be mocked (it is also said for finals, but the OpenClass plugin fix that problem).
Does anyone have an idea of how can I workaround this or how to fix the test?
Thanks in advance.

Instead of mocking setTagIfAbsent you can mock mBagOfTags using reflection on already mocked instance of ViewModel.
setInternalFieldValue(mockedViewModel, "mBagOfTags", HashMap<String, Any>())
fun setInternalFieldValue(target: Any, fieldName: String, value: Any, javaClass: Class<in Any> = target.javaClass) {
try {
val field = javaClass.getDeclaredField(fieldName)
field.isAccessible = true
field.set(target, value)
} catch (exception: NoSuchFieldException) {
val superClass = javaClass.superclass
if (superClass != null) {
setInternalFieldValue(target, fieldName, value, superClass)
} else {
throw RuntimeException("Field $fieldName is not declared in a hierarchy of this class")
}
}
}

If you use mockk(relaxed = true) this problem is solved

Related

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())

How do I inject a class that uses androidContext in an instrumented test with Koin?

One of my classes has a dependency of type Context. Before adding Koin to my project, I initialized this with a hard dependency on my Application class:
class ProfileRepository(
private var _context: Context? = null,
private var _profileRestService: IProfileRestService? = null
) : IProfileRepository {
init {
if (_context == null) {
_context = MyApplication.getInstance().applicationContext
}
}
Now, I want to use Koin to inject this dependency. This is how I've defined the module:
object AppModule {
#JvmField
val appModule = module {
single<IProfileRestService> { ProfileRestService() }
single<IProfileRepository> { ProfileRepository(androidContext(), get()) }
}
}
I'm starting Koin in the onCreate method of my Application class (which is written in Java):
startKoin(singletonList(AppModule.appModule));
I want to test this class with an instrumented test and not a unit test because I want to use the real context and not a mock. This is my test:
#RunWith(AndroidJUnit4::class)
class MyTest : KoinTest {
private val _profileRepository by inject<IProfileRepository>()
#Test
fun testSomething() {
assertNotNull(_profileRepository)
}
The test is failing with an exception:
org.koin.error.BeanInstanceCreationException: Can't create definition for 'Single [name='IProfileRepository',class='com.my.app.data.profile.IProfileRepository']' due to error :
No compatible definition found. Check your module definition
I can get it to work with a unit test if I mock the context like so:
class MyTest : KoinTest {
private val _profileRepository by inject<IProfileRepository>()
#Before
fun before() {
startKoin(listOf(AppModule.appModule)) with mock(Context::class.java)
}
#After
fun after() {
stopKoin()
}
#Test
fun testSomething() {
assertNotNull(_profileRepository)
}
How can I make it work as an instrumented test with a real context?
In place of (in Application):
startKoin(applicationContext, modules)
Use a mocked Context:
startKoin(modules) with (mock(Context::class.java))
From https://insert-koin.io/docs/1.0/documentation/koin-android/index.html#_starting_koin_with_android_context_from_elsewhere
Apparently there's no way to start Koin from a Java class and inject the application context. What that means is if one of your classes needs to get the context from the container, you must use org.koin.android.ext.android.startKoin instead of org.koin.java.standalone.KoinJavaStarter.startKoin.
Since my Application class is still written in Java, I created an object called KoinHelper with one method:
#JvmStatic
fun start(application: Application) {
application.startKoin(application, listOf(AppModule.appModule))
}
Then I called this from the onCreate method of my Application class:
KoinHelper.start(this);
Now, the instrumented test I posted in my original answer runs just fine.
Please see this issue on GitHub for more info.
Please check this section in the documentation. It says
if you need to start Koin from another Android class, you can use the
startKoin() function and provide your Android Context instance with
just like:
startKoin(androidContext, myAppModules)
So in your instrumentation test, you can pass a context while starting the Koin.
#Before
fun before() {
startKoin(InstrumentationRegistry.getContext(), listOf(AppModule.appModule))
}
Or if you want an application level context
#Before
fun before() {
startKoin(InstrumentationRegistry.getTargetContext(), listOf(AppModule.appModule))
}
The referenced documentation is for Version 1.0.1
In terms of getting the Application context in the instrumented test, you can use androidx.test.core.app.ApplicationProvider or InstrumentationRegistry.targetContext.applicationContext.
#Before
fun setUp() {
stopKoin()
loadKoinModules(testModule) with ApplicationProvider.getApplicationContext<Application>()
}
...where testModule uses androidApplication() to retrieve the Application context:
val testModule = module {
single {
ToDoDatabase.newInstance(
androidApplication(),
memoryOnly = true
)
}
single { ToDoRepository(get()) }
}
Note that my stopKoin() call is there because I was having difficulty overriding an existing module created by startKoin() in my custom Application subclass. ¯\_(ツ)_/¯
#Before
fun setUp() {
stopKoin()
startKoin {
androidContext(app) // for example ApplicationProvider.getApplicationContext<TestApplication>()
modules(module1, module2)
}
}

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")
}
}

Custom JUnit Rule allows all tests to pass no matter what

I have a file that references some static methods:
class MyViewModel {
fun test() { }
companion object {
private val MY_STRING = ResourceGrabber.grabString(R.string.blah_blah)
}
}
In my JUnit test for this file, I write some code to mock my resource grabber in setup. This compiles and runs, and the following test fails as I'd expect it to:
#PrepareForTest(ResourceGrabber::class)
#RunWith(PowerMockRunner::class)
class MyViewModelTest {
private lateinit var viewModel: MyViewModel
#Before
fun setup() {
PowerMockito.mockStatic(ResourceGrabber::class.java)
val mockResourceGrabber = Mockito.mock(ResourceGrabber::class.java)
whenever(mockResourceGrabber.grabString(Mockito.anyInt())).thenAnswer { invocation ->
val res: Int? = invocation?.arguments?.get(0) as? Int
TestResourceGrabber.grabString(res)
}
viewModel = MyViewModel()
}
#Test
fun someTest() {
// Fails, as expected.
assertEquals(2, 3)
}
}
Here is where things get weird. I recently learned about custom JUnit rules that you can use to avoid some duplicated code across tests. In this case, I don't want to have to copy and paste my resource grabber work into every single test suite that uses it, so I made a custom rule:
class ResourceGrabberRule : TestRule {
override fun apply(base: Statement?, description: Description?): Statement {
return object : Statement() {
override fun evaluate() {
PowerMockito.mockStatic(ResourceGrabber::class.java)
val mockResourceGrabber = Mockito.mock(ResourceGrabber::class.java)
whenever(mockResourceGrabber.grabString(Mockito.anyInt())).thenAnswer { invocation ->
val res: Int? = invocation?.arguments?.get(0) as? Int
TestResourceGrabber.grabString(res)
}
}
}
}
}
Below is the implementation of that. The crazy thing is that now EVERY test is passing no matter what:
#PrepareForTest(ResourceGrabber::class)
#RunWith(PowerMockRunner::class)
class MyViewModelTest {
private lateinit var viewModel: MyViewModel
#Rule
#JvmField
val resourceGrabber = ResourceGrabberRule()
#Before
fun setup() {
viewModel = MyViewModel()
}
#Test
fun someTest() {
// PASSES!!!?!?!?!?!
assertEquals(2, 3)
}
}
I'm not sure where the problem lies. I've tried building and running tests from both Android Studio and the command line. I don't know if I've implemented my rule incorrectly, or if it's an issue with the JUnit Rule connected with Powermock, or if it's an issue with Kotlin annotation processing. The tests compile and run but just pass no matter what's inside the tests themselves.
I'm open to comments about the architecture here (I'm sure the community has plenty) but I'm really looking for an explanation as to why the rule I wrote passes every test.
In your custom TestRule, you need to call base.evaluate() to continue the chain of rules https://github.com/junit-team/junit4/wiki/rules#custom-rules

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.

Categories

Resources