BillingClient.querySkuDetails(SkuDetailsParams.builder()) not working? - google-play-billing

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.

Related

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

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

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!

Unit testing when using a kotlin extension function with receiver

Is it possible to pass a specific value into an extension function with receiver, in a unit test?
I'm trying to test the folowing subscribe method:
...
private lateinit var subscription: SubscriptionReceiveChannel<UiStateModel>
suspend fun subscribe(model: MainViewModel) {
subscription = model.connect()
subscription.consumeEach { value -> loadView(value) /** or loadView(it) */ }
}
private fun loadView(uiState: UiStateModel) {
when(uiState) {
is Loading -> view.isLoading()
is Error -> view.isError(uiState.exception.localizedMessage)
is Success -> when {
uiState.result != null -> view.isSuccess(uiState.result)
else -> view.isEmpty()
}
}
}
I want to be able to apply a specific value to the consumeEach function, but how can this be done?
Here's my unit test:
...
private val view = mock<MainView>()
private val model = mock<MainViewModel>()
private val subscription = mock<SubscriptionReceiveChannel<UiStateModel>>()
private val presenter = MainPresenter(view)
#Test
fun `When uistate is loading, view should show loading message`() = runBlocking {
// Given
val state = UiStateModel.Loading()
whenever(model.connect()).thenReturn(subscription)
whenever(subscription.consumeEach { any() }).thenAnswer({ state })
// When
presenter.subscribe(model)
// Then
verify(model).connect()
verify(view).isLoading()
verify(view, never()).isSuccess(anyString())
verify(view, never()).isEmpty()
verify(view, never()).isError(anyString())
}
...

How to write proper test for reactive interface repository which returns Observable only when there is some event, how to mock triggering that event

I am trying to write proper test for my reactive repository, which just listen for location changes. I don't want to actually listen for location changes and just 'invoke' new location change multiple times with just created custom Location.
Here is the repository and its function for gathering locations:
interface RxLocationRepository {
#SuppressLint("MissingPermission")
fun onLocationUpdate(): Observable<Location>
fun stopLocationUpdates()
}
Here is the implementation of it, which actually DON'T MATTER because i don't want to listen for real location updates, but i want to show you that it is just reactive implementation for listen to LocationManager updates:
class LocationNativeRepository(
val locationManager: LocationManager,
val geoEventsDistanceMeters: Int,
val geoEventsIntervalSeconds: Int) : RxLocationRepository{
var locationToPopulate: Location = Location(LocationManager.GPS_PROVIDER)
lateinit var mLocationCallbackNativeApi: LocationListener
private val subject: BehaviorSubject<Location> = BehaviorSubject.createDefault(locationToPopulate)
var locationEmitter: Observable<Location> = subject.hide()
init {
configureNativeLocationEmitter()
}
override fun onLocationUpdate(): Observable<Location> {
return locationEmitter
}
#SuppressLint("MissingPermission")
override fun stopLocationUpdates() {
locationManager.removeUpdates(mLocationCallbackNativeApi)
}
#SuppressLint("MissingPermission")
private fun configureNativeLocationEmitter() {
mLocationCallbackNativeApi = object : LocationListener {
override fun onLocationChanged(location: Location) {
subject.onNext(location)
}
override fun onStatusChanged(provider: String, status: Int, extras: Bundle) {}
override fun onProviderEnabled(provider: String) {}
override fun onProviderDisabled(provider: String) {}
}
try {
locationManager.requestLocationUpdates(LocationManager.GPS_PROVIDER,
(geoEventsIntervalSeconds * 1000).toLong(),
geoEventsDistanceMeters.toFloat(),
mLocationCallbackNativeApi,
Looper.getMainLooper())
} catch (ignored: IllegalArgumentException) {
ignored.printStackTrace()
}
try {
locationManager.requestLocationUpdates(LocationManager.NETWORK_PROVIDER,
(geoEventsIntervalSeconds * 1000).toLong(),
geoEventsDistanceMeters.toFloat(),
mLocationCallbackNativeApi,
Looper.getMainLooper())
} catch (ignored: IllegalArgumentException) {
ignored.printStackTrace()
}
}
}
So how i can invoke this repository in my test to actually trigger the onLocationUpdate() method? So for example i will make it emit 3 times location like this:
val location = Location("test").apply {
latitude = 1.234
longitude = 5.678
accuracy = 20f
time = Date().time
}
Create a PublishSubject<Location> variable. Create an anonymous sub-class of RxLocationRepository, where the onLocationUpdate() method returns the variable. Then emit values into the variable to provide new locations.