Skip to content

Commit

Permalink
Merge pull request #122 from GetStream/feature/stream-result-call-tests
Browse files Browse the repository at this point in the history
[PBE-3875] Feature/stream result call tests
  • Loading branch information
skydoves authored Oct 22, 2024
2 parents 70be92c + bc36e22 commit ff76c22
Show file tree
Hide file tree
Showing 10 changed files with 379 additions and 15 deletions.
4 changes: 2 additions & 2 deletions app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -36,8 +36,8 @@ android {
}

compileOptions {
sourceCompatibility = JavaVersion.VERSION_11
targetCompatibility = JavaVersion.VERSION_11
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
}

lint {
Expand Down
2 changes: 1 addition & 1 deletion build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ apiValidation {

subprojects {
tasks.withType<org.jetbrains.kotlin.gradle.tasks.KotlinCompile>().all {
kotlinOptions.jvmTarget = JavaVersion.VERSION_11.toString()
kotlinOptions.jvmTarget = JavaVersion.VERSION_17.toString()
}

apply(plugin = rootProject.libs.plugins.spotless.get().pluginId)
Expand Down
5 changes: 4 additions & 1 deletion gradle/libs.versions.toml
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
[versions]
atomicfuVersion = "0.25.0"
junitJupiterApi = "5.11.1"
streamLog = "1.3.1"
androidGradlePlugin = "8.6.1"
androidxActivity = "1.4.0"
Expand Down Expand Up @@ -33,6 +34,8 @@ mockitoKotlin = "4.1.0"

[libraries]
atomicfu = { module = "org.jetbrains.kotlinx:atomicfu", version.ref = "atomicfuVersion" }
junit-jupiter-api = { module = "org.junit.jupiter:junit-jupiter-api", version.ref = "junitJupiterApi" }
junit-jupiter-engine = { module = "org.junit.jupiter:junit-jupiter-engine", version.ref = "junitJupiterApi" }
stream-log = { group = "io.getstream", name = "stream-log", version.ref = "streamLog" }
androidx-appcompat = { group = "androidx.appcompat", name = "appcompat", version.ref = "androidxAppCompat" }
androidx-annotation = { group = "androidx.annotation", name = "annotation", version.ref = "androidxAnnotation" }
Expand Down Expand Up @@ -75,4 +78,4 @@ spotless = { id = "com.diffplug.spotless", version.ref = "spotless" }
dokka = { id = "org.jetbrains.dokka", version.ref = "dokka" }
nexus-plugin = { id = "com.vanniktech.maven.publish", version.ref = "nexusPlugin" }
kotlinBinaryCompatibilityValidator = { id = "org.jetbrains.kotlinx.binary-compatibility-validator", version.ref = "compatibilityValidator" }
kotlin-atomicfu = { id = "org.jetbrains.kotlinx.atomicfu", version.ref = "atomicfu" }
kotlin-atomicfu = { id = "org.jetbrains.kotlinx.atomicfu", version.ref = "atomicfu" }
17 changes: 12 additions & 5 deletions stream-result-call-retrofit/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ plugins {
id(libs.plugins.kotlin.android.get().pluginId)
id(libs.plugins.kotlin.serialization.get().pluginId)
id(libs.plugins.nexus.plugin.get().pluginId)
id("de.mannodermaus.android-junit5") version "1.11.0.0"
}

mavenPublishing {
Expand All @@ -48,13 +49,10 @@ android {
}

compileOptions {
sourceCompatibility = JavaVersion.VERSION_11
targetCompatibility = JavaVersion.VERSION_11
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
}

kotlinOptions {
jvmTarget = "11"
}
}

tasks.withType<org.jetbrains.kotlin.gradle.tasks.KotlinCompile> {
Expand All @@ -71,4 +69,13 @@ dependencies {
implementation(libs.kotlinx.coroutines)
implementation(libs.kotlinx.serialization.json)
implementation(libs.stream.log)

testImplementation(libs.testing.kluent)
testImplementation(libs.testing.coroutines.test)
testImplementation(libs.testing.mockito)
testImplementation(libs.testing.mockito.kotlin)
testImplementation(libs.testing.mockito.kotlin)
testImplementation(libs.androidx.test.junit)
testImplementation(libs.junit.jupiter.api)
testRuntimeOnly(libs.junit.jupiter.engine)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
/*
* Copyright (c) 2014-2022 Stream.io Inc. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.getstream.result.call.retrofit

import io.getstream.result.call.dispatcher.CallDispatcherProvider
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withContext
import okhttp3.Request
import okio.Timeout
import org.mockito.kotlin.mock
import retrofit2.Call
import retrofit2.Callback
import retrofit2.Response
import java.io.IOException
import java.util.concurrent.atomic.AtomicBoolean

internal class BlockedRetrofit2Call<T>(
private val scope: CoroutineScope,
private val value: T? = null,
private val error: IOException? = null
) : retrofit2.Call<T> {

init {
if (value == null) {
requireNotNull(error) {
"BlockedRetrofit2Call should be initialized with an error or value not null"
}
}
if (error == null) {
requireNotNull(value) {
"BlockedRetrofit2Call should be initialized with an error or value not null"
}
}
if (error != null && value != null) error("BlockedRetrofit2Call can't be initialized with a value and an error")
}

private val isBlocked = AtomicBoolean(true)
private val started = AtomicBoolean(false)
private val completed = AtomicBoolean(false)
private val cancelled = AtomicBoolean(false)

fun unblock() {
isBlocked.set(false)
}

private suspend fun run() = withContext(CallDispatcherProvider.IO) {
started.set(true)
while (isBlocked.get()) {
delay(10)
}
if (!cancelled.get()) completed.set(true)
}

fun isStarted(): Boolean = started.get()
fun isCompleted(): Boolean = completed.get()
override fun enqueue(callback: Callback<T>) {
scope.launch {
run()
if (value != null) callback.onResponse(this@BlockedRetrofit2Call, Response.success(value))
if (error != null) callback.onFailure(this@BlockedRetrofit2Call, error)
}
}

override fun execute(): Response<T> = runBlocking {
run()
if (value != null) {
Response.success(value)
} else {
throw error!!
}
}

override fun isExecuted(): Boolean = started.get()
override fun cancel() { cancelled.set(true) }
override fun isCanceled(): Boolean = cancelled.get()
override fun request(): Request = mock()
override fun timeout(): Timeout = mock()
override fun clone(): Call<T> = BlockedRetrofit2Call(scope, value, error)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
/*
* Copyright (c) 2014-2022 Stream.io Inc. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.getstream.result.call

import kotlin.random.Random

private val charPool: CharArray = (('a'..'z') + ('A'..'Z') + ('0'..'9')).toCharArray()

public fun positiveRandomInt(maxInt: Int = Int.MAX_VALUE - 1): Int =
Random.nextInt(1, maxInt + 1)

public fun positiveRandomLong(maxLong: Long = Long.MAX_VALUE - 1): Long =
Random.nextLong(1, maxLong + 1)

public fun randomInt(): Int = Random.nextInt()
public fun randomIntBetween(min: Int, max: Int): Int = Random.nextInt(min, max + 1)
public fun randomLong(): Long = Random.nextLong()
public fun randomLongBetween(min: Long, max: Long = Long.MAX_VALUE - 1): Long = Random.nextLong(min, max + 1)
public fun randomBoolean(): Boolean = Random.nextBoolean()
public fun randomString(size: Int = 20): String = buildString(capacity = size) {
repeat(size) {
append(charPool.random())
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
/*
* Copyright (c) 2014-2024 Stream.io Inc. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.getstream.result.call.retrofit

import io.getstream.result.Error
import io.getstream.result.Result
import io.getstream.result.call.Call
import io.getstream.result.call.randomString
import kotlinx.coroutines.async
import kotlinx.coroutines.delay
import kotlinx.coroutines.test.runTest
import org.amshove.kluent.`should be equal to`
import org.amshove.kluent.`should be instance of`
import org.amshove.kluent.shouldBeInstanceOf
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.extension.RegisterExtension
import org.mockito.Mockito
import org.mockito.kotlin.any
import org.mockito.kotlin.mock
import org.mockito.kotlin.never
import org.mockito.kotlin.only
import java.io.IOException

internal class RetrofitCallTest {

companion object {
@JvmField
@RegisterExtension
val testCoroutines = TestCoroutineExtension()
}

val resultValue = randomString()
val validResult: Result<String> = Result.Success(resultValue)
val parser: ErrorParser<*> = mock()

@Test
fun `Call should be executed and return a valid result`() = runTest {
val blockedRetrofit2Call = BlockedRetrofit2Call(testCoroutines.scope, value = resultValue).apply { unblock() }
val call = RetrofitCall(blockedRetrofit2Call, parser, testCoroutines.scope)

val result = call.execute()

result `should be equal to` validResult
blockedRetrofit2Call.isStarted() `should be equal to` true
blockedRetrofit2Call.isCompleted() `should be equal to` true
}

@Test
fun `Call should be executed and return a failure result`() = runTest {
val blockedRetrofit2Call =
BlockedRetrofit2Call<String>(testCoroutines.scope, error = IOException(randomString())).apply { unblock() }
val call = RetrofitCall(blockedRetrofit2Call, parser, testCoroutines.scope)

val result = call.execute()

result.shouldBeInstanceOf(Result.Failure::class)
(result as Result.Failure).value `should be instance of` Error::class
blockedRetrofit2Call.isStarted() `should be equal to` true
blockedRetrofit2Call.isCompleted() `should be equal to` true
}

@Test
fun `Canceled Call should be executed and return a cancel error`() = runTest {
val blockedRetrofit2Call =
BlockedRetrofit2Call<String>(testCoroutines.scope, error = IOException(randomString()))
val call = RetrofitCall(blockedRetrofit2Call, parser, testCoroutines.scope)

val deferedResult = async { call.execute() }
call.cancel()
blockedRetrofit2Call.unblock()
val result = deferedResult.await()

result.shouldBeInstanceOf(Result.Failure::class)
(result as Result.Failure).value `should be instance of` Error::class
blockedRetrofit2Call.isStarted() `should be equal to` true
blockedRetrofit2Call.isCompleted() `should be equal to` false
blockedRetrofit2Call.isCanceled() `should be equal to` true
}

@Test
fun `Call should be enqueued and return a valid result by the callback`() = runTest {
val callback: Call.Callback<String> = mock()
val blockedRetrofit2Call = BlockedRetrofit2Call(testCoroutines.scope, value = resultValue).apply { unblock() }
val call = RetrofitCall(blockedRetrofit2Call, parser, testCoroutines.scope)

call.enqueue(callback)

Mockito.verify(callback, only()).onResult(
org.mockito.kotlin.check {
it `should be equal to` validResult
}
)
blockedRetrofit2Call.isStarted() `should be equal to` true
blockedRetrofit2Call.isCompleted() `should be equal to` true
}

@Test
fun `Canceled Call should be enqueued and shouldn't return value on the callback`() = runTest {
val callback: Call.Callback<String> = mock()
val blockedRetrofit2Call = BlockedRetrofit2Call(testCoroutines.scope, value = resultValue)
val call = RetrofitCall(blockedRetrofit2Call, parser, testCoroutines.scope)

call.enqueue(callback)
call.cancel()
blockedRetrofit2Call.unblock()

Mockito.verify(callback, never()).onResult(any())
blockedRetrofit2Call.isStarted() `should be equal to` true
blockedRetrofit2Call.isCompleted() `should be equal to` false
blockedRetrofit2Call.isCanceled() `should be equal to` true
}

@Test
fun `Call should be executed asynchronous and return a valid result`() = runTest {
val blockedRetrofit2Call = BlockedRetrofit2Call(testCoroutines.scope, value = resultValue).apply { unblock() }
val call = RetrofitCall(blockedRetrofit2Call, parser, testCoroutines.scope)

val result = call.await()

result `should be equal to` validResult
blockedRetrofit2Call.isStarted() `should be equal to` true
blockedRetrofit2Call.isCompleted() `should be equal to` true
}

@Test
fun `Canceled Call should be executed asynchronous and return a cancel error`() = runTest {
val blockedRetrofit2Call = BlockedRetrofit2Call(testCoroutines.scope, value = resultValue)
val call = RetrofitCall(blockedRetrofit2Call, parser, testCoroutines.scope)

val deferedResult = async { call.await() }
delay(10)
call.cancel()
blockedRetrofit2Call.unblock()
val result = deferedResult.await()

result `should be equal to` Call.callCanceledError()
blockedRetrofit2Call.isStarted() `should be equal to` true
blockedRetrofit2Call.isCompleted() `should be equal to` false
blockedRetrofit2Call.isCanceled() `should be equal to` true
}
}
Loading

0 comments on commit ff76c22

Please sign in to comment.