Testing federated subgraph with Apollo GraphQL

Alexander Lindquister
Volvo Car Mobility Tech

--

This is the fifth part in a series of blog posts on the topic of Apollo Federation, and how we use it at Volvo Car Mobility. If you want to read more, make sure to check out our other posts.

In order to write relevant tests for a subgraph, it helps to understand how the federation gateway makes queries to subgraphs. Make sure to read Part 2 (Entities) and Part 4 (Subgraph dependencies), where we explain the _entities query and how the gateway use it in different situations.

Writing tests for a subgraph

When writing integration tests for the federated GraphQL endpoints you can either write type-safe tests using generated client classes or write non type-safe tests utilising a string representation of a query.

Type-safe testing

With io.github.deweyjose:graphqlcodegen-maven-plugin you can enable generating client classes like so: <generateClient>true</generateClient> (This goes in the maven plugins execution configuration)

Consider the Car entity from Part 2:

# Car subgraph
type Car @key(fields: "id") {
id: ID!
model: String!
}

Let’s take a look at how we can write a test for the following query that resolves the fields of Car:

query EntitiesQuery($representations: [Any!]!) {
_entities(representations: $representations) {
... on Car {
id
model
}
}
}

Note that to test the fields of Car in the car subgraph, we must simulate the queries made by the federation gateway to our specific part of the subgraph, which is handled by our microservice. To accomplish this, we utilise the _entities query. DGS uses the _entities query together with the representations input to resolve queries to a subgraph.

The following Kotlin code is an example of how we test the above query using DGS code generation library:

@Test
fun `test small Car entity`() {
// Set up
val entitiesQuery = EntitiesGraphQLQuery.Builder().addRepresentationAsVariable(
CarRepresentation().newBuilder().carId("123").build()
).build()
    val request = GraphQLQueryRequest(
entitiesQuery,
EntitiesProjectionRoot()
.onCar()
.carId()
.model()
)
// Act
val car = dgsQueryExecutor.executeAndExtractJsonPath<Car>(request.serialize(), "data._entities[0]")
// Assert
assertThat(car.carId).isEqualTo("123")
assertThat(car.model).isEqualTo("Volvo V60")
}

This looks relatively compact, but quickly becomes overwhelming when the request is larger. Let’s say there are a bunch of other fields on the Car entity we want to query in our test:

query EntitiesQuery($representations: [Any]) {
_entities(representations: $representations) {
... on Car {
id
model {
name
description
price {
hourlyPrice
}
images {
uri
size {
width
height
}
}
}
color {
name
hexCode
}
damages {
type
images {
uri
size {
width
height
}
}
}
}
}
}

The corresponding test would look like this:

@Test
fun `test large Car entity`() {
// Set up
val entitiesQuery = EntitiesGraphQLQuery.Builder().addRepresentationAsVariable(
CarRepresentation().newBuilder().carId("123").build()
).build()

Non type-safe testing

In certain situations, we encountered the challenge of overly complex and difficult-to-read code while generating entities projections. To mitigate this issue, we decided to adopt a non-typesafe string definition approach, despite the potential drawbacks of not receiving compilation errors in the tests during schema changes. Our test would hopefully fail in runtime though.

Another reason is that generating the client libraries can be time-consuming. If we do not need them in the service resolving the subgraph, we can save some build time.

The above small test then looks as follows:

@Test
fun `test small Car entity`() {
// Set up
val variables = mapOf(
"representations" to listOf(
mapOf(
"__typename" to "Car",
"carId" to "123"
)
)
)
val request = GraphQLQueryRequest(
entitiesQuery,
EntitiesProjectionRoot()
.onCar()
.carId()
.model()
.name()
.description()
.price()
.hourlyPrice()
.parent()
.images()
.uri()
.size()
.width()
.height()
.parent()
.parent()
.parent()
.color()
.name()
.hexCode()
.parent()
.parent()
.damages()
.type()
.images()
.uri()
.size()
.width()
.height()
.parent()
.parent()
)
// Act
val car = dgsQueryExecutor.executeAndExtractJsonPath<Car>(request.serialize(), "data._entities[0]")
// Assert
assertThat(car.carId).isEqualTo("123")
assertThat(car.model).isEqualTo("Volvo V60")
// ...
}

and the larger example

@Test
fun `test large Car entity`() {
// Set up
val variables = mapOf(
"representations" to listOf(
mapOf(
"__typename" to "Car",
"carId" to "123"
)
)
)
// Act
val car = dgsQueryExecutor.executeAndExtractJsonPath<Car>(
entitiesQuery,
"data._entities[0]",
variables
)
// Assert
assertThat(car.carId).isEqualTo("123")
assertThat(car.model).isEqualTo("Volvo V60")
}

val entitiesQuery =
"""
query ($representations: [Any!]!) {
_entities(representations: $representations) {
... on Car {
id
model {
name
description
price {
hourlyPrice
}
images {
uri
size {
width
height
}
}
}
color {
name
hexCode
}
damages {
type
images {
uri
size {
width
height
}
}
}
}
}
}
"""

This is definitely more verbose, but some of us prefer it because it’s easier to read and understand.

Join the movement

This was the last part of our Apollo Federation blog post series. If you have made it this far, great job and thank you! We hope our blog posts have been insightful and useful. If this made you interested in working with Apollo Federation at Volvo Car Mobility, make sure to explore our careers page for open roles.

Written by Iman Radjavi, Christopher Gustafson and Alexander Lindquister

--

--