How do we test Ktor server schemas? #1860
-
Hi. How do we approach testing the Ktor server with Expedia GraphQL 7+? In the lower version without GraphQl plugin I had access to the GraphQL, which I was using to get GraphQLRequestHandler and then I was able to send requests. This is how my test wrapper looked like: // from test
fun test(
users: List<User> = mockUsers,
query: String,
variables: Map<String, Any?>? = emptyMap(),
graphQLContext: Map<*, Any> = mapOf("id" to user1.id),
assert: suspend (GraphQLResponse<*>) -> Unit,
) {
val userRepository: UserRepository by inject(UserRepository::class.java)
val graphQLRequestHandler = GraphQLRequestHandler(getGraphQLObject()) // getGraphQLObject() cannot be build now
testApplication {
users.forEach { userRepository.create(it) }
val request = GraphQLRequest(query, null, variables, null)
val response = graphQLRequestHandler.executeRequest(
request,
graphQLContext = graphQLContext
) as GraphQLResponse<*>
assert(response)
}
}
// from main
fun getGraphQLObject(): GraphQL = GraphQL.newGraphQL(schema) // Not valid anymore
.valueUnboxer(IDValueUnboxer())
.build() The test itself looked like this: private const val QUERY =
"mutation Login(\$authInput: AuthInput!) { register(authInput: \$authInput) { token userMinimal { id displayName imageUrl }}}"
@Test
fun `Sign up new user should return data with AuthResponse`() = test(
users = emptyList(),
query = QUERY,
variables = mapOf(
"authInput" to mapOf(
"email" to user1.email,
"password" to password1
)
),
assert = { response ->
response.data.toString() shouldContain "{signUp={token=" shouldContain "userMinimal={id=" shouldContain ", displayName= , imageUrl=}}}"
assertNull(response.errors)
}
) The issue is that currently I have no access to the |
Beta Was this translation helpful? Give feedback.
Replies: 2 comments 1 reply
-
Hello 👋 |
Beta Was this translation helpful? Give feedback.
-
I can share the setup I'm using right now. Maybe someone else had a similar use case. My setup is Expedia GraphQL with MongoDb. To run Mongo locally for tests, I use TestContainers with Docker. Here are the test dependencies: // Server
expedia-ktor-server = { module = "com.expediagroup:graphql-kotlin-ktor-server", version.ref = "expedia" }
expedia-federation = { module = "com.expediagroup:graphql-kotlin-federation", version.ref = "expedia" }
server-mongodb = { module = "org.mongodb:mongodb-driver-kotlin-coroutine", version.ref = "mongodb" }
// Tests
test-kluent = { module = "org.amshove.kluent:kluent", version.ref = "kluent" }
test-koin = { module = "io.insert-koin:koin-test", version.ref = "koin" }
test-kotlin-koin-junit = { module = "io.insert-koin:koin-test-junit4", version.ref = "koin" }
test-kotlin-mockk = { module = "io.mockk:mockk", version.ref = "mockk" }
test-kotlin-ktor-server = { module = "io.ktor:ktor-server-tests", version.ref = "ktor" }
test-kotlin-ktor-serverHost = { module = "io.ktor:ktor-server-test-host", version.ref = "ktor" }
test-testContainers-mongo = { module = "org.testcontainers:mongodb", version.ref = "testContainers" }
test-junit-jupiter-api = { module = "org.junit.jupiter:junit-jupiter-api", version.ref = "junit" }
test-junit-jupiter-params = { module = "org.junit.jupiter:junit-jupiter-params", version.ref = "junit" }
test-junit-jupiter-engine = { module = "org.junit.jupiter:junit-jupiter-engine", version.ref = "junit" } Here is the test class setup: private const val MONGO_CONTAINER = "mongo:4.0.10"
private const val DB_NAME = "test_db"
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
abstract class ServerTests<T : Any> {
private lateinit var collection: MongoCollection<T>
private val mongoDBContainer = MongoDBContainer(DockerImageName.parse(MONGO_CONTAINER))
.waitingFor(org.testcontainers.containers.wait.strategy.Wait.forListeningPort())
.withReuse(true)
abstract fun initCollection(database: MongoDatabase): MongoCollection<T>
@BeforeAll
fun setupAll() {
mongoDBContainer.addExposedPort(27017)
mongoDBContainer.start()
}
@BeforeEach
fun setup() {
val mongoClient = MongoClient.create(mongoDBContainer.connectionString)
val database = mongoClient.getDatabase(DB_NAME)
collection = initCollection(database)
startKoin {
modules(
module {
factory { mongoClient }
single { database }
},
authModule,
userModule,
messageModule,
paymentModule,
)
}
}
@AfterAll
fun cleanup() {
mongoDBContainer.stop()
}
@AfterEach
fun teardown() {
runBlocking {
collection.drop()
stopKoin()
}
}
fun test(
users: List<User> = mockUsers,
query: String,
operationName: String? = null,
variables: Map<String, Any?>? = emptyMap(),
headers: Map<*, Any> = mapOf("id" to user1.id),
block: suspend HttpResponse.() -> Unit,
) {
testApplication {
application {
install(io.ktor.server.plugins.contentnegotiation.ContentNegotiation) {
jackson()
}
}
populateDatabase(
users = users
)
val client = getHttpClient()
val response = client.post("graphql") {
contentType(ContentType.Application.Json)
headers.forEach { (key, value) ->
header(key.toString(), value.toString())
}
setBody(
GraphQLRequest(
query = query,
operationName = operationName,
variables = variables,
)
)
}
println(response.bodyAsText())
block(response)
}
}
private suspend fun populateDatabase(users: List<User>) {
val userRepository: UserRepository by KoinJavaComponent.inject(UserRepository::class.java)
users.forEach { userRepository.create(it) }
}
context(ApplicationTestBuilder)
private fun getHttpClient(): HttpClient {
return createClient {
install(io.ktor.client.plugins.contentnegotiation.ContentNegotiation) {
jackson {
enable(SerializationFeature.INDENT_OUTPUT)
setDefaultLeniency(true)
registerModules(
KotlinModule(
nullToEmptyMap = true,
nullToEmptyCollection = true,
)
)
}
}
}
}
}
fun Application.testGraphQLModule() {
configureGraphQL()
install(io.ktor.server.websocket.WebSockets)
install(Routing) {
graphQLGetRoute()
graphQLPostRoute()
graphQLSubscriptionsRoute()
graphQLSDLRoute()
graphiQLRoute()
}
} To handle GraphQL response with operation name in data, I use an extension: suspend inline fun <reified T> HttpResponse.getData(key: String): T {
val mapper = jacksonObjectMapper()
mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false)
val body = body<GraphQLResponse<Map<String, Any>>>()
val dataMap = body.data?.get(key) as? Map<*, *>
?: throw IllegalStateException("Key '$key' not found or not a map, in ${this.bodyAsText()}.")
return mapper.convertValue(dataMap)
} And the example test: private const val OPERATION = "register"
private const val QUERY =
"mutation $OPERATION(\$authInput: AuthInput!) { $OPERATION(authInput: \$authInput) " +
"{ token userMinimal { id email }}}"
class RegisterTests : ServerTests<User>() {
override fun initCollection(database: MongoDatabase): MongoCollection<User> {
return database.getCollection<User>(User::class.java.simpleName)
}
@Test
fun `when registering user with email and password then the user should be registered`() = test(
users = emptyList(),
query = QUERY,
operationName = OPERATION,
variables = mapOf(
"authInput" to mapOf(
"email" to user1.email,
"password" to password1,
)
),
) {
val authResponse = getData<AuthResponse>(OPERATION)
HttpStatusCode.OK shouldBeEqualTo status
authResponse.userMinimal.email shouldBeEqualTo user1.email
} @dariuszkuc let me know if you have any suggestions on improving it, Please :) |
Beta Was this translation helpful? Give feedback.
Hello 👋
Take a look at Ktor testing documentation and our integration test.