How can I collect values from a mutableSharedFlow in a unit test? - unit-testing

The following test doesn't pass. The string "before adding XXX" is never printed.
#Test
fun testFoo() = runBlocking {
val testCoroutineScope = TestCoroutineScope().apply {
pauseDispatcher() // This needs to be here because the actual test handles time.
}
val sharedFlow = MutableSharedFlow<Int>()
val values = mutableListOf<Int>()
println("before launch")
val job = testCoroutineScope.launch {
println("before collect")
sharedFlow.collect {
println("before adding $it")
values.add(it)
}
}
println("before emits")
sharedFlow.emit(1)
sharedFlow.emit(2)
testCoroutineScope.runCurrent()
assertEquals(mutableListOf(1, 2), values)
job.cancel()
}
I would like to have a way to emit values once the collect part is handled. I can't set a replay value, nor can I use an onSubscription because that would change the business logic.
Adding delay or yield don't seem to make any difference so it might not be a racing condition.
The test passes if pauseDispatcher() is commented.
My understanding was that emit would suspend until the collect lambda were called.

The way I solved it is via extraBufferCapacity and one more testCoroutineScope.runCurrent()
#Test
fun testFoo() = runBlocking {
val testCoroutineScope = TestCoroutineScope().apply {
pauseDispatcher()
}
val sharedFlow = MutableSharedFlow<Int>(
extraBufferCapacity = 2 // Without it, sharedFlow.emit won't have a space to save data. It will be collected
// next time there's a testCoroutineScope.runCurrent()
)
val values = mutableListOf<Int>()
println("before launch")
val job = testCoroutineScope.launch {
println("before collect")
sharedFlow.collect {
println("before adding $it")
values.add(it)
}
}
testCoroutineScope.runCurrent() // Allows the previous launch to start collecting
println("before emits")
sharedFlow.emit(1)
sharedFlow.emit(2)
testCoroutineScope.runCurrent()
assertEquals(mutableListOf(1, 2), values)
job.cancel()
}

UPDATE: Turns out my initial attempt wasn't bulletproof either, a race condition still existed, here's my revised solution, so far so good:
val msf = MutableSharedFlow<Int>()
runBlocking {
val job1 = GlobalScope.launch {
msf
.onStart {
println("${Thread.currentThread().name} - on start ")
}
.onEach { println("got $it") }
.collect()
}
val job2 = launch {
msf.subscriptionCount
.filter { it > 0 }
.onEach {
msf.emit(42)
cancel()
}
.collect()
}
delay(100)
job1.cancel()
println(job2)
}
println("${Thread.currentThread().name} - escaped!")
Old solution (incorrect):
runBlocking {
val msf = MutableSharedFlow<Int>()
val d = CompletableDeferred<Boolean>()
launch {
msf
.onStart {
d.complete(true)
}
.onEach { println("got $it") }
.collect()
}
d.join()
msf.emit(42)
}
using the invokeOnCompletion() handler cannot use suspended functions, using it with tryEmit() did not work for me (returns true but does nothing).

Related

BillingClient.querySkuDetails(SkuDetailsParams.builder()) not working?

