Kotlin GraphQL Test

GraphQL

Graphql을 통한 API 테스트

  • spring-boot-starter-web:2.7.0
  • spring-boot-starter-graphql:2.7.0
  • junit-jupiter:5.8.2

Graphql over HTTP

MockMvc 를 활용하여 graphql의 요청 및 응답을 JSON 형식으로 처리하여 Spring MVC 테스트를 진행한다.

graphql request & response

graphql over http 요청 시 POST body 이다. 해당 형식을 활용하여 graphql 테스트를 진행한다.

class GraphqlForm {

    data class Request(

        val query: String,

        val operationName: String? = null,

        val variables: Map<String, Any> = mapOf()
    )

    data class Response(

        val data: Map<String, *>

        val errors: List<*>?,
    )
}

support

request의 content-type을 application/graphql+json 이나, application/json로 요청한다. 그리고, spring-graphql의 경우 Mono 를 통한 비동기 요청 및 응답을 하기 때문에 비동기 API 테스트로 진행한다.

@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@AutoConfigureMockMvc
@ActiveProfiles("test")
abstract class ControllerTestSupport {

    companion object {
        protected val objectMapper: ObjectMapper = jacksonObjectMapper()
    }

    @Autowired
    protected lateinit var mockMvc: MockMvc

    @Value("\${spring.graphql.path}")
    private lateinit var graphqlUrl: String

    protected fun postGraphql(requestBody: GraphqlForm.Request): MvcResult {
        val requestResult = mockMvc
            .perform(
                MockMvcRequestBuilders.post(graphqlUrl)
                    .content(objectMapper.writeValueAsBytes(requestBody))
                    .contentType(MediaType.APPLICATION_GRAPHQL)
                    .characterEncoding("UTF-8")
            ).andExpect {
                MockMvcResultMatchers.request().asyncStarted()
                MockMvcResultMatchers.request().asyncResult(CoreMatchers.notNullValue())
            }.andReturn()

        return mockMvc
            .perform(
                MockMvcRequestBuilders.asyncDispatch(requestResult)
            )
            .andExpect(MockMvcResultMatchers.status().isOk)
            .andReturn()
    }

    protected fun <T> getGraphqlResponseBody(name: String, response: String, clazz: Class<T>): T? =
        getResponseBody(name, response)?.let { objectMapper.readValue(objectMapper.writeValueAsString(it), clazz) }

    private fun getResponseBody(name: String, response: String): Any? {
        val graphqlResponse: GraphqlForm.Response = objectMapper.readValue(response)
        return graphqlResponse.data[name]
    }
}

example#1 shorthand syntax

Graphql Request Body의 query필드에 해당 syntax를 작성하여 요청한다.

/*
mutation {
    orderCreate(order: {
        items: [{
            itemId: "62946b347bfac12ff5f2ea56"
            quantity: 1
        }, {
            itemId: "62946b347bfac12ff5f2ea57"
            quantity: 2
        }]
    }) {
        id
        items {
            item {
                id
                name
                price
            }
            quantity
        }
    }
}
*/
@Test
@DisplayName("[GRAPHQL OVER HTTP|MUTATION] 주문 생성하기")
fun orderCreate() {
    /* GIVEN */
    val requestBody = GraphqlForm.Request(
        query = "mutation {\n" +
                "    orderCreate(order: {\n" +
                "        items: [{\n" +
                "            itemId: \"62946b347bfac12ff5f2ea56\"\n" +
                "            quantity: 1\n" +
                "        }, {\n" +
                "            itemId: \"62946b347bfac12ff5f2ea57\"\n" +
                "            quantity: 2\n" +
                "        }]\n" +
                "    }) {\n" +
                "        id\n" +
                "        items {\n" +
                "            item {\n" +
                "                id\n" +
                "                name\n" +
                "                price\n" +
                "            }\n" +
                "            quantity\n" +
                "        }\n" +
                "    }\n" +
                "}",
    )

    /* WHEN */
    val response = postGraphql(requestBody)
    val order = getGraphqlResponseBody("orderCreate", response.response.contentAsString, OrderCollection::class.java)

    /* THEN */
    MatcherAssert.assertThat(order?.items?.sumOf { it.item.price * it.quantity }, CoreMatchers.`is`(closeTo(26.65, 0.001)))
}

example#2 operation syntax

Graphql Request Body의 query필드에 해당 syntax를 작성하며, operationNamevariables를 작성하여 요청한다.

