Skip to content

Commit

Permalink
Merge pull request #63 from soil-kt/testing-module
Browse files Browse the repository at this point in the history
Add a testing module for soil-query 🧪
  • Loading branch information
ogaclejapan authored Aug 11, 2024
2 parents 3d57997 + d1eef31 commit 3368a47
Show file tree
Hide file tree
Showing 9 changed files with 378 additions and 0 deletions.
1 change: 1 addition & 0 deletions buildSrc/src/main/kotlin/BuildModule.kt
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ val publicModules = setOf(
"soil-query-core",
"soil-query-compose",
"soil-query-compose-runtime",
"soil-query-test",
"soil-form",
"soil-serialization-bundle",
"soil-space"
Expand Down
1 change: 1 addition & 0 deletions settings.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ include(
":soil-query-core",
":soil-query-compose",
":soil-query-compose-runtime",
":soil-query-test",
":soil-form",
":soil-serialization-bundle",
":soil-space"
Expand Down
77 changes: 77 additions & 0 deletions soil-query-test/build.gradle.kts
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
}
}
2 changes: 2 additions & 0 deletions soil-query-test/gradle.properties
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
POM_ARTIFACT_ID=query-test
POM_NAME=query-test
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
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
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 soil-query-test/src/commonMain/kotlin/soil/query/test/TestSwrClient.kt
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)
}
}
}
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()
}

0 comments on commit 3368a47

Please sign in to comment.