I have a function that retrieves a list of items from a repository. Instead of using a regular callback I pass in a function and invoke this with the result. But how can you unittest this kind of function. Is there some way to verify that the passed in function is being invoked or should I refactor and use a regular callback and test it using a mocked callback-interface?
My code:
class WorklistInteractor #Inject
constructor(private val worklistRepository: WorklistRepository,
private val preferenceManager: PreferenceManager,
private val executor: Executor)
: WorklistDialogContract.Interactor, Executor by executor {
#Volatile private var job: Job? = null
override fun getWorklist(callback: (Result<List<WorklistItem>>) -> Unit) {
job = onWorkerThread {
val result = worklistRepository.getWorklist().awaitResult()
onMainThread { callback(result) }
}
}
override fun cancel() {
job?.cancel()
}
}
To check it is called, something like that would work:
var hasBeenCalled = false
interactor.getWorklist({ result -> hasBeenCalled = true })
assertTrue(hasBeenCalled)
Of course you could also check that the expected result is passed, etc.
You can simply pass a callback function that sets a test-local variable which you verify as an assertion. For simplicity, I changed the example a bit:
fun getWorklist(callback: (String) -> Unit) = callback("helloWorld")
#Test
fun testCase() {
var invoked = false
getWorklist {
invoked = true
it.length
}
Assert.assertTrue(invoked)
}
Related
I am trying to test a function that executes a block of code launching a new coroutine, using this extension function:
fun ViewModel.execute(block: () -> Unit) = viewModelScope.launch(Dispatchers.IO) { block() }
Basically is launching a new coroutine in a ViewModel scope.
In my ViewModel, I use this function to invoke a use case and the result of this invocation, is a Kotlin Result, so I call the function fold to get the result and make the implementation for the success and the failure cases.
fun function() {
execute {
useCase.invoke()
).fold(
onSuccess = { },
onFailure = { }
)
}
}
In any of these cases, I am updating a LiveData object stored in my ViewModel.
I have tried to use the InstantExecutionRule
#get:Rule
val instantTaskExecutorRule = InstantTaskExecutorRule()
and I have also tried to set the coroutine context using
#get:Rule
val testCoroutineRule = TestCoroutineRule()
#ExperimentalCoroutinesApi
class TestCoroutineRule : TestRule {
val testCoroutineDispatcher = StandardTestDispatcher()
private val testScope = TestScope(testCoroutineDispatcher)
override fun apply(base: Statement, description: Description) = object : Statement() {
#Throws(Throwable::class)
override fun evaluate() {
Dispatchers.setMain(testCoroutineDispatcher)
base.evaluate()
Dispatchers.resetMain()
}
}
fun runTest(block: suspend TestScope.() -> Unit) = testScope.runTest { block() }
}
But whenever I call this function from the test class, I get an error because the function inside the fold function (onSuccess and onFailure cases) is not called and the LiveData is not updated.
Do I miss something?
Thanks!
Here's my ViewModel class.
#HiltViewModel
class MainViewModel #Inject constructor(
private val serviceRepository: ServiceRepository,
private val userPrefRepository: UserPreferencesRepository,
override val preferencesRepository: PreferencesRepository,
) : BaseViewModel() {
private val _menuList = mutableResultState<MenuData>(ResultState.UnInitialize)
val menuListState: StateFlow<ResultState<MenuData>>
get() = _menuList.asStateFlow()
var isCancelButtonVisible: Boolean = false
init {
isCancelButtonVisible = getCancelButtonVisibility()
}
// I used this to test private method.
#VisibleForTesting(otherwise = PRIVATE)
fun getCancelButtonVisibility(): Boolean = preferencesRepository.cancelButtonVisibility
fun fetchMenus() = viewModelScope.launch{
serviceRepository.fetchMenus()
.onState {
_menuList.value = it
}
}
}
FYI, I never have written test codes. And at the moment, I'd like to test fetchMenus() method. So, maybe I need to check menuListState is set after calling fetchMenus().
(I will use turbine library to check flow data)
// TODO: must initialize getCancelButtonVisibility first and then need to set something to test MainViewModel and fetchMenus() method.
#Test
fun fetchMenus_success_returnMenuList() = runTest {
// given
coEvery { mainViewModel.getCancelButtonVisibility()} returns true
// then
mainViewModel.menuListState.test {
assertNotNull(awaitItem())
}
}
I get error this error after running the test:
no answer found for: PreferencesRepository(#2).getCancelButtonVisibility()
io.mockk.MockKException: no answer found for: PreferencesRepository(#2).getCancelButtonVisibility()
I want to convert some 3rd-party API based on callbacks to simple suspend functions that are easy to use. Here's a simplified example of mine implementation.
class LibraryWrapper {
private lateinit var onFooFired: (Boolean) -> Unit
private val libraryCallback = object : LibraryCallback {
override fun foo(result: Boolean) {
onFooFired(result)
}
}
private val library = Library(libraryCallback)
suspend fun bar(): Boolean {
return suspendCoroutine { performingBar: Continuation<Boolean> ->
onFooFired = {fooResult -> performingBar.resume(fooResult) }
library.bar()
}
}
}
But this solution sometimes works and sometimes not. There is such an issue with this lambda field, that sometimes it initializes correctly, but sometimes the exception is thrown that "lateinit property onFooFired is not initialized".
It's very strange because I do initialize it before run library.bar() and foo of LibraryCallback is called only after library.bar() was called.
first of all, I think it is not a good approach to use "lateinit var" when you don't control the initialization of a field. Use lateinit only when you have the promise of initialization.
Try to use nullable field implementation like
private var onFooFired: ((Boolean) -> Unit)? = null
and in callback :
private val libraryCallback = object : LibraryCallback {
override fun foo(result: Boolean) {
onFooFired?.invoke(result)
}
}
In this case, until you do not have an implementation of "onFooFired" lambda, it does not invoke
I have a quick question about Kotlin,
For example I have a class A which have this field:
private val observer: Observer<O> = object : Observer<O> {
override fun onChanged(output: O) {
}
}
Is there any Kotlin way of returning/passing/extending the onChange event (not the value) thru a method?
I don't want to expose the output thru a listener/callback(Java way).
What I'm looking for is to somehow return the onChanged method call, without using a "middle" object/callback
Thanks
when we say return a value, it returns a value back to the callee, in this case, whoever called the onChanged method. This happens in case of synchronous calls.
In this case, onChanged call will be invoked in an asynchronous manner which makes it impossible to simply return a value back to the callee without a callback.
If i correctly understand your question, you can use observer.onChanged as Kotlin Function:
val observerOnChangedFunction = observer.run { ::onChanged }
Than you can invoke this function:
observerOnChangedFunction(instanceOfO)
Usecase: onChanged as var field
class Foo<O> {
var onChanged: (O) -> Unit = { /* default */}
private val observer: Observer<O> = object : Observer<O> {
override fun onChanged(output: O) = onChanged(output)
}
}
fun main() {
val foo = Foo<Int>()
foo.onChanged = { it.toString() }
}
-
Usecase: parameter in constructor as observer
class Foo<O> (
observer: Observer<O>
) {
private val observer: Observer<O> = observer
}
-
Usecase: parameter in constructor as onChanged lambda
class Foo<O> (
onChanged: (O) -> Unit
) {
private val observer: Observer<O> = object : Observer<O> {
override fun onChanged(output: O) = onChanged(output)
}
}
I am new to coroutines. SO I just wanted to know what is the best way to use them.
My scenraio/use case is I want to make a API call on IO thread and observe the results on Main thread and update the UI. Also when fragment's onDestoryView() is called, then I want to cancel my job.
My fragment asks the presenter for some updates. So my presenter has a coroutine running like this -
class MyPresenter(view: MyView,
private val coroutineCtx: CoroutineContext = Dispatchers.Main) : CoroutineScope {
private val job: Job = Job()
private var view: MyView? = null
init {
this.view= view
}
override val coroutineContext: CoroutineContext
get() = job + coroutineCtx
fun updateData() = launch{
//repo is singleton
val scanResult = repo.updateData()
when(scanResult) {
sucess -> { this.view.showSuccess()}
}
}
fun stopUpdate() {
job.cancel()
}
}
In my repository,
suspend fun updateData(): Result<Void> {
val response = API.update().await()
return response
}
Am I using coroutines correctly? If yes, my job.cancel() never seems to work although I call it from fragment's onDestroyView().
From my point of view you are using coroutine correctly. A few notes:
You don't have to pass view: MyView to the constructor, and assign its value to the property in init block. Instead you can mark view parameter in the constructor as val and it will became a property:
class MyPresenter(private val view: MyView,
private val coroutineCtx: CoroutineContext = Dispatchers.Main) : CoroutineScope {
// you can get rid of the next lines:
private var view: MyView? = null
init {
this.view= view
}
}
launch function returns a Job. You can add an extension function, e.g. launchSilent, to return Unit :
fun CoroutineScope.launchSilent(
context: CoroutineContext = EmptyCoroutineContext,
start: CoroutineStart = CoroutineStart.DEFAULT,
block: suspend CoroutineScope.() -> Unit
) {
launch(context, start, block)
}
From my observation job.cancel() works correctly: when you invoke it a coroutine must stop. For example if we put some logs:
fun updateData() = launch{
Log.d("Tag", "launch start")
val scanResult = repo.updateData()
when(scanResult) {
success -> { this.view.showSuccess()}
}
Log.d("Tag", "launch end")
}
And add some delay to the repo's updateData() function:
suspend fun updateData(): Result<Void> {
delay(5000)
val response = API.update().await()
return response
}
And, for example, in the fragment after invoking presenter.updateData() we call something like Handler().postDelayed({ presenter.stopUpdate() }, 3000) we won't see "launch end" log in the Logcat.