/*
mutation OrderCreate($order: OrderInput){
    orderCreate(order: $order) {
        id
        items {
            item {
                id
                name
                price
            }
            quantity
        }
    }
}

# VARIABLES
{
    "order": {
        "items": [{
            "itemId": "62946b347bfac12ff5f2ea56",
            "quantity": 1
        }, {
            "itemId": "62946b347bfac12ff5f2ea57",
            "quantity": 2
        }]
    }
}
*/
@Test
@DisplayName("[GRAPHQL OVER HTTP|MUTATION] 주문 생성하기 with Operation")
fun orderCreateWithOperation() {
    /* GIVEN */
    val requestBody = GraphqlForm.Request(
        query = "mutation OrderCreate(\$order: OrderInput){\n" +
                "    orderCreate(order: \$order) {\n" +
                "        id\n" +
                "        items {\n" +
                "            item {\n" +
                "                id\n" +
                "                name\n" +
                "                price\n" +
                "            }\n" +
                "            quantity\n" +
                "        }\n" +
                "    }\n" +
                "}",
        operationName = "OrderCreate",
        variables = mapOf(Pair("order", OrderInput.builder()
            .withItems(
                listOf(
                    OrderItemInput.builder()
                        .withItemId("62946b347bfac12ff5f2ea56")
                        .withQuantity(1)
                        .build(),
                    OrderItemInput.builder()
                        .withItemId("62946b347bfac12ff5f2ea57")
                        .withQuantity(2)
                        .build()
                )
            )
            .build()))
    )

    /* WHEN */
    val response = postGraphql(requestBody)
    val order = getGraphqlResponseBody("orderCreate", response.response.contentAsString, OrderCollection::class.java)

    /* THEN */
    MatcherAssert.assertThat(order?.items?.sumOf { it.item.price * it.quantity }, CoreMatchers.`is`(closeTo(26.65, 0.001)))
}

GraphQlTester

org.springframework.graphql:spring-graphql-testGraphQlTester 를 활용하여 손쉽게 Graphql 테스트를 작성할 수 있다. 또한 GraphQL over HTTP인 HttpGraphQlTester , GraphQL over Websocket인 WebSocketGraphQlTester 를 지원한다.

testImplementation("org.springframework.graphql:spring-graphql-test:1.0.0")

support

@AutoConfigureGraphQlTester 를 활용하여 GraphQlTester 빈을 주입받는다.

@SpringBootTest
@AutoConfigureGraphQlTester
@ActiveProfiles("test")
abstract class GraphQlTestSupport {

    @Autowired
    protected lateinit var graphQlTester: GraphQlTester
}

example#1 shorthand syntax

/*
mutation {
    orderCreate(order: {
        items: [{
            itemId: "62946b347bfac12ff5f2ea56"
            quantity: 1
        }, {
            itemId: "62946b347bfac12ff5f2ea57"
            quantity: 2
        }]
    }) {
        id
        items {
            item {
                id
                name
                price
            }
            quantity
        }
    }
}
*/
@Test
@DisplayName("[GRAPHQL|MUTATION] 주문 생성하기")
fun orderCreate() {
    /* GIVEN */
    val query = "mutation {\n" +
            "    orderCreate(order: {\n" +
            "        items: [{\n" +
            "            itemId: \"62946b347bfac12ff5f2ea56\"\n" +
            "            quantity: 1\n" +
            "        }, {\n" +
            "            itemId: \"62946b347bfac12ff5f2ea57\"\n" +
            "            quantity: 2\n" +
            "        }]\n" +
            "    }) {\n" +
            "        id\n" +
            "        items {\n" +
            "            item {\n" +
            "                id\n" +
            "                name\n" +
            "                price\n" +
            "            }\n" +
            "            quantity\n" +
            "        }\n" +
            "    }\n" +
            "}"

    /* WHEN */
    val response = graphQlTester
        .document(query).execute()
        .path("orderCreate").hasValue().entity(OrderCollection::class.java)

    /* THEN */
    MatcherAssert.assertThat(response.get().items?.sumOf { it.item.price * it.quantity }, CoreMatchers.`is`(Matchers.closeTo(26.65, 0.001)))
}

example#2 operation syntax


/*
mutation OrderCreate($order: OrderInput){
    orderCreate(order: $order) {
        id
        items {
            item {
                id
                name
                price
            }
            quantity
        }
    }
}

# VARIABLES
{
    "order": {
        "items": [{
            "itemId": "62946b347bfac12ff5f2ea56",
            "quantity": 1
        }, {
            "itemId": "62946b347bfac12ff5f2ea57",
            "quantity": 2
        }]
    }
}
*/
@Test
@DisplayName("[GRAPHQL|MUTATION] 주문 생성하기 with Operation")
fun orderCreateWithOperation() {
    /* GIVEN */
    val query = "mutation OrderCreate(\$order: OrderInput){\n" +
            "    orderCreate(order: \$order) {\n" +
            "        id\n" +
            "        items {\n" +
            "            item {\n" +
            "                id\n" +
            "                name\n" +
            "                price\n" +
            "            }\n" +
            "            quantity\n" +
            "        }\n" +
            "    }\n" +
            "}"
    val variable = OrderInput.builder()
        .withItems(
            listOf(
                OrderItemInput.builder()
                    .withItemId("62946b347bfac12ff5f2ea56")
                    .withQuantity(1)
                    .build(),
                OrderItemInput.builder()
                    .withItemId("62946b347bfac12ff5f2ea57")
                    .withQuantity(2)
                    .build()
            )
        )
        .build()

    /* WHEN */
    val response = graphQlTester.document(query)
        .operationName("OrderCreate")
        .variable("order", variable.toMap()).execute()
        .path("orderCreate").hasValue().entity(OrderCollection::class.java)

    /* THEN */
    MatcherAssert.assertThat(response.get().items?.sumOf { it.item.price * it.quantity }, CoreMatchers.`is`(Matchers.closeTo(26.65, 0.001)))
}

예제

spring-graphql-example

Leave a comment