Skip to content

Commit 2b7f8c2

Browse files
authored
Add a bunch of unit test coverage for mutations and queries (#6296)
1 parent 2f96d94 commit 2b7f8c2

File tree

12 files changed

+1304
-9
lines changed

12 files changed

+1304
-9
lines changed

firebase-dataconnect/api.txt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -193,6 +193,9 @@ package com.google.firebase.dataconnect.core {
193193
public final class LoggerKt {
194194
}
195195

196+
public final class MutationRefImplKt {
197+
}
198+
196199
public final class QueryRefImplKt {
197200
}
198201

firebase-dataconnect/firebase-dataconnect.gradle.kts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -148,6 +148,7 @@ dependencies {
148148
testImplementation(libs.androidx.test.junit)
149149
testImplementation(libs.kotest.assertions)
150150
testImplementation(libs.kotest.property)
151+
testImplementation(libs.kotest.property.arbs)
151152
testImplementation(libs.mockk)
152153
testImplementation(libs.robolectric)
153154
testImplementation(libs.truth)
@@ -173,6 +174,7 @@ dependencies {
173174
androidTestImplementation(libs.kotlin.coroutines.test)
174175
androidTestImplementation(libs.kotest.assertions)
175176
androidTestImplementation(libs.kotest.property)
177+
androidTestImplementation(libs.kotest.property.arbs)
176178
androidTestImplementation(libs.mockk)
177179
androidTestImplementation(libs.mockk.android)
178180
androidTestImplementation(libs.truth)

firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/core/MutationRefImpl.kt

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,3 +94,45 @@ internal class MutationRefImpl<Data, Variables>(
9494
override fun toString() = "MutationResultImpl(data=$data, ref=$ref)"
9595
}
9696
}
97+
98+
internal fun <Data, Variables> MutationRefImpl<Data, Variables>.copy(
99+
dataConnect: FirebaseDataConnectInternal = this.dataConnect,
100+
operationName: String = this.operationName,
101+
variables: Variables = this.variables,
102+
dataDeserializer: DeserializationStrategy<Data> = this.dataDeserializer,
103+
variablesSerializer: SerializationStrategy<Variables> = this.variablesSerializer,
104+
isFromGeneratedSdk: Boolean = this.isFromGeneratedSdk,
105+
) =
106+
MutationRefImpl(
107+
dataConnect = dataConnect,
108+
operationName = operationName,
109+
variables = variables,
110+
dataDeserializer = dataDeserializer,
111+
variablesSerializer = variablesSerializer,
112+
isFromGeneratedSdk = isFromGeneratedSdk,
113+
)
114+
115+
internal fun <Data, NewVariables> MutationRefImpl<Data, *>.withVariablesSerializer(
116+
variables: NewVariables,
117+
variablesSerializer: SerializationStrategy<NewVariables>,
118+
): MutationRefImpl<Data, NewVariables> =
119+
MutationRefImpl(
120+
dataConnect = dataConnect,
121+
operationName = operationName,
122+
variables = variables,
123+
dataDeserializer = dataDeserializer,
124+
variablesSerializer = variablesSerializer,
125+
isFromGeneratedSdk = isFromGeneratedSdk,
126+
)
127+
128+
internal fun <NewData, Variables> MutationRefImpl<*, Variables>.withDataDeserializer(
129+
dataDeserializer: DeserializationStrategy<NewData>,
130+
): MutationRefImpl<NewData, Variables> =
131+
MutationRefImpl(
132+
dataConnect = dataConnect,
133+
operationName = operationName,
134+
variables = variables,
135+
dataDeserializer = dataDeserializer,
136+
variablesSerializer = variablesSerializer,
137+
isFromGeneratedSdk = isFromGeneratedSdk,
138+
)

firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/core/QueryRefImpl.kt

Lines changed: 14 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -69,12 +69,19 @@ internal class QueryRefImpl<Data, Variables>(
6969
}
7070
}
7171

72-
internal fun <Data, Variables> QueryRefImpl<Data, Variables>.withVariables(variables: Variables) =
72+
internal fun <Data, Variables> QueryRefImpl<Data, Variables>.copy(
73+
dataConnect: FirebaseDataConnectInternal = this.dataConnect,
74+
operationName: String = this.operationName,
75+
variables: Variables = this.variables,
76+
dataDeserializer: DeserializationStrategy<Data> = this.dataDeserializer,
77+
variablesSerializer: SerializationStrategy<Variables> = this.variablesSerializer,
78+
isFromGeneratedSdk: Boolean = this.isFromGeneratedSdk,
79+
) =
7380
QueryRefImpl(
74-
dataConnect,
75-
operationName,
76-
variables,
77-
dataDeserializer,
78-
variablesSerializer,
79-
isFromGeneratedSdk,
81+
dataConnect = dataConnect,
82+
operationName = operationName,
83+
variables = variables,
84+
dataDeserializer = dataDeserializer,
85+
variablesSerializer = variablesSerializer,
86+
isFromGeneratedSdk = isFromGeneratedSdk,
8087
)

firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/core/QuerySubscriptionImpl.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,7 @@ internal class QuerySubscriptionImpl<Data, Variables>(query: QueryRefImpl<Data,
6969
}
7070

7171
override suspend fun update(variables: Variables) {
72-
_query.value = _query.value.withVariables(variables)
72+
_query.value = _query.value.copy(variables = variables)
7373
reload()
7474
}
7575

firebase-dataconnect/src/test/kotlin/com/google/firebase/dataconnect/core/DataConnectGrpcClientUnitTest.kt

Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,16 +16,22 @@
1616
package com.google.firebase.dataconnect.core
1717

1818
import com.google.firebase.dataconnect.DataConnectError
19+
import com.google.firebase.dataconnect.DataConnectException
20+
import com.google.firebase.dataconnect.DataConnectUntypedData
1921
import com.google.firebase.dataconnect.core.DataConnectGrpcClient.OperationResult
2022
import com.google.firebase.dataconnect.testutil.DataConnectLogLevelRule
2123
import com.google.firebase.dataconnect.testutil.connectorConfig
24+
import com.google.firebase.dataconnect.testutil.dataConnectError
2225
import com.google.firebase.dataconnect.testutil.iterator
2326
import com.google.firebase.dataconnect.testutil.newMockLogger
2427
import com.google.firebase.dataconnect.testutil.operationName
28+
import com.google.firebase.dataconnect.testutil.operationResult
2529
import com.google.firebase.dataconnect.testutil.projectId
2630
import com.google.firebase.dataconnect.testutil.requestId
2731
import com.google.firebase.dataconnect.testutil.shouldHaveLoggedExactlyOneMessageContaining
2832
import com.google.firebase.dataconnect.util.buildStructProto
33+
import com.google.firebase.dataconnect.util.encodeToStruct
34+
import com.google.firebase.dataconnect.util.toMap
2935
import com.google.protobuf.ListValue
3036
import com.google.protobuf.Value
3137
import google.firebase.dataconnect.proto.ExecuteMutationRequest
@@ -36,24 +42,38 @@ import google.firebase.dataconnect.proto.GraphqlError
3642
import google.firebase.dataconnect.proto.SourceLocation
3743
import io.grpc.Status
3844
import io.grpc.StatusException
45+
import io.kotest.assertions.asClue
3946
import io.kotest.assertions.throwables.shouldThrow
47+
import io.kotest.matchers.collections.shouldContainExactly
48+
import io.kotest.matchers.nulls.shouldBeNull
4049
import io.kotest.matchers.shouldBe
50+
import io.kotest.matchers.string.shouldContain
4151
import io.kotest.matchers.types.shouldBeSameInstanceAs
4252
import io.kotest.property.Arb
4353
import io.kotest.property.RandomSource
4454
import io.kotest.property.arbitrary.Codepoint
4555
import io.kotest.property.arbitrary.alphanumeric
4656
import io.kotest.property.arbitrary.boolean
4757
import io.kotest.property.arbitrary.egyptianHieroglyphs
58+
import io.kotest.property.arbitrary.filter
4859
import io.kotest.property.arbitrary.int
60+
import io.kotest.property.arbitrary.map
4961
import io.kotest.property.arbitrary.merge
5062
import io.kotest.property.arbitrary.next
5163
import io.kotest.property.arbitrary.string
64+
import io.kotest.property.arbs.firstName
65+
import io.kotest.property.arbs.travel.airline
66+
import io.kotest.property.checkAll
5267
import io.mockk.coEvery
5368
import io.mockk.coVerify
69+
import io.mockk.every
5470
import io.mockk.mockk
71+
import io.mockk.spyk
5572
import java.util.concurrent.atomic.AtomicBoolean
5673
import kotlinx.coroutines.test.runTest
74+
import kotlinx.serialization.DeserializationStrategy
75+
import kotlinx.serialization.Serializable
76+
import kotlinx.serialization.serializer
5777
import org.junit.Rule
5878
import org.junit.Test
5979

@@ -584,3 +604,107 @@ class DataConnectGrpcClientUnitTest {
584604
}
585605
}
586606
}
607+
608+
@Suppress("IMPLICIT_NOTHING_TYPE_ARGUMENT_AGAINST_NOT_NOTHING_EXPECTED_TYPE")
609+
class DataConnectGrpcClientOperationResultUnitTest {
610+
611+
@Test
612+
fun `deserialize() should ignore the module given with DataConnectUntypedData`() {
613+
val errors = listOf(Arb.dataConnectError().next())
614+
val operationResult = OperationResult(buildStructProto { put("foo", 42.0) }, errors)
615+
val result = operationResult.deserialize(DataConnectUntypedData)
616+
result shouldBe DataConnectUntypedData(mapOf("foo" to 42.0), errors)
617+
}
618+
619+
@Test
620+
fun `deserialize() should treat DataConnectUntypedData specially`() = runTest {
621+
checkAll(iterations = 1000, Arb.operationResult()) { operationResult ->
622+
val result = operationResult.deserialize(DataConnectUntypedData)
623+
624+
result.asClue {
625+
if (operationResult.data === null) {
626+
it.data.shouldBeNull()
627+
} else {
628+
it.data shouldBe operationResult.data.toMap()
629+
}
630+
it.errors shouldContainExactly operationResult.errors
631+
}
632+
}
633+
}
634+
635+
@Test
636+
fun `deserialize() should throw if one or more errors and data is null`() = runTest {
637+
val arb = Arb.operationResult().filter { it.errors.isNotEmpty() }.map { it.copy(data = null) }
638+
checkAll(iterations = 5, arb) { operationResult ->
639+
val exception =
640+
shouldThrow<DataConnectException> { operationResult.deserialize<Nothing>(mockk()) }
641+
exception.message shouldContain "${operationResult.errors}"
642+
}
643+
}
644+
645+
@Test
646+
fun `deserialize() should throw if one or more errors and data is _not_ null`() = runTest {
647+
val arb = Arb.operationResult().filter { it.data !== null && it.errors.isNotEmpty() }
648+
checkAll(iterations = 5, arb) { operationResult ->
649+
val exception =
650+
shouldThrow<DataConnectException> { operationResult.deserialize<Nothing>(mockk()) }
651+
exception.message shouldContain "${operationResult.errors}"
652+
}
653+
}
654+
655+
@Test
656+
fun `deserialize() should throw if data is null and errors is empty`() {
657+
val operationResult = OperationResult(data = null, errors = emptyList())
658+
val exception =
659+
shouldThrow<DataConnectException> { operationResult.deserialize<Nothing>(mockk()) }
660+
exception.message shouldContain "no data"
661+
}
662+
663+
@Test
664+
fun `deserialize() successfully deserializes`() = runTest {
665+
val testData = TestData(Arb.firstName().next().name)
666+
val operationResult = OperationResult(encodeToStruct(testData), errors = emptyList())
667+
668+
val deserializedData = operationResult.deserialize(serializer<TestData>())
669+
670+
deserializedData shouldBe testData
671+
}
672+
673+
@Test
674+
fun `deserialize() throws if decoding fails`() = runTest {
675+
val data = buildStructProto { put("zzzz", 42) }
676+
val operationResult = OperationResult(data, errors = emptyList())
677+
shouldThrow<DataConnectException> { operationResult.deserialize(serializer<TestData>()) }
678+
}
679+
680+
@Test
681+
fun `deserialize() re-throws DataConnectException`() = runTest {
682+
val data = encodeToStruct(TestData("fe45zhyd3m"))
683+
val operationResult = OperationResult(data = data, errors = emptyList())
684+
val deserializer: DeserializationStrategy<TestData> = spyk(serializer())
685+
val exception = DataConnectException(message = Arb.airline().next().name)
686+
every { deserializer.deserialize(any()) } throws (exception)
687+
688+
val thrownException =
689+
shouldThrow<DataConnectException> { operationResult.deserialize(deserializer) }
690+
691+
thrownException shouldBeSameInstanceAs exception
692+
}
693+
694+
@Test
695+
fun `deserialize() wraps non-DataConnectException in DataConnectException`() = runTest {
696+
val data = encodeToStruct(TestData("rbmkny6b4r"))
697+
val operationResult = OperationResult(data = data, errors = emptyList())
698+
val deserializer: DeserializationStrategy<TestData> = spyk(serializer())
699+
class MyException : Exception("y3cx44q43q")
700+
val exception = MyException()
701+
every { deserializer.deserialize(any()) } throws (exception)
702+
703+
val thrownException =
704+
shouldThrow<DataConnectException> { operationResult.deserialize(deserializer) }
705+
706+
thrownException.cause shouldBeSameInstanceAs exception
707+
}
708+
709+
@Serializable data class TestData(val foo: String)
710+
}

0 commit comments

Comments
 (0)