Integration Testing
The Star Wars demo shows how to build a Viaduct tenant. This page teaches how to test it—both through end-to-end HTTP calls and with fast, focused resolver unit tests.
- If you want a gentle overview of what gets tested and why, see Testing Overview.
- If you want to jump straight to code, start with Use
DefaultAbstractResolverTestBase.
What you will learn
- How to run integration tests against
/graphqlwith@SpringBootTestandTestRestTemplate. - How to write unit tests for resolvers using
DefaultAbstractResolverTestBase. - How to test field, node, query, and batch resolvers.
- How to pass arguments, build GlobalID values, and (optionally) control selection sets.
Integration tests (HTTP)
Integration tests exercise the full stack by sending GraphQL over HTTP to a real Spring Boot server.
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
class ResolverIntegrationTest {
@Autowired
private lateinit var restTemplate: TestRestTemplate
@LocalServerPort
private var port: Int = 0
private val objectMapper = ObjectMapper()
private fun executeGraphQLQuery(query: String): JsonNode {
val headers = HttpHeaders()
headers.contentType = MediaType.APPLICATION_JSON
val request = mapOf("query" to query)
val entity = HttpEntity(request, headers)
val response = restTemplate.postForEntity(
"http://localhost:$port/graphql",
entity,
String::class.java
)
return objectMapper.readTree(response.body)
}
@Nested
inner class QueryResolvers {
// Note: Individual node query tests are covered by StarWarsNodeResolversTest
@Test
fun `should resolve allCharacters list`() {
Integration tests are useful for:
- Verifying end-to-end behavior of the GraphQL API.
- Catching issues with dependency injection, application startup, and configuration.
- Testing real HTTP request/response cycles.
- Smoke tests before a release.
Use DefaultAbstractResolverTestBase (unit testing resolvers)
DefaultAbstractResolverTestBase is the fast path to exercise resolvers in isolation. It:
- Builds an
ExecutionContextand resolver‑specific context objects for you. - Lets you call resolver
resolve()/batchResolve()without HTTP, DI frameworks, or Spring. - Provides helpers to create
GlobalIDs and to run field, node, and batch resolvers.
Create a test class that extends the base and overrides getSchema():
@OptIn(ExperimentalCoroutinesApi::class)
class CharacterResolverUnitTests : DefaultAbstractResolverTestBase() {
override fun getSchema(): ViaductSchema =
SchemaFactory(DefaultCoroutineInterop)
.fromResources("com.example.starwars", Regex(".*\\.graphqls"))
@Test
fun `DisplayNameResolver returns name correctly`(): Unit =
runBlocking {
val resolver = CharacterDisplayNameResolver()
Resources are loaded from the given directory. The regex should match all your *.graphqls files.
Key helpers you can find
-
context: ExecutionContextUse it to build GRT objects via generated*.Builder(context)and to createGlobalIDs withcontext.globalIDFor(Type.Reflection, "internal-id"). -
runFieldResolver(resolver, objectValue, queryValue, arguments)Executes a field resolver and returns its value. -
runFieldBatchResolver(resolver, objectValues, queryValues)Executes a batch field resolver and returns a list ofFieldValue<T>. -
runNodeResolver(resolver, id)/runNodeBatchResolver(resolver, ids)Executes a node resolver (single or batch) for one or moreGlobalIDs.
Optionally, all runners accept selections and contextQueryValues when you need to customize
the selection set or seed the context with root query results.
Field resolver
Example: test a simple field resolver that formats a character’s display name.
@OptIn(ExperimentalCoroutinesApi::class)
class CharacterResolverUnitTests : DefaultAbstractResolverTestBase() {
override fun getSchema(): ViaductSchema =
SchemaFactory(DefaultCoroutineInterop)
.fromResources("com.example.starwars", Regex(".*\\.graphqls"))
@Test
fun `DisplayNameResolver returns name correctly`(): Unit =
runBlocking {
val resolver = CharacterDisplayNameResolver()
val result = runFieldResolver(
resolver = resolver,
objectValue = Character.Builder(context).name("Leia Organa").build(),
)
assertEquals("Leia Organa", result)
}
@Test
fun `DisplaySummaryResolver returns formatted name and birth year`(): Unit =
runBlocking {
val resolver = CharacterDisplaySummaryResolver()
val result = runFieldResolver(
resolver = resolver,
objectValue = Character.Builder(context).name("Darth Vader").birthYear("41.9BBY").build(),
)
assertEquals("Darth Vader (41.9BBY)", result)
}
Notes:
objectValuerepresents the object returned by the resolver’s required selection set.- If your resolver takes arguments, build them with the generated
..._Arguments.Builder(context).
Query resolver
Example: test with limit arguments.
@Test
fun `allCharacters respects limit and maps fields`(): Unit =
runBlocking {
val limit = 3
val resolver = AllCharactersQueryResolver()
val args = Query_AllCharacters_Arguments.Builder(context)
.limit(limit)
.build()
val result = runFieldResolver(
resolver = resolver,
arguments = args
)
assertNotNull(result)
assertEquals(limit, result!!.size)
val ref = CharacterRepository.findAll().first()
val first = result.first()!!
assertEquals(ref.name, first.getName())
assertEquals(ref.birthYear, first.getBirthYear())
}
Notes:
- For query resolvers, you usually omit
objectValueandqueryValue—the runner supplies defaults.
Node resolver
Example: resolve a Film node by GlobalID.
@Test
fun `film by id returns the correct Film using node resolver`(): Unit =
runBlocking {
val ref = FilmsRepository.getAllFilms().first()
val resolver = FilmNodeResolver()
// Create global ID for the film
val filmGlobalId = context.globalIDFor(Film.Reflection, ref.id)
// Use runNodeResolver to fetch film
val result = runNodeResolver(resolver, filmGlobalId)
assertNotNull(result)
assertEquals(ref.title, result.getTitle())
Tips:
- In this demo, internal ids are small strings (e.g.,
"4"). Your app will use your own id scheme.
Batch resolver
Example: batch node resolver returning multiple starships.
@Test
fun `CharacterBatchNodeResolver resolves multiple ids`() =
runBlocking {
val resolver = CharacterNodeResolver()
val ids = listOf("1", "2").map {
context.globalIDFor(Character.Reflection, it)
}
val results = runNodeBatchResolver(
resolver = resolver,
ids = ids
)
For batch field resolvers, use runFieldBatchResolver(resolver, objectValues = ..., queryValues = ...).
Header examples
This code shows how to test a field resolver that depends on header values.
private fun executeGraphQLQuery(
query: String,
headers: Map<String, String> = emptyMap()
): JsonNode {
val httpHeaders = HttpHeaders()
httpHeaders.contentType = MediaType.APPLICATION_JSON
// Add any custom headers (for scopes, etc.)
headers.forEach { (key, value) ->
httpHeaders.set(key, value)
}
val request = mapOf("query" to query)
val entity = HttpEntity(request, httpHeaders)
val response = restTemplate.postForEntity(
"http://localhost:$port/graphql",
entity,
String::class.java
)
return objectMapper.readTree(response.body)
Advanced: controlling selections and context queries (optional)
Most resolvers do not need this. When they do:
- Selections: pass a
SelectionSetto the runner if the resolver branches on selected subfields. - Context query values: seed the root query cache with specific results using
contextQueryValues. This is rare; prefer straightforward inputs when possible.
When to choose unit vs integration tests
-
Choose unit tests with
DefaultAbstractResolverTestBasefor:- Resolver behavior, argument handling, mapping logic, and edge cases.
- Fast feedback during development (no HTTP, DB, or Spring).
-
Choose integration tests for:
- Wiring across modules, startup behavior, and JSON marshalling.
- Request filters and application security.
In practice, keep most tests unit‑level, plus a few end‑to‑end HTTP tests as smoke tests.
Troubleshooting
-
IllegalArgumentExceptionabout query/object sizes When usingrunFieldBatchResolver, make sureobjectValues.size == queryValues.size. -
Missing schema types Ensure
getSchema()loads all*.graphqlsfiles. The regex".*\.graphqls"is a safe default. -
GlobalID building Use
context.globalIDFor(Type.Reflection, "<your-internal-id>"). Avoid constructing IDs by hand.
Full examples in the repo
See these demo tests in demoapps/starwars/src/test/kotlin/viaduct/demoapp/starwars:
- CharacterResolverUnitTests.kt
- FilmResolverUnitTests.kt
- QueryResolverUnitTests.kt
- StarshipResolversUnitTests.kt
- ResolverIntegrationTest.kt
- BatchResolverIntegrationTest.kt
They are small, focused, and make good templates for new tests.
Summary
- Use integration tests to verify the end‑to‑end server.
- Use
DefaultAbstractResolverTestBaseto test resolvers fast and in isolation. - Prefer small, targeted tests with clear inputs/outputs and minimal mocking.
Feedback
Was this page helpful?
Glad to hear it! Please tell us how we can improve.
Sorry to hear that. Please tell us how we can improve.