My view model holds a very simple recyclerview adapter
When I try to send it messages (which in turn calls notifyDatasetChanged) it throws an exception like so
java.lang.NullPointerException
at androidx.recyclerview.widget.RecyclerView$AdapterDataObservable.notifyChanged(RecyclerView.java:11996)
at
the problem is that the mObservers variable from AdapterDataObservable is null
the thing is that this extends Observable<AdapterDataObserver> which in turn defines mObservers as
protected final ArrayList<T> mObservers = new ArrayList<T>();
so basically the moment my adapter is instantiated , it will call
private final AdapterDataObservable mObservable = new AdapterDataObservable();
(which is called by the way, mObservable is not null)
which in turn should call mObservers = new ArrayList<T>();
can someone explain why this is never called? or if there is a way to get past this problem?
as a side note the adapter is not mocked it is a solid object.
Edit:
Here is the code of the tests I'm using:
class LoginViewModelTest {
private lateinit var vm: LoginViewModel
#get:Rule
val rule = InstantTaskExecutorRule()
#Before
fun setUp() {
whenever(settings.hasShownWelcome).thenReturn(false)
whenever(settings.serverIp).thenReturn("http://127.0.0.1")
//this is where the crash happens
vm = LoginViewModel(settings, service, app, TestLog, TestDispatchers) { p -> permissionGranted }
}
And below is the code that is tested:
class LoginViewModel(private val settings: ISettings, private val service: AppService, application: Application, l: ILog, dispatchers: IDispatchers, val permissionChecker: (String) -> Boolean) : BaseViewModel(application, l, dispatchers)
val stepAdapter :StepAdapter
init {
val maxSteps = calculateSteps()
//after this assignment, during the normal run, the stepAdapter.mObservable.mObservers is an empty array
//during unit tests, after this assignment it is null
stepAdapter = StepAdapter(maxSteps)
}
I fixed mine by spying on the adapter and stubbing notifyDataSetChanged.
val spyAdapter = spyk(adapter)
every { spyAdapter.notifyDataSetChanged() } returns Unit
spyAdapter.changeItems(items)
verify { spyAdapter.notifyDataSetChanged() }
Please, note that changeItems calls notifyDataSetChanged internally.
i don't know if you have already found a solution or not, but this is for other people like me who ran into a similar problem:
make the test an android test (aka instrumented test) and not a unit test.
while i cannot fully explain why, it seems that when notifying an adapter about a change (notifyItemChanged(), notifyDataSetChanged() etc.) something about the inner logic of android requires an actual RecyclerView/adapter to receive the message.
once i moved my test from the Test folder to the AndroidTest folder, the problem was fixed.
P.S.
make sure to remove your old build configuration! android studio keeps referring to the old one (in the Test folder) and if you don't remove it you will receive a classNotFound error
I solved my problem doing this:
#RunWith(RobolectricTestRunner::class)
#Config(sdk = [Build.VERSION_CODES.N, Build.VERSION_CODES.O], application = AppTest::class)
class HomeAdapterTest {
Where the AppTest is am empty class.
in my case, I use #RunWith(MockitoJUnitRunner.class) instrad of #RunWith(JUnit4.class) and work fine for me... the sample code :
Inside viewModel.fetchPokemonList() function I use adapter.notifyDatasetChanged()
#RunWith(MockitoJUnitRunner.class)
public class PokemonCardListTest {
#Rule public InstantTaskExecutorRule instantExecutorRule = new InstantTaskExecutorRule();
#Mock Context context;
#Mock LifecycleOwner lifecycleOwner;
#Mock public List<PokemonCard> pokemonCardList;
private Repository repository;
private Lifecycle lifecycle;
private PokemonCardListViewModel viewModel;
#Before
public void setUp() throws Exception {
MockitoAnnotations.initMocks(this);
pokemonCardList = Arrays.asList(
new PokemonCard("1" ,"Card1" , "Artist1"),
new PokemonCard("2" ,"Card2" , "Artist2")
);
repository = new FakeRepository(pokemonCardList);
lifecycle = new LifecycleRegistry(lifecycleOwner);
viewModel = new PokemonCardListViewModel(context , repository);
}
#Test
public void testSearch() {
viewModel.fetchPokemonList("99");
assertEquals(viewModel.getAdapter().getValue().getItemCount() , pokemonCardList.size());
viewModel.fetchPokemonList("0");
assertEquals(viewModel.getAdapter().getValue().getItemCount() , 0);
}
Related
I have written test cases for my view model. Which when I run individually or when I run the Test class. They get executed successfully. But when I run the complete androidTest package, I get this Exception
io.mockk.MockKException
Here is the code that runs successfully in isolation.
#RunWith(AndroidJUnit4::class)
class MyViewModelTest{
#Test
fun test_one(){
getInstrumentation().runOnMainSync(Runnable {
val context = ApplicationProvider.getApplicationContext<Context>()
mockkStatic(MyManager::class)
val myInterface = mockk<MyInterface>()
every { MyManager.getCommunicator() } returns myInterface
every { myInterface.context } returns context
every { myInterface.getLongFromGTM(any()) } returns 0
val viewModel = MyViewModel(context as Application)
viewModel.model = MyDataModel()
viewModel.model.isRepeatEligible = true
val res = viewModel.isRepeatEligible()
Truth.assertThat(res).isTrue()
})
}
}
This is the error I am getting while running entire androidTest package:
Here are the detailed used classes
1 .) MyManager.java
public class MyManager {
private static MyInterface myCommunicator;
public static MyInterface getCommunicator() {
if (myCommunicator == null) {
synchronized (MyManager.class) {
if (myCommunicator == null) {
Class<?> cls = Class.forName("mypackage.communicator.MyCommunicator");
myCommunicator = (MyInterface) cls.newInstance();
}
}
}
return myCommunicator;
}
}
2.) MyViewModel.kt
class MyViewModel(application: Application) : BaseViewModel(application) {
var model = MyDataModel()
private val timerDelay: Long by lazy {
myCommunicator.getLongFromGTM("key_p2m_timer_delay")
}
val timerDuration: Long by lazy {
myCommunicator.getLongFromGTM("key_p2m_timer_duration")
}
fun isRepeatEligible(): Boolean {
model.apply {
return isRepeatEligible && !isLinkBased && !isAlreadyPresent
}
}
Mocking something with MockK is not contrained to just one function. Specifically, when you mock an object with mockkStatic, the object will from then on be a mock until it is unmocked using unmockkStatic or unmockkAll.
In your case, I guess the problem arises due to the static mocking of MyManager that lets subsequent tests fail, because they do not expect the object to be mocked.
This could be solved with an "after" function (e.g. using JUnit4, a function annotated with #After) that calls unmockAll.
Alternatively, if you want to make sure that the object is only mocked locally, you can use a variant of mockkStatic that accepts a block that is the only place where the object is mocked like this:
mockkStatic(MyManager::class) {
// inside this block, MyManager is mocked
}
// MyManager is automatically unmocked after the block
Update
As mentioned in your comment, you do not call MyManager.getCommunicator() directly in MyViewModel, but via an extension property
val myCommunicator : MyInterface = MyManager.getCommunicator()
This may cause your test setup to be still in place after your test, even when you unmock MyManager, because the property myCommunicator will keep its value - the mocked interface.
This can be solved by changing your property to not be initialized with the value of MyManager.getCommunicator(), but instead you should define a getter that calls MyManager.getCommunicator():
val myCommunicator: MyInterface get() = MyManager.getCommunicator()
This way, you do always get the current value of MyManager.getCommunicator() and not the value that was set once on initialization.
See https://kotlinlang.org/docs/properties.html#getters-and-setters for details on property getters.
I am trying to unit test the following class:
class UserProfileDetailsAnalyticUseCaseImp #Inject constructor(private val analyticsProvider: AnalyticsProvider) : UserProfileDetailsAnalyticUseCase {
override fun execute(cdsCustomer: CDSCustomer) {
with(analyticsProvider) {
log(AnalyticEvent.UserId(cdsCustomer.id.toString()))
log(AnalyticEvent.UserEmail(cdsCustomer.email))
}
}
}
And this is my unit test:
class UserProfileDetailsAnalyticUseCaseImpTest {
private lateinit var userProfileDetailsAnalyticUseCaseImp: UserProfileDetailsAnalyticUseCaseImp
private val analyticsProviders: AnalyticsProvider = mock()
#Before
fun setUp() {
userProfileDetailsAnalyticUseCaseImp = UserProfileDetailsAnalyticUseCaseImp(analyticsProviders)
}
#Test
fun `should send analytic event`() {
// Arrange
val cdsCustomer = CDSCustomer(
id = Random.nextInt(0, 100000),
email = UUID.randomUUID().toString())
val userIdCapture= argumentCaptor<AnalyticEvent.UserId>()
val userEmailCapture= argumentCaptor<AnalyticEvent.UserEmail>()
// Act
userProfileDetailsAnalyticUseCaseImp.execute(cdsCustomer)
// Assert
verify(analyticsProviders, atLeastOnce()).log(userIdCapture.capture())
verify(analyticsProviders, atLeastOnce()).log(userEmailCapture.capture())
assertThat(userIdCapture.firstValue.userId).isEqualTo(cdsCustomer.id.toString())
assertThat(userEmailCapture.firstValue.email).isEqualTo(cdsCustomer.email)
}
}
The error I get is the following:
AnalyticEvent$UserId cannot be cast to AnalyticEvent$UserEmail
I am suspecting that because class under test is creating a new object for each log method they will not be the same for the verified methods in the unit test
i.e log(AnalyticEvent.UserId(cdsCustomer.id.toString()))
As a new AnaltyicEvent.UserId will be created and just for the same AnalyticProvider mock
Many thanks for any suggetions
In the documentation of ArgumentCaptor we can read that:
This utility class doesn't do any type checks. The generic
signatures are only there to avoid casting in your code.
Moreover CapturingMatcher which is used for collecting captured arguments has a method which matches all objects:
public boolean matches(Object argument) {
return true;
}
It means that it is normal behaviour and even when we specify concrete type of captor it will record all arguments passed.
Of course all these arguments have to inherit from the same base class because in other case capture method will cause compilation error.
So, both your captors record two arguments.
To fix class cast exception for your test you can assert secondValue for email.
assertThat(userEmailCapture.secondValue.email).isEqualTo(cdsCustomer.email)
You can also stop using argument captors and simply verify invocations of log method.
verify(analyticsProviders).log(AnalyticEvent.UserId(cdsCustomer.id.toString()))
verify(analyticsProviders).log(AnalyticEvent.UserEmail(cdsCustomer.email))
I'm writing a unit test. Below is my code. The architecture is MVVM using Dagger2. I'm calling the login function residing in the LoginViewModel, which is notifying the getLoginState function. The error I'm getting is:
Error:
io.mockk.MockKException: no answer found for: Observer(#8).onChanged(Success(data=))
at io.mockk.impl.stub.MockKStub.defaultAnswer(MockKStub.kt:90)
LoginViewModelClass:
fun logIn(phone: String, phoneCode: String) {
loginState.value = Outcome.success("")
}
fun getLoginState(): LiveData<Outcome<String>> = loginState
LoginViewModelTest class:
#RelaxedMockK
var SUT: LoginViewModel? = null
#Mock
var loginInteractor: LoginInteractor? = null
#Mock
var textValidator: TextValidator? = null
#Mock
var textProvider: TextProvider? = null
#Mock
var blinkUserPreferences: BlinkUserPreferences? = null
#get:Rule
var rule: TestRule = InstantTaskExecutorRule()
#RelaxedMockK
var mockObserver: Observer<Outcome<String>>? = null
#Before
fun setUp() {
MockKAnnotations.init(this, relaxUnitFun = true)
SUT = spyk(
LoginViewModel(
mockk<LoginInteractor>(),
mockk<TextValidator>(relaxed = true),
mockk<TextProvider>(),
mockk<BlinkUserPreferences>()))
mockObserver = mockk<Observer<Outcome<String>>>()
SUT!!.getLoginState().observeForever(mockObserver!!)
}
#Test
fun logIn() {
//Arrange
every {SUT!!.getLoginState().value} returns Outcome.success("")
//Act
SUT!!.logIn("89989676","89998")
//Assert
verify() { mockObserver!!.onChanged(Outcome.success("abc")) }
}
Question:
In verification, why onChanged method is not being called, or what does it mean that no answer found for Observer().onChanged, how can I notify my onChanged method so I can verify it?
After watching this: https://mockk.io/#answers. It says
specify that the matched call answers with a code block scoped with
answer scope
I just posted this:
every { mockObserver!!.onChanged(any()) } answers {}
in the following test function and it worked.
#Test
fun logIn() {
//Arrange
every { mockObserver!!.onChanged(any()) } answers {}
every {SUT!!.getLoginState().value} returns Outcome.success("abc")
//Act
SUT!!.logIn("89989676","89998")
//Assert
verify() { mockObserver!!.onChanged(Outcome.success("abc")) }
}
According to my understanding, if you mockk a function, and you want to use its particular function you must use the every expression to tell framework that it will answer, because framework needs to know that it needs to answer something.
And if you want that all behaviour functions should also be added with mock with their implementation then you must spyk your class so that it gets the behaviour as well and then you can easily use the function without using expression every.
Please note that every expression is used for many cases like to get a mocked result out of that function, or just need to tell the framework that this function should answers this.
Please correct me through comments if I'm wrong, Ill update it.
I'm trying my hand at TDD with an Android app. I'm writing it in Kotlin, and because of that I've turned to MockK for testing, but there's one thing (for now) that I haven't been able to find out how to do: test a suspend call.
I wrote a test for a LiveData value in a ViewModel, and made it work. However, when I added coroutines to the mix, I started getting the "Method getMainLooper not mocked" message.
Here's my code:
ToDoListViewModelTest.kt
class ToDoListViewModelTest {
#get:Rule
var instantExecutorRule = InstantTaskExecutorRule()
#MockK
private lateinit var toDoListLiveDataObserver: Observer<List<ToDoItem>>
#MockK
private lateinit var getToDoItemsUseCase: GetToDoItemsUseCase
#Before
fun setUp() {
MockKAnnotations.init(this)
every { toDoListLiveDataObserver.onChanged(any()) } answers { nothing }
}
#Test
fun toDoList_listItems_noItems() = runBlocking {
coEvery { getToDoItemsUseCase() } coAnswers { emptyList<ToDoItem>() }
val toDoListViewModel = ToDoListViewModel(getToDoItemsUseCase)
toDoListViewModel.toDoItemList.observeForever(toDoListLiveDataObserver)
toDoListViewModel.updateItemList()
assertEquals(0, toDoListViewModel.toDoItemList.value?.size)
}
}
ToDoListViewModel.kt
class ToDoListViewModel(private val getToDoItemsUseCase: GetToDoItemsUseCase) : ViewModel() {
private val _toDoItemList: MutableLiveData<List<ToDoItem>> = MutableLiveData()
val toDoItemList : LiveData<List<ToDoItem>> = _toDoItemList
fun updateItemList() {
viewModelScope.launch(Dispatchers.IO) {
_toDoItemList.value = getToDoItemsUseCase()
}
}
}
GetToDoItemsUseCase.kt
class GetToDoItemsUseCase {
suspend operator fun invoke(): List<ToDoItem> {
return listOf()
}
}
Things I've tried:
Adding "#RunWith(BlockJUnit4ClassRunner::class)": No change
Adding "testOptions { unitTests.returnDefaultValues = true }" to the Gradle file: The Looper error goes away, but the value coming from the LiveData is null, instead of the empty list specified in the "coEvery" call.
Calling "Dispatchers.setMain(newSingleThreadContext("UI Thread"))": Same as previous case, getting null from LiveData.
I'm not very experienced with testing, and I've run out of options. I feel I definitely need some help from the community ;)
Also, if for some reason my setup isn't the right one (should use something other than MockK, or some other testing framework...), please comment on that too. I still have much to learn regarding this.
Use postValue _toDoItemList.postValue(getToDoItemsUseCase())
Based on the documentation:
setValue():
Sets the value. If there are active observers, the value will be
dispatched to them. This method must be called from the main thread.
postValue():
Posts a task to a main thread to set the given value. If you called
this method multiple times before a main thread executed a posted
task, only the last value would be dispatched.
I am trying to do an unit test for an android app and I need to get a string from res.string resources. The class that I want to test is a POJO class. I am doing the app in two languages, due to this, I need to get a string from resource. The problem is that I cannot get the context or the activity, is possible? I know that with Instrumentation test I can do it, but I need to test some functions (white box test) before to do the instrumentation test (black box test).
This is the function that I have to test:
public void setDiaByText(String textView) {
getll_diaSeleccionado().clear();
if (textView.contains(context.getResources().getString(R.string.sInicialLunes))) {
getll_diaSeleccionado().add(0);
getIsSelectedArray()[0] = true;
getI_idiaSeleccionado()[0] =1;
} else
{
getIsSelectedArray()[0] = false;
getI_idiaSeleccionado()[0] =0;
}
}
And this is the test:
#Test
public void setDiaByTextView() {
String texto = "L,M,X,J,V,S,D";
alertaPOJO.setDiaByText(texto);
assertEquals(alertaPOJO.getIsSelectedArray()[0], true);
assertEquals(alertaPOJO.getI_idiaSeleccionado()[0], 1);
}
It crash when try to do context.getResources().getString(R.string.sInicialLunes))
If I put 'Mon' instead of context.getResources().getString(R.string.sInicialLunes)) or 'L' it work perfectly so, is possible to get the context or the activity in order to access to resource folder?
I am testing with Mockito and the setUp function is:
#Before
public void setUp() throws Exception {
MockitoAnnotations.initMocks(this);
mContext = Mockito.mock(Alerta.class);
Mockito.when(mContext.getApplicationContext()).thenReturn(mContext);
alertaPOJO = new AlertaPOJO();
}
Thanks
If you are using Context only for obtaining String resource, I would go by mocking only getResources().getString() part like this (see JUnit4 notation):
#RunWith(MockitoJUnitRunner.class)
public class AlertaPOJOTest {
#Mock
Context mMockContext;
#Test
public void setDiaByTextView() {
String texto = "L,M,X,J,V,S,D";
when(mMockContext.getString(R.string.sInicialLunes))
.thenReturn(INITIAL_LUNES);
alertaPOJO.setDiaByText(texto);
assertEquals(alertaPOJO.getIsSelectedArray()[0], true);
assertEquals(alertaPOJO.getI_idiaSeleccionado()[0], 1);
}
}
There are many reasons to stay with JVM tests, most important one, they are running quicker.
Untested: would it work to use the below, and probably targetContext?
android {
testOptions {
unitTests {
includeAndroidResources = true
}
}
}
You don't have a real android Context while you are using JVM unit test. For your case, maybe you can try Android Instrumentation Test, typically it is implemented in the "androidTest" directory of your project.
If you use MockK it's the same.
#RunWith(AndroidJUnit4::class)
class YourClassUnitTest : TestCase() {
#MockK
private lateinit var resources: Resources
#Before
public override fun setUp() {
MockKAnnotations.init(this)
}
#Test
fun test() {
every {
resources.getQuantityString(R.plurals.age, YEARS, YEARS)
} returns AGE
every {
resources.getString(
R.string.surname,
SURNAME
)
} returns TITLE
// Assume you test this method that returns data class
// (fields are calculated with getQuantityString and getString)
val data = getData(YEARS, SURNAME)
assertEquals(AGE, data.age)
assertEquals(TITLE, data.title)
}
companion object {
const val YEARS = 10
const val AGE = "$YEARS years"
const val SURNAME = "Johns"
const val TITLE = "Mr. $SURNAME"
}
}
See also Skip a parameter in MockK unit test, Kotlin to get a result of string resources for any data.