I'm trying to make some unit tests for my business logic.
I have repository in which I save to room database (2.1.0-rc01) some data from response.
Data saving into different tables with different dao in single transaction.
Code is simplified:
ItemRepository
suspend fun saveItems(response: Response) {
val items = response.items.map { it.toLocalItem() }
val subItems = response.items.flatMap { item ->
item.subItems.map { it.toLocal(item.id) }
}
db.withTransaction {
db.itemDao().deleteAll()
db.itemDao().insertAll(items)
db.subItemDao().insertAll(subItems)
}
}
For unit test I'm using Mockk library. How can I mock room withTransaction method?. withTransaction is declared as
suspend fun <R> RoomDatabase.withTransaction(block: suspend () -> R): R
I'm trying to writing test
#MockK
private lateinit var database: AppDatabase
#MockK
private lateinit var itemDao: ItemDao
#MockK
private lateinit var subItemDao: SubItemDao
#Test
fun checkSaveItems() = runBlocking {
repository = ItemRepository(database)
coEvery { database.itemDao() } returns itemDao
coEvery { database.subItemDao() } returns subItemDao
//TODO: execute database.withTransaction(block: suspend () -> R)
coEvery { itemDao.deleteAll() } just Runs
coEvery { itemDao.insertAll(any()) } just Runs
coEvery { subItemDao.insertAll(any()) } just Runs
repository.saveItems(testResponse)
coVerifySequence {
itemDao.deleteAll()
itemDao.insertAll(testItems)
subItemDao.insertAll(testSubItems)
}
}
You first have to enable static mocks for the Android Room KTX method withTransaction {}. You also need to capture the suspend lambda function passed to it. This captured function can just be invoked so the code inside it runs. Since you're mocking all the database calls, you don't need a real transaction here.
#Before
fun initMocks() {
MockKAnnotations.init(this)
mockkStatic(
"androidx.room.RoomDatabaseKt"
)
val transactionLambda = slot<suspend () -> R>()
coEvery { db.withTransaction(capture(transactionLambda)) } coAnswers {
transactionLambda.captured.invoke()
}
}
You should then be able to run your code as written.
To expand on Andrew's answer, the mockk documentation for extension functions shows that if you are mocking an object wide or class wide extension function, you can just use regular mockk to achieve that. However, if you are using a module wide extension function, like withTransaction, you also need to perform mockkStatic on the module's class name.
Related
I am using 2 separate liveData exposed to show the error coming from the API. I am basically checking if there is an exception with the API call, pass a failure status and serverErrorLiveData will be observed.
So I have serverErrorLiveData for error and creditReportLiveData for result without an error.
I think I am not doing this the right way. Could you please guide me on what is the right way of catching error from the API call. Also, any concerns/recommendation on passing data from repository on to view model.
What is the right way of handing loading state?
CreditScoreFragment
private fun initViewModel() {
viewModel.getCreditReportObserver().observe(viewLifecycleOwner, Observer<CreditReport> {
showScoreUI(true)
binding.score.text = it.creditReportInfo.score.toString()
binding.maxScoreValue.text = "out of ${it.creditReportInfo.maxScoreValue}"
initDonutView(
it.creditReportInfo.score.toFloat(),
it.creditReportInfo.maxScoreValue.toFloat()
)
})
viewModel.getServerErrorLiveDataObserver().observe(viewLifecycleOwner, Observer<Boolean> {
if (it) {
showScoreUI(false)
showToastMessage()
}
})
viewModel.getCreditReport()
}
MainActivityViewModel
class MainActivityViewModel #Inject constructor(
private val dataRepository: DataRepository
) : ViewModel() {
var creditReportLiveData: MutableLiveData<CreditReport>
var serverErrorLiveData: MutableLiveData<Boolean>
init {
creditReportLiveData = MutableLiveData()
serverErrorLiveData = MutableLiveData()
}
fun getCreditReportObserver(): MutableLiveData<CreditReport> {
return creditReportLiveData
}
fun getServerErrorLiveDataObserver(): MutableLiveData<Boolean> {
return serverErrorLiveData
}
fun getCreditReport() {
viewModelScope.launch(Dispatchers.IO) {
val response = dataRepository.getCreditReport()
when(response.status) {
CreditReportResponse.Status.SUCCESS -> creditReportLiveData.postValue(response.creditReport)
CreditReportResponse.Status.FAILURE -> serverErrorLiveData.postValue(true)
}
}
}
}
DataRepository
class DataRepository #Inject constructor(
private val apiServiceInterface: ApiServiceInterface
) {
suspend fun getCreditReport(): CreditReportResponse {
return try {
val creditReport = apiServiceInterface.getDataFromApi()
CreditReportResponse(creditReport, CreditReportResponse.Status.SUCCESS)
} catch (e: Exception) {
CreditReportResponse(null, CreditReportResponse.Status.FAILURE)
}
}
}
ApiServiceInterface
interface ApiServiceInterface {
#GET("endpoint.json")
suspend fun getDataFromApi(): CreditReport
}
CreditScoreResponse
data class CreditReportResponse constructor(val creditReport: CreditReport?, val status: Status) {
enum class Status {
SUCCESS, FAILURE
}
}
It's creates complexity and increased chances for a coding error to have two LiveData channels for success and failure. You should have a single LiveData that can offer up the data or an error so you know it's coming in orderly and you can observe it in one place. Then if you add a retry policy, for example, you won't risk somehow showing an error after a valid value comes in. Kotlin can facilitate this in a type-safe way using a sealed class. But you're already using a wrapper class for success and failure. I think you can go to the source and simplify it. You can even just use Kotlin's own Result class.
(Side note, your getCreditReportObserver() and getServerErrorLiveDataObserver() functions are entirely redundant because they simply return the same thing as a property. You don't need getter functions in Kotlin because properties basically are getter functions, with the exception of suspend getter functions because Kotlin doesn't support suspend properties.)
So, to do this, eliminate your CreditReportResponse class. Change your repo function to:
suspend fun getCreditReport(): Result<CreditReport> = runCatching {
apiServiceInterface.getDataFromApi()
}
If you must use LiveData (I think it's simpler not to for a single retrieved value, see below), your ViewModel can look like:
class MainActivityViewModel #Inject constructor(
private val dataRepository: DataRepository
) : ViewModel() {
val _creditReportLiveData = MutableLiveData<Result<CreditReport>>()
val creditReportLiveData: LiveData<Result<CreditReport>> = _creditReportLiveData
fun fetchCreditReport() { // I changed the name because "get" implies a return value
// but personally I would change this to an init block so it just starts automatically
// without the Fragment having to manually call it.
viewModelScope.launch { // no need to specify dispatcher to call suspend function
_creditReportLiveData.value = dataRepository.getCreditReport()
}
}
}
Then in your fragment:
private fun initViewModel() {
viewModel.creditReportLiveData.observe(viewLifecycleOwner) { result ->
result.onSuccess {
showScoreUI(true)
binding.score.text = it.creditReportInfo.score.toString()
binding.maxScoreValue.text = "out of ${it.creditReportInfo.maxScoreValue}"
initDonutView(
it.creditReportInfo.score.toFloat(),
it.creditReportInfo.maxScoreValue.toFloat()
)
}.onFailure {
showScoreUI(false)
showToastMessage()
}
viewModel.fetchCreditReport()
}
Edit: the below would simplify your current code, but closes you off from being able to easily add a retry policy on failure. It might make better sense to keep the LiveData.
Since you are only retrieving a single value, it would be more concise to expose a suspend function instead of LiveData. You can privately use a Deferred so the fetch doesn't have to be repeated if the screen rotates (the result will still arrive and be cached in the ViewModel). So I would do:
class MainActivityViewModel #Inject constructor(
private val dataRepository: DataRepository
) : ViewModel() {
private creditReportDeferred = viewModelScope.async { dataRepository.getCreditReport() }
suspend fun getCreditReport() = creditReportDeferred.await()
}
// In fragment:
private fun initViewModel() = lifecycleScope.launch {
viewModel.getCreditReport()
.onSuccess {
showScoreUI(true)
binding.score.text = it.creditReportInfo.score.toString()
binding.maxScoreValue.text = "out of ${it.creditReportInfo.maxScoreValue}"
initDonutView(
it.creditReportInfo.score.toFloat(),
it.creditReportInfo.maxScoreValue.toFloat()
)
}.onFailure {
showScoreUI(false)
showToastMessage()
}
}
I have an issue in unit test for a function used Coroutine to call API with networkBoundResource.
The issue is when run the test the API actually called, although it's supposed to return the expected response such as I determined in this line: whenever(mfSDKPaymentRepository.sendPayment(request)).thenReturn(expectedResponse)
This is the function want to test:
fun callSendPayment(
coroutineScope: CoroutineScope? = GlobalScope,
request: MFSendPaymentRequest,
apiLang: String,
listener: (MFResult<MFSendPaymentResponse>) -> Unit
) {
Const.apiLang = apiLang
coroutineScope?.launch(Dispatchers.Main) {
val dataResource = networkBoundResource {
mInteractors.sendPayment(request)
}
when (dataResource) {
is MFResult.Success ->
listener.invoke(MFResult.Success(dataResource.value.response!!))
is MFResult.Fail ->
listener.invoke(MFResult.Fail(dataResource.error))
}
}
}
This is the test class:
class MFSDKMainTest {
private val mfSDKPaymentRepository = mock<MFSDKPaymentGateWay>()
private val testScope = TestCoroutineScope()
#get:Rule
val instantTaskExecutorRule = InstantTaskExecutorRule()
#Before
fun setup() {
Dispatchers.setMain(Dispatchers.Unconfined)
}
#After
fun tearDown() {
Dispatchers.resetMain()
testScope.cleanupTestCoroutines()
}
#Test
fun testCallSendPayment() = runBlockingTest {
val data = MFSendPaymentResponse(invoiceId = ID)
val expectedResponse = SDKSendPaymentResponse(data)
val request = MFSendPaymentRequest(
0.100,
"Customer name",
MFNotificationOption.LINK
)
val lang = MFAPILanguage.EN
whenever(mfSDKPaymentRepository.sendPayment(request))
.thenReturn(expectedResponse)
MFSDKMain.callSendPayment(testScope, request, lang) {
assert(it is MFResult.Success)
}
}
}
In your function when you call
mInteractors.sendPayment(request)
how do you get a refrence to MFSDKPaymentGateWay? In your test method you are not setting the mocked object in MFSDKMain class.
If you are creating it inside the same class (which i assume is an Object class) you may need to find a way to mock object class. Probably you need to use mockk instead of mockito
If you have setter method for MFSDKPaymentGateWay you should call it in your test method.
I am investigation the MockK library with my Android JUnit tests
testImplementation "io.mockk:mockk:1.10.0"
I have an issue when attempting to spyk on suspend functions
heres my Junit test
#ExperimentalCoroutinesApi
#FlowPreview
#RunWith(AndroidJUnit4::class)
class BackOffCriteriaDaoTest : BaseTest() {
#Rule
#JvmField
val instantTaskExecutorRule = InstantTaskExecutorRule()
private lateinit var dao: BackoffCriteriaDAO
#Test
fun backOffCriteria() = runBlocking {
dao = spyk(myRoomDatabase.backoffCriteriaDAO())
assertNotNull(dao.getBackoffCriteria())
assertEquals(backOffCriteriaDO, dao.getBackoffCriteria())
dao.delete()
coVerify {
myRoomDatabase.backoffCriteriaDAO()
dao.reset()
}
}
}
This test throws an java.lang.AssertionError at dao.reset() as follows:-
java.lang.AssertionError: Verification failed: call 2 of 2: BackoffCriteriaDAO_Impl(#2).reset(eq(continuation {}))). Only one matching call to BackoffCriteriaDAO_Impl(#2)/reset(Continuation) happened, but arguments are not matching:
[0]: argument: continuation {}, matcher: eq(continuation {}), result: -
My dao reset() method resembles this:-
#Transaction
suspend fun reset() {
delete()
insert(BackoffCriteriaDO(THE_BACKOFF_CRITERIA_ID, BACKOFF_CRITERIA_MILLISECOND_DELAY, BACKOFF_CRITERIA_MAX_RETRY_COUNT))
}
Why am I seeing this java.lang.AssertionError?
How do I coVerify that suspend functions have been called?
UPDATE
I believe the issue is caused by the fact I am using Room database.
My dao interface method reset() is implemented by room generated code as
#Override
public Object reset(final Continuation<? super Unit> p0) {
return RoomDatabaseKt.withTransaction(__db, new Function1<Continuation<? super Unit>, Object>() {
#Override
public Object invoke(Continuation<? super Unit> __cont) {
return BackoffCriteriaDAO.DefaultImpls.reset(BackoffCriteriaDAO_Impl.this, __cont);
}
}, p0);
}
which means the coVerify{} is matching this function and not my interface version.
Is it possible to match this generated version of public Object reset(final Continuation<? super Unit> p0)?
Is this a more basic issue with mockk that it cannot mockk java classes?
Or Java implementations of Kotlin interfaces?
UPDATE 2
When my Room DAO functions are not suspend then Mockk works as required
using these dummy functions in my DAO:-
#Transaction
fun experimentation() {
experiment()
}
#Transaction
fun experiment() {
experimental()
}
#Query("DELETE from backoff_criteria")
fun experimental()
My test passes
#Test
fun experimentation() = runBlocking {
val actual = myRoomDatabase.backoffCriteriaDAO()
val dao = spyk(actual)
dao.experimentation()
verify { dao.experiment() }
}
When I change my dummy functions as follows the test still passes
#Transaction
suspend fun experimentation() {
experiment()
}
#Transaction
fun experiment() {
experimental()
}
#Query("DELETE from backoff_criteria")
fun experimental()
However when I change my dummy functions as follows the test throws an exception
#Transaction
suspend fun experimentation() {
experiment()
}
#Transaction
suspend fun experiment() {
experimental()
}
#Query("DELETE from backoff_criteria")
fun experimental()
The failing tests resembles this:-
#Test
fun experimentation() = runBlocking {
val actual = myRoomDatabase.backoffCriteriaDAO()
val dao = spyk(actual)
dao.experimentation()
coVerify { dao.experiment() }
}
The exception is
java.lang.AssertionError: Verification failed: call 1 of 1: BackoffCriteriaDAO_Impl(#2).experiment(eq(continuation {}))). Only one matching call to BackoffCriteriaDAO_Impl(#2)/experiment(Continuation) happened, but arguments are not matching:
[0]: argument: continuation {}, matcher: eq(continuation {}), result: -
There might be nothing wrong with spy but the asynchronous nature of the transaction function you are invoking.
To test with suspending functions with scope you might need to use
launch builder and advance time until idle, or for a time period for testing the progress, like it's done with RxJava counter parts.
I had the same issue with MockWebServer, here you can check out the question.
launch {
dao.delete()
}
advanceUntilIdle()
And use Coroutine rule with tests to have same scope for each operation.
class TestCoroutineRule : TestRule {
private val testCoroutineDispatcher = TestCoroutineDispatcher()
val testCoroutineScope = TestCoroutineScope(testCoroutineDispatcher)
override fun apply(base: Statement, description: Description?) = object : Statement() {
#Throws(Throwable::class)
override fun evaluate() {
Dispatchers.setMain(testCoroutineDispatcher)
base.evaluate()
Dispatchers.resetMain()
try {
testCoroutineScope.cleanupTestCoroutines()
} catch (exception: Exception) {
exception.printStackTrace()
}
}
}
fun runBlockingTest(block: suspend TestCoroutineScope.() -> Unit) =
testCoroutineScope.runBlockingTest { block() }
}
You can use rule as below
testCoroutineRule.runBlockingTest {
dao.delete()
advanceUntilIdle()
coVerify {
myRoomDatabase.backoffCriteriaDAO()
dao.reset()
}
}
Also you can try putting dao.delete() in launch. In some tests it did not work without launch while some other work without it and even some of them are flakky with everything i tried. There are some issues with coroutines-test to be solved.
here you can check how it's done and there are some issues with test-coroutines, you can check out my other question here.
I created a playground to test coroutines, it might helpful and you can test out the issues with coroutines, and another one with mockK and coroutines tests.
I'm writing unit tests base on Google's samples: TaskDetailPresenterTest.kt#L102
They use ArgumentCaptor<TasksDataSource.GetTaskCallback> to trigger callback with fake data COMPLETED_TASK
#Test
fun getCompletedTaskFromRepositoryAndLoadIntoView() {
presenter = TaskDetailPresenter(COMPLETED_TASK.id, tasksRepository, taskDetailView)
presenter.start()
// Then task is loaded from model, callback is captured
verify(tasksRepository).getTask(
eq(COMPLETED_TASK.id), capture(taskCallbackCaptor))
// When task is finally loaded
taskCallbackCaptor.value.onTaskLoaded(COMPLETED_TASK) // Trigger callback
}
Everything work fine because they use TasksDataSource.GetTaskCallback to return data. See: TaskDetailPresenter.kt#L36:
fun getTask(taskId: String, callback: GetTaskCallback)
Then use as
tasksRepository.getTask(taskId, object : TasksDataSource.GetTaskCallback {
override fun onTaskLoaded(task: Task) {
showTask(task)
}
}
But when I try to use RxJava Single<> instead of normal callback, like:
fun getTask(taskId: String): Single<Task>
Then use as
tasksRepository.getTask(taskId)
.subscribe(object : SingleObserver<Task> {
override fun onSuccess(task: Task) {
showTask(task)
}
override fun onError(e: Throwable) {
}
})
}
Then I cannot use ArgumentCaptor<> to trigger return fake data. It always throw NullPointerException when I execute my test, because tasksRepository.getTask(taskId) is always return null.
So how can I achieve the same unit test like Google did, but in RxJava?
My unit test code:
#Mock private lateinit var tasksRepository: TasksRepository
#Captor private lateinit var taskCaptor: ArgumentCaptor<SingleObserver<Task>>
private lateinit var presenter: TaskDetailPresenter
#Before fun setup() {
MockitoAnnotations.initMocks(this)
}
#Test
fun getCompletedTaskFromRepositoryAndLoadIntoView() {
presenter = TaskDetailPresenter(COMPLETED_TASK.id, tasksRepository, taskDetailView)
presenter.start()
// Then task is loaded from model, callback is captured
verify(tasksRepository).getTask(
eq(COMPLETED_TASK.id)).subscribe(taskCaptor.capture())
// When task is finally loaded
taskCaptor.value.onSuccess(COMPLETED_TASK) // Trigger callback
}
Note that all other parts (declare, setup, mocking,..) is the same as Google.
I don't know if you have already used this library but I would suggest you to use the Dagger 2 library with a MVP code architecture to ease your unit tests by improving your dependencies and couplings
All this method is doing is showTask(task: Task). So assert that this method is called after your observer starts observing. You shouldn't care what the showTask is going to do once it's called. If you use Rx it is much better to make your methods take arguments and return value of the observe pattern to make it easier write unit test.
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