Getting an empty list when fetching with querySkuDetails()?
So in case you've been having this issue lately, where you want to fetch your Google Play Console list of SkuDetail, maybe to show the price of one of the SkuDetail and show it to the user has it was in my case or to display some other information about a SkuDetail from your Google Play Console merchant account. Anyways, here's what's worked for me:
First you need to add this to your build.gradle app file:
implementation "com.android.billingclient:billing-ktx:4.0.0"
Then Inside of my Fragment's ViewModel, I did the following:
class MainViewModel(application: Application) : AndroidViewModel(application) {
private val billingClient by lazy {
BillingClient.newBuilder(application.applicationContext)
.setListener(purchasesUpdatedListener)
.enablePendingPurchases()
.build()
}
/**
#param result Returns true if connection was successful, false if otherwise
*/
private inline fun billingStartConnection(crossinline result: (Boolean) -> Unit) {
billingClient.startConnection(object : BillingClientStateListener {
override fun onBillingSetupFinished(billingResult: BillingResult) {
if (billingResult.responseCode == BillingClient.BillingResponseCode.OK) {
// The BillingClient is ready. You can query purchases here.
result(true)
}
}
override fun onBillingServiceDisconnected() {
// Try to restart the connection on the next request to
// Google Play by calling the startConnection() method.
result(false)
}
})
}
sealed class BillingClientObserver {
object Loading : BillingClientObserver()
object ClientDisconnected : BillingClientObserver()
object HasNoPurchases : BillingClientObserver()
object HasNoAdsPrivilege : BillingClientObserver()
object UserCancelledPurchase : BillingClientObserver()
data class UnexpectedError(val debugMessage: String = "") : BillingClientObserver()
}
private val _billingClientObserver: MutableStateFlow<BillingClientObserver> =
MutableStateFlow(BillingClientObserver.Loading)
val billingClientObserver: StateFlow<BillingClientObserver> = _billingClientObserver
suspend fun checkSkuDetailById(productId: String) =
billingStartConnection { billingClientReady ->
if (billingClientReady) {
val skuList = ArrayList<String>()
skuList.add(productId)
val params = SkuDetailsParams.newBuilder()
params.setSkusList(skuList).setType(BillingClient.SkuType.INAPP)
viewModelScope.launch(Dispatchers.Main) {
val skuDetailList = withContext(Dispatchers.IO) {
billingClient.querySkuDetails(params.build())
}
skuDetailList.skuDetailsList?.let {
Timber.d("Timber> List<SkuDetails>: $it")
if (it.isNotEmpty()) {
val skuDetails: SkuDetails = it[0]
_goAdsFreePricing.value = skuDetails.price
} else {
_billingClientObserver.value =
BillingClientObserver.UnexpectedError(context.getString(R.string.unable_to_get_price_msg))
}
} ?: run {
_billingClientObserver.value =
BillingClientObserver.UnexpectedError(context.getString(R.string.unable_to_get_price_msg))
}
}
} else {
_billingClientObserver.value =
BillingClientObserver.UnexpectedError(context.getString(R.string.unable_connect_to_play_store))
}
}
}
The most important thing to do for the
BillingClient.querySkuDetails(SkuDetailsParams.builder())
to be successful, is to first establish a successful connection through the
BillingClient.startConnection( listener: BillingClientStateListener)
then do the query on the background thread, very important that the
querySkuDetails()
happens in the background thread as it is made to fail if done on the
#MainThread
then listen for its result on the
#MainThread
like in the above example.

Coroutine testing fails with "This job has not completed yet"

