-
Notifications
You must be signed in to change notification settings - Fork 2
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #63 from soil-kt/testing-module
Add a testing module for soil-query 🧪
- Loading branch information
Showing
9 changed files
with
378 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,77 @@ | ||
import org.jetbrains.kotlin.gradle.targets.js.dsl.ExperimentalWasmDsl | ||
|
||
plugins { | ||
alias(libs.plugins.android.library) | ||
alias(libs.plugins.kotlin.multiplatform) | ||
alias(libs.plugins.maven.publish) | ||
alias(libs.plugins.dokka) | ||
} | ||
|
||
val buildTarget = the<BuildTargetExtension>() | ||
|
||
kotlin { | ||
applyDefaultHierarchyTemplate() | ||
|
||
jvm() | ||
androidTarget { | ||
compilations.all { | ||
kotlinOptions { | ||
jvmTarget = buildTarget.javaVersion.get().toString() | ||
} | ||
} | ||
publishLibraryVariants("release") | ||
} | ||
|
||
iosX64() | ||
iosArm64() | ||
iosSimulatorArm64() | ||
|
||
@OptIn(ExperimentalWasmDsl::class) | ||
wasmJs { | ||
browser() | ||
} | ||
|
||
sourceSets { | ||
commonMain.dependencies { | ||
api(libs.kotlinx.coroutines.core) | ||
api(projects.soilQueryCore) | ||
} | ||
|
||
commonTest.dependencies { | ||
implementation(libs.kotlin.test) | ||
implementation(libs.kotlinx.coroutines.test) | ||
implementation(projects.internal.testing) | ||
} | ||
} | ||
} | ||
|
||
android { | ||
namespace = "soil.query.test" | ||
compileSdk = buildTarget.androidCompileSdk.get() | ||
|
||
sourceSets["main"].manifest.srcFile("src/androidMain/AndroidManifest.xml") | ||
sourceSets["main"].res.srcDirs("src/androidMain/res") | ||
sourceSets["main"].resources.srcDirs("src/commonMain/resources") | ||
|
||
defaultConfig { | ||
minSdk = buildTarget.androidMinSdk.get() | ||
} | ||
packaging { | ||
resources { | ||
excludes += "/META-INF/{AL2.0,LGPL2.1}" | ||
} | ||
} | ||
buildTypes { | ||
getByName("release") { | ||
isMinifyEnabled = false | ||
} | ||
} | ||
compileOptions { | ||
sourceCompatibility = buildTarget.javaVersion.get() | ||
targetCompatibility = buildTarget.javaVersion.get() | ||
} | ||
@Suppress("UnstableApiUsage") | ||
testOptions { | ||
unitTests.isIncludeAndroidResources = true | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,2 @@ | ||
POM_ARTIFACT_ID=query-test | ||
POM_NAME=query-test |
19 changes: 19 additions & 0 deletions
19
soil-query-test/src/commonMain/kotlin/soil/query/test/FakeInfiniteQueryKey.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,19 @@ | ||
// Copyright 2024 Soil Contributors | ||
// SPDX-License-Identifier: Apache-2.0 | ||
|
||
package soil.query.test | ||
|
||
import soil.query.InfiniteQueryKey | ||
import soil.query.QueryReceiver | ||
|
||
/** | ||
* Creates a fake infinite query key that returns the result of the given [mock] function. | ||
*/ | ||
class FakeInfiniteQueryKey<T, S>( | ||
private val target: InfiniteQueryKey<T, S>, | ||
private val mock: FakeInfiniteQueryFetch<T, S> | ||
) : InfiniteQueryKey<T, S> by target { | ||
override val fetch: suspend QueryReceiver.(param: S) -> T = { mock(it) } | ||
} | ||
|
||
typealias FakeInfiniteQueryFetch<T, S> = suspend (param: S) -> T |
19 changes: 19 additions & 0 deletions
19
soil-query-test/src/commonMain/kotlin/soil/query/test/FakeMutationKey.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,19 @@ | ||
// Copyright 2024 Soil Contributors | ||
// SPDX-License-Identifier: Apache-2.0 | ||
|
||
package soil.query.test | ||
|
||
import soil.query.MutationKey | ||
import soil.query.MutationReceiver | ||
|
||
/** | ||
* Creates a fake mutation key that returns the result of the given [mock] function. | ||
*/ | ||
class FakeMutationKey<T, S>( | ||
private val target: MutationKey<T, S>, | ||
private val mock: FakeMutationMutate<T, S> | ||
) : MutationKey<T, S> by target { | ||
override val mutate: suspend MutationReceiver.(variable: S) -> T = { mock(it) } | ||
} | ||
|
||
typealias FakeMutationMutate<T, S> = suspend (variable: S) -> T |
19 changes: 19 additions & 0 deletions
19
soil-query-test/src/commonMain/kotlin/soil/query/test/FakeQueryKey.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,19 @@ | ||
// Copyright 2024 Soil Contributors | ||
// SPDX-License-Identifier: Apache-2.0 | ||
|
||
package soil.query.test | ||
|
||
import soil.query.QueryKey | ||
import soil.query.QueryReceiver | ||
|
||
/** | ||
* Creates a fake query key that returns the result of the given [mock] function. | ||
*/ | ||
class FakeQueryKey<T>( | ||
private val target: QueryKey<T>, | ||
private val mock: FakeQueryFetch<T> | ||
) : QueryKey<T> by target { | ||
override val fetch: suspend QueryReceiver.() -> T = { mock() } | ||
} | ||
|
||
typealias FakeQueryFetch<T> = suspend () -> T |
102 changes: 102 additions & 0 deletions
102
soil-query-test/src/commonMain/kotlin/soil/query/test/TestSwrClient.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,102 @@ | ||
// Copyright 2024 Soil Contributors | ||
// SPDX-License-Identifier: Apache-2.0 | ||
|
||
package soil.query.test | ||
|
||
import soil.query.InfiniteQueryId | ||
import soil.query.InfiniteQueryKey | ||
import soil.query.InfiniteQueryRef | ||
import soil.query.MutationId | ||
import soil.query.MutationKey | ||
import soil.query.MutationRef | ||
import soil.query.QueryId | ||
import soil.query.QueryKey | ||
import soil.query.QueryRef | ||
import soil.query.SwrClient | ||
|
||
/** | ||
* This extended interface of the [SwrClient] provides the capability to mock specific queries and mutations for the purpose of testing. | ||
* By registering certain keys as mocks, you can control the behavior of these specific keys while the rest of the keys function normally. | ||
* This allows for more targeted and precise testing of your application. | ||
* | ||
* ```kotlin | ||
* val client = SwrCache(..) | ||
* val testClient = client.test() | ||
* testClient.mock(MyQueryId) { "returned fake data" } | ||
* | ||
* testClient.doSomething() | ||
* ``` | ||
*/ | ||
interface TestSwrClient : SwrClient { | ||
|
||
/** | ||
* Mocks the mutation process corresponding to [MutationId]. | ||
*/ | ||
fun <T, S> mock(id: MutationId<T, S>, mutate: FakeMutationMutate<T, S>) | ||
|
||
/** | ||
* Mocks the query process corresponding to [QueryId]. | ||
*/ | ||
fun <T> mock(id: QueryId<T>, fetch: FakeQueryFetch<T>) | ||
|
||
/** | ||
* Mocks the query process corresponding to [InfiniteQueryId]. | ||
*/ | ||
fun <T, S> mock(id: InfiniteQueryId<T, S>, fetch: FakeInfiniteQueryFetch<T, S>) | ||
} | ||
|
||
/** | ||
* Switches [SwrClient] to a test interface. | ||
*/ | ||
fun SwrClient.test(): TestSwrClient = TestSwrClientImpl(this) | ||
|
||
internal class TestSwrClientImpl( | ||
private val target: SwrClient | ||
) : TestSwrClient, SwrClient by target { | ||
|
||
private val mockMutations = mutableMapOf<MutationId<*, *>, FakeMutationMutate<*, *>>() | ||
private val mockQueries = mutableMapOf<QueryId<*>, FakeQueryFetch<*>>() | ||
private val mockInfiniteQueries = mutableMapOf<InfiniteQueryId<*, *>, FakeInfiniteQueryFetch<*, *>>() | ||
|
||
override fun <T, S> mock(id: MutationId<T, S>, mutate: FakeMutationMutate<T, S>) { | ||
mockMutations[id] = mutate | ||
} | ||
|
||
override fun <T> mock(id: QueryId<T>, fetch: FakeQueryFetch<T>) { | ||
mockQueries[id] = fetch | ||
} | ||
|
||
override fun <T, S> mock(id: InfiniteQueryId<T, S>, fetch: FakeInfiniteQueryFetch<T, S>) { | ||
mockInfiniteQueries[id] = fetch | ||
} | ||
|
||
@Suppress("UNCHECKED_CAST") | ||
override fun <T, S> getMutation(key: MutationKey<T, S>): MutationRef<T, S> { | ||
val mock = mockMutations[key.id] as? FakeMutationMutate<T, S> | ||
return if (mock != null) { | ||
target.getMutation(FakeMutationKey(key, mock)) | ||
} else { | ||
target.getMutation(key) | ||
} | ||
} | ||
|
||
@Suppress("UNCHECKED_CAST") | ||
override fun <T> getQuery(key: QueryKey<T>): QueryRef<T> { | ||
val mock = mockQueries[key.id] as? FakeQueryFetch<T> | ||
return if (mock != null) { | ||
target.getQuery(FakeQueryKey(key, mock)) | ||
} else { | ||
target.getQuery(key) | ||
} | ||
} | ||
|
||
@Suppress("UNCHECKED_CAST") | ||
override fun <T, S> getInfiniteQuery(key: InfiniteQueryKey<T, S>): InfiniteQueryRef<T, S> { | ||
val mock = mockInfiniteQueries[key.id] as? FakeInfiniteQueryFetch<T, S> | ||
return if (mock != null) { | ||
target.getInfiniteQuery(FakeInfiniteQueryKey(key, mock)) | ||
} else { | ||
target.getInfiniteQuery(key) | ||
} | ||
} | ||
} |
138 changes: 138 additions & 0 deletions
138
soil-query-test/src/commonTest/kotlin/soil/query/test/TestSwrClientTest.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,138 @@ | ||
// Copyright 2024 Soil Contributors | ||
// SPDX-License-Identifier: Apache-2.0 | ||
|
||
package soil.query.test | ||
|
||
import kotlinx.coroutines.CompletableDeferred | ||
import kotlinx.coroutines.ExperimentalCoroutinesApi | ||
import kotlinx.coroutines.completeWith | ||
import kotlinx.coroutines.test.UnconfinedTestDispatcher | ||
import kotlinx.coroutines.test.runTest | ||
import soil.query.InfiniteQueryCommands | ||
import soil.query.InfiniteQueryId | ||
import soil.query.InfiniteQueryKey | ||
import soil.query.InfiniteQueryRef | ||
import soil.query.MutationId | ||
import soil.query.MutationKey | ||
import soil.query.QueryChunks | ||
import soil.query.QueryCommands | ||
import soil.query.QueryId | ||
import soil.query.QueryKey | ||
import soil.query.QueryRef | ||
import soil.query.SwrCache | ||
import soil.query.SwrCachePolicy | ||
import soil.query.buildInfiniteQueryKey | ||
import soil.query.buildMutationKey | ||
import soil.query.buildQueryKey | ||
import soil.query.mutate | ||
import soil.testing.UnitTest | ||
import kotlin.test.Test | ||
import kotlin.test.assertEquals | ||
|
||
@OptIn(ExperimentalCoroutinesApi::class) | ||
class TestSwrClientTest : UnitTest() { | ||
|
||
@Test | ||
fun testMutation() = runTest { | ||
val client = SwrCache( | ||
policy = SwrCachePolicy( | ||
coroutineScope = backgroundScope, | ||
mainDispatcher = UnconfinedTestDispatcher(testScheduler) | ||
) | ||
) | ||
val testClient = client.test().apply { | ||
mock(ExampleMutationKey.Id) { | ||
"Hello, World!" | ||
} | ||
} | ||
val key = ExampleMutationKey() | ||
val mutation = testClient.getMutation(key).also { it.launchIn(backgroundScope) } | ||
mutation.mutate(0) | ||
assertEquals("Hello, World!", mutation.state.value.data) | ||
} | ||
|
||
@Test | ||
fun testQuery() = runTest { | ||
val client = SwrCache( | ||
policy = SwrCachePolicy( | ||
coroutineScope = backgroundScope, | ||
mainDispatcher = UnconfinedTestDispatcher(testScheduler) | ||
) | ||
) | ||
val testClient = client.test().apply { | ||
mock(ExampleQueryKey.Id) { | ||
"Hello, World!" | ||
} | ||
} | ||
val key = ExampleQueryKey() | ||
val query = testClient.getQuery(key).also { it.launchIn(backgroundScope) } | ||
query.test() | ||
assertEquals("Hello, World!", query.state.value.data) | ||
} | ||
|
||
@Test | ||
fun testInfiniteQuery() = runTest { | ||
val client = SwrCache( | ||
policy = SwrCachePolicy( | ||
coroutineScope = backgroundScope, | ||
mainDispatcher = UnconfinedTestDispatcher(testScheduler) | ||
) | ||
) | ||
val testClient = client.test().apply { | ||
mock(ExampleInfiniteQueryKey.Id) { | ||
"Hello, World!" | ||
} | ||
} | ||
val key = ExampleInfiniteQueryKey() | ||
val query = testClient.getInfiniteQuery(key).also { it.launchIn(backgroundScope) } | ||
query.test() | ||
assertEquals("Hello, World!", query.state.value.data?.first()?.data) | ||
} | ||
} | ||
|
||
private class ExampleMutationKey : MutationKey<String, Int> by buildMutationKey( | ||
id = Id, | ||
mutate = { | ||
error("Not implemented") | ||
} | ||
) { | ||
object Id : MutationId<String, Int>( | ||
namespace = "mutation/example" | ||
) | ||
} | ||
|
||
private class ExampleQueryKey : QueryKey<String> by buildQueryKey( | ||
id = Id, | ||
fetch = { | ||
error("Not implemented") | ||
} | ||
) { | ||
object Id : QueryId<String>( | ||
namespace = "query/example" | ||
) | ||
} | ||
|
||
private class ExampleInfiniteQueryKey : InfiniteQueryKey<String, Int> by buildInfiniteQueryKey( | ||
id = Id, | ||
fetch = { | ||
error("Not implemented") | ||
}, | ||
initialParam = { 0 }, | ||
loadMoreParam = { null } | ||
) { | ||
object Id : InfiniteQueryId<String, Int>( | ||
namespace = "infinite-query/example" | ||
) | ||
} | ||
|
||
private suspend fun <T> QueryRef<T>.test(): T { | ||
val deferred = CompletableDeferred<T>() | ||
send(QueryCommands.Connect(key, callback = deferred::completeWith)) | ||
return deferred.await() | ||
} | ||
|
||
private suspend fun <T, S> InfiniteQueryRef<T, S>.test(): QueryChunks<T, S> { | ||
val deferred = CompletableDeferred<QueryChunks<T, S>>() | ||
send(InfiniteQueryCommands.Connect(key, callback = deferred::completeWith)) | ||
return deferred.await() | ||
} |