I'm following Craig Russell's blog post about testing coroutines: https://craigrussell.io/2019/11/unit-testing-coroutine-suspend-functions-using-testcoroutinedispatcher/ but I can't get this test to pass:
#Test
fun multipleLaunch() = runBlockingTest {
var result = 0
val jobs = mutableListOf<Job>()
for (j in 0 until 10) {
val job = launch(testDispatcherProvider.io()) {
delay(1000)
result++
}
jobs.add(job)
}
jobs.forEach { job ->
job.join()
}
assertEquals(10, result)
}
Basically I'm launching a bunch of parallel jobs and want to get the result once all of them are finished.
I'm getting this, by now classical exception:
java.lang.IllegalStateException: This job has not completed yet
Please advise how to get this to work as intended.
My complete code:
class LaunchTest {
#get:Rule
var coroutinesTestRule = CoroutineTestRule()
val testDispatcherProvider = object : DispatcherProvider {
override fun io(): CoroutineDispatcher = coroutinesTestRule.testDispatcher
}
#Test
fun multipleLaunch() = runBlockingTest {
var result = 0
val jobs = mutableListOf<Job>()
for (j in 0 until 10) {
val job = launch(testDispatcherProvider.io()) {
delay(1000)
result++
}
jobs.add(job)
}
jobs.forEach { job ->
job.join()
}
assertEquals(10, result)
}
}
class CoroutineTestRule constructor(val testDispatcher: TestCoroutineDispatcher = TestCoroutineDispatcher()) : TestWatcher() {
override fun starting(description: Description?) {
super.starting(description)
Dispatchers.setMain(testDispatcher)
}
override fun finished(description: Description?) {
super.finished(description)
Dispatchers.resetMain()
testDispatcher.cleanupTestCoroutines()
}
}
Solved.
I totally blame Android Studio's auto-completion for this. :)
I simply ran the wrong "runBlockingTest()".
Replace this line:
fun multipleLaunch() = runBlockingTest {
with this line:
fun multipleLaunch() = coroutinesTestRule.testDispatcher.runBlockingTest {
Instead of runBlockingTest{} I used runBlocking{} because of Issue 1204

Kotlin Mockk test for suspend cancellable coroutine cancellation

I have classes
// final class from some library like okhttp
class NetworkCaller {
fun call() {
// performs some real operation
}
fun cancel() {
// .... cancels the request
}
}
class Request {
suspend fun asyncRequest(): String = suspendCancellableCoroutine { continuation ->
val call = NetworkCaller()
continuation.invokeOnCancellation {
call.cancel() // i need to write a test to mock if call.cancel is getting called or not
}
// rest of the code...
}
}
When i am doing
#Test
fun testRequestCancellation() {
val request = Request()
val job = GlobalScope.launch {
val response = request.asyncRequest()
println(response)
}
runBlocking {
job.cancel()
job.join()
}
}
The job is getting cancelled and continuation.invokeOnCancellation() is getting called, i checked with println statements. But i want to mock if the call.cancel method is getting called or not, using mockk library.
I am stuck on this, need help.
In your class, expose the NetworkCaller so it can be switched out for a mock during testing:
class Request(val call: NetworkCaller = NetworkCaller()) {
suspend fun asyncRequest(): String = suspendCancellableCoroutine { continuation ->
continuation.invokeOnCancellation {
call.cancel() // i need to write a test to mock if call.cancel is getting called or not
}
// rest of the code...
}
}
Then you can use mockk in your test:
#Test
fun testRequestCancellation() {
val mockkCall = mockk<NetworkCaller> {
coEvery { cancel() } just Runs
}
val request = Request(mockkCall)
val job = GlobalScope.launch {
val response = request.asyncRequest()
println(response)
}
runBlocking {
job.cancel()
job.join()
}
coVerify { mockkCall.cancel() }
confirmVerified(mockkCall)
}

mock retrofit suspend function infinite response

I would like to test case when server does not return response, and we trigger the next network call ( like for example search query).
So we basically have a method inside ViewModel and Retrofit method
interface RetrofitApi {
#GET("Some Url")
suspend fun getVeryImportantStuff(): String
}
class TestViewModel(private val api: RetrofitApi) : ViewModel() {
private var askJob: Job? = null
fun load(query: String) {
askJob?.cancel()
askJob = viewModelScope.launch {
val response = api.getVeryImportantStuff()
//DO SOMETHING WITH RESPONSE
}
}
}
And I would like to test case when new query is asked, and the old one didn't returns.
for case when response returns test is easy
#Test
fun testReturnResponse() {
runBlockingTest {
//given
val mockApi:RetrofitApi = mock()
val viewModel = TestViewModel(mockApi)
val response = "response from api"
val query = "fancy query"
whenever(mockApi.getVeryImportantStuff()).thenReturn(response)
//when
viewModel.load(query)
//then
//verify what happens
}
}
But I don't know how to mock suspend function that did't come back, and test case when new request is triggered like this
#Test
fun test2Loads() {
runBlockingTest {
//given
val mockApi:RetrofitApi = mock()
val viewModel = TestViewModel(mockApi)
val response = "response from api"
val secondResponse = "response from api2"
val query = "fancy query"
whenever(mockApi.getVeryImportantStuff())
.thenReturn(/* Here return some fancy stuff that is suspend* or something like onBlocking{} stub but not blocking but dalayed forever/)
.thenReturn(secondResponse)
//when
viewModel.load(query)
viewModel.load(query)
//then
//verify that first response did not happens , and only second one triggered all the stuff
}
}
Any ideas ?
EDIT: I'm not really attached to mockito, any mock library will be good :)
regards
Wojtek
I came up with kind of solution to the problem, but slightly different than I was thinking at the beginning
interface CoroutineUtils {
val io: CoroutineContext
}
interface RetrofitApi {
#GET("Some Url")
suspend fun getVeryImportantStuff(query: String): String
}
class TestViewModel(private val api: RetrofitApi,
private val utils: CoroutineUtils) : ViewModel() {
private val text = MutableLiveData<String>()
val testStream: LiveData<String> = text
private var askJob: Job? = null
fun load(query: String) {
askJob?.cancel()
askJob = viewModelScope.launch {
val response = withContext(utils.io) { api.getVeryImportantStuff(query) }
text.postValue(response)
}
}
}
And the test scenario would look like this
class TestViewModelTest {
#get:Rule
val coroutineScope = MainCoroutineScopeRule()
#get:Rule
val instantTaskExecutorRule = InstantTaskExecutorRule()
lateinit var retrofit: RetrofitApi
lateinit var utils: CoroutineUtils
val tottalyDifferentDispatcher = TestCoroutineDispatcher()
lateinit var viewModel: TestViewModel
#Before
fun setup() {
retrofit = mock()
utils = mock()
viewModel = TestViewModel(retrofit, utils)
}
#UseExperimental(ExperimentalCoroutinesApi::class)
#Test
fun test2Loads() {
runBlockingTest {
//given
val response = "response from api"
val response2 = "response from api2"
val query = "fancy query"
val query2 = "fancy query2"
whenever(utils.io)
.thenReturn(tottalyDifferentDispatcher)
val mutableListOfStrings = mutableListOf<String>()
whenever(retrofit.getVeryImportantStuff(query)).thenReturn(response)
whenever(retrofit.getVeryImportantStuff(query2)).thenReturn(response2)
//when
viewModel.testStream.observeForever {
mutableListOfStrings.add(it)
}
tottalyDifferentDispatcher.pauseDispatcher()
viewModel.load(query)
viewModel.load(query2)
tottalyDifferentDispatcher.resumeDispatcher()
//then
mutableListOfStrings shouldHaveSize 1
mutableListOfStrings[0] shouldBe response2
verify(retrofit, times(1)).getVeryImportantStuff(query2)
}
}
}
It is not exactly what I wanted, because retrofit call is not triggered when load method is called for the first time, but it is the closest solution.
What would be a perfect test for me will be assertion that retrofit was called twice , but only the second one returned to ViewModel. Solution for that will be to wrap Retrofit around method that returns suspend function like this
interface RetrofitWrapper {
suspend fun getVeryImportantStuff(): suspend (String)->String
}
class TestViewModel(private val api: RetrofitWrapper,
private val utils: CoroutineUtils) : ViewModel() {
private val text = MutableLiveData<String>()
val testStream: LiveData<String> = text
private var askJob: Job? = null
fun load(query: String) {
askJob?.cancel()
askJob = viewModelScope.launch {
val veryImportantStuff = api.getVeryImportantStuff()
val response = withContext(utils.io) {
veryImportantStuff(query)
}
text.postValue(response)
}
}
}
and test for it
#Test
fun test2Loads() {
runBlockingTest {
//given
val response = "response from api"
val response2 = "response from api2"
val query = "fancy query"
val query2 = "fancy query2"
whenever(utils.io)
.thenReturn(tottalyDifferentDispatcher)
val mutableListOfStrings = mutableListOf<String>()
whenever(retrofit.getVeryImportantStuff())
.thenReturn(suspendCoroutine {
it.resume { response }
})
whenever(retrofit.getVeryImportantStuff()).thenReturn(suspendCoroutine {
it.resume { response2 }
})
//when
viewModel.testStream.observeForever {
mutableListOfStrings.add(it)
}
tottalyDifferentDispatcher.pauseDispatcher()
viewModel.load(query)
viewModel.load(query2)
tottalyDifferentDispatcher.resumeDispatcher()
//then
mutableListOfStrings shouldHaveSize 1
mutableListOfStrings[0] shouldBe response2
verify(retrofit, times(2)).getVeryImportantStuff()
}
}
But in my opinion it is a little bit too much in interference in code only to be testable. But maybe I'm wrong :P
Looks like you want to test scenario when you have unreachable server, timeout or something similar.
In this case while doing your mock you can say that on first try it returns object and then on second executions throws exception like java.net.ConnectException: Connection timed out.
whenever(mockApi.getVeryImportantStuff())
.thenReturn(someObjet)
.thenThrow(ConnectException("timed out"))
And this this should work but you will have to do try/catch block in ViewModel witch is not ideal. I would suggest you to add additional abstraction.
You could you Repository or UseCase or whatever pattern/name you like to move the network call there. Then introduce sealed class Result to encapsulate behaviour and make your ViewModel more readable.
class TestViewModel(val repo: Repo): ViewModel() {
private var askJob: Job? = null
fun load(query: String) {
askJob?.cancel()
askJob = viewModelScope.launch {
when (repo.getStuff()) {
is Result.Success -> TODO()
is Result.Failure -> TODO()
}
}
}
}
class Repo(private val api: Api) {
suspend fun getStuff() : Result {
return try {
Result.Success(api.getVeryImportantStuff())
} catch (e: java.lang.Exception) {
Result.Failure(e)
}
}
}
sealed class Result {
data class Success<out T: Any>(val data: T) : Result()
data class Failure(val error: Throwable) : Result()
}
interface Api {
suspend fun getVeryImportantStuff() : String
}
With that level of abstraction your ViewModelTest only checks what happens in two cases.
Hope that's helpful!

When unit testing my ViewModel, list always returns empty

I'm trying to learn unit testing, I have my code factored into an MVVM(i) architecture, but when I run my testParseToList() test function, it always comes back with an empty list and I can't figure out why. I fear it may have something to do with the i part of the MVVM(i) and whether or not I'm correctly mocking my viewmodel. I'm starting with my most simple viewmodel in hopes to get a grasp of the concepts before moving onto my more complex ones.
OfflineViewModelUnitTest.kt
#RunWith(JUnit4::class)
class OfflineViewModelUnitTest {
#get:Rule
val rule = InstantTaskExecutorRule()
#Mock
var offlineViewModel: OfflineViewModel = OfflineViewModel(OfflineInteractorImpl())
#Before
fun setup() {
MockitoAnnotations.initMocks(this)
DaggerOfflineViewModelComponent.builder()
.offlineInteractorImplModule(OfflineInteractorImplModule())
.build()
.inject(offlineViewModel)
// this.offlineViewModel = OfflineViewModel(OfflineInteractorImpl())
}
#Test
fun testParseToList() {
val test = offlineViewModel.parseTextToList("dried bonito extract,\n" +
" ketchup,\n" +
" millet,\n" +
" corn & wheat protein")
val a = "dried bonito extract"
val b = "ketchup"
val c = "millet"
val d = "corn & wheat protein"
val expectedResult = listOf(a, b, c, d)
assertEquals(expectedResult, test)
}
}
OfflineViewModel.kt
class OfflineViewModel(private val offlineInteractor: OfflineInteractor): ViewModel() {
init {
DaggerOfflineViewModelComponent.builder()
.offlineInteractorImplModule(OfflineInteractorImplModule())
.build()
.inject(this)
}
fun parseTextToList(firebaseVisionTextString: String): MutableList<String> {
Log.d("here it is", firebaseVisionTextString)
return offlineInteractor.parseTextToList(firebaseVisionTextString)
}
fun readCsvFromAssetFolder(inputStream: InputStream): List<String>{
return offlineInteractor.readCsvFromAssetFolder(inputStream)
}
}
OfflineInteractorImpl.kt
class OfflineInteractorImpl: OfflineInteractor {
override fun parseTextToList(firebaseVisionTextString: String): MutableList<String> {
val ingredientsList: MutableList<String> = firebaseVisionTextString.split(",").map { it.trim() }.toMutableList()
return ingredientsList
}
override fun readCsvFromAssetFolder(inputStream: InputStream): List<String> {
val csvLine: ArrayList<String> = ArrayList()
var content: Array<String>?
try
{
val br = BufferedReader(InputStreamReader(inputStream))
for (line in br.lines())
{
content = line.split((",").toRegex()).dropLastWhile{ it.isEmpty() }.toTypedArray()
csvLine.add(content[0].substringBefore(";"))
}
br.close()
}
catch (e: IOException) {
e.printStackTrace()
}
return csvLine
}
}
Test Results
java.lang.AssertionError:
Expected :[dried bonito extract, ketchup, millet, corn & wheat protein]
Actual :[]
Like second said, since you mocked offlineViewModel it is going to return an empty string, unless you define something for it to return using when().
Source: https://github.com/mockito/mockito/wiki/FAQ#what-values-do-mocks-return-by-default