Testing
Kotlin Star Wars resolver tests use ResolverTestBase. The Java demo uses JUnit 5, AssertJ, and Micronaut HTTP integration tests; focused Java unit tests can use viaduct.java.api.testing.ResolverTestBase through composition, with FieldInputs and NodeInputs instead of Kotlin’s DSL. The examples below show the demo’s existing HTTP tests; the Testing guide documents the Java fixture and its limitations. For setup, context construction, and the full API reference see the Testing guide.
Run ./gradlew test from your Kotlin demo checkout.
Run the existing integration suite from demoapps/starwars-java:
StarWarsJavaIntegrationTest starts a Micronaut server on an available port, injects an HTTP client, and sends requests through the real /graphql controller. This exercises generated types, resolver discovery, dependency injection, scopes, and request context together.
@MicronautTest
@Property(name = "micronaut.server.port", value = "-1")
final class StarWarsJavaIntegrationTest {
private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();
@Inject
@Client("/")
HttpClient client;
View full file on GitHub (lines 21-28)
The Java examples share this HTTP helper and response assertions:
private JsonNode execute(String query, Map<String, String> headers) throws Exception {
return execute(query, Map.of(), headers);
}
private JsonNode execute(String query, Map<String, Object> variables, Map<String, String> headers)
throws Exception {
return executeAt("/graphql", query, variables, headers);
}
private JsonNode executeAt(
String path, String query, Map<String, Object> variables, Map<String, String> headers)
throws Exception {
Map<String, Object> body = Map.of("query", query, "variables", variables);
MutableHttpRequest<Map<String, Object>> request =
HttpRequest.POST(path, body).contentType(MediaType.APPLICATION_JSON_TYPE);
for (Map.Entry<String, String> header : headers.entrySet()) {
request.header(header.getKey(), header.getValue());
}
String responseBody = client.toBlocking().retrieve(request);
return OBJECT_MAPPER.readTree(responseBody);
}
private void assertNoErrors(JsonNode response) {
assertThat(response.path("errors").isMissingNode() || response.path("errors").isNull())
.as(response.toPrettyString())
.isTrue();
}
private void assertHasErrors(JsonNode response) {
assertThat(response.path("errors").isArray()).as(response.toPrettyString()).isTrue();
}
View full file on GitHub (lines 432-462)
Field resolver¶
CharacterDisplayNameResolver resolves displayName from the Character GRT. The Kotlin test uses runFieldResolver and builds the input object with Character.of(context), setting only the fields the resolver reads.
The Java integration test selects computed fields and asserts the returned values. It exercises real resolver wiring rather than constructing a Kotlin resolver-test spec.
@Test
void resolvesConditionalSelectionsAndNamedFragments() throws Exception {
JsonNode response =
execute(
"""
query {
allCharacters(limit: 1) {
basicProfile: characterProfile(includeDetails: false)
detailedProfile: characterProfile(includeDetails: true)
appearanceDescription
formattedDescription(format: "detailed")
characterStats(minAge: 18, maxAge: 80)
richSummary
}
}
""",
Map.of());
assertNoErrors(response);
JsonNode character = response.at("/data/allCharacters/0");
assertThat(character.path("basicProfile").asText())
.isEqualTo("Character Profile: Luke Skywalker (basic info only)");
assertThat(character.path("detailedProfile").asText())
.contains("Born: 19BBY", "Height: 172cm", "Mass: 77.0kg");
assertThat(character.path("appearanceDescription").asText())
.isEqualTo("Luke Skywalker has blue eyes and blond hair");
assertThat(character.path("formattedDescription").asText())
.isEqualTo("Luke Skywalker (born 19BBY) - blue eyes, blond hair");
assertThat(character.path("characterStats").asText())
.contains("Age range: 18-80", "Species: Human");
assertThat(character.path("richSummary").asText()).contains("appears in 3 films");
}
View full file on GitHub (lines 242-273)
With arguments¶
CharacterFormattedDescriptionResolver takes a format argument that controls the output shape. The Kotlin test builds the arguments object with Character_FormattedDescription_Arguments.of(context) and passes it alongside the object value.
@Test
fun `FormattedDescriptionResolver returns full description for detailed format`(): Unit =
runBlocking {
val resolver = CharacterFormattedDescriptionResolver()
val result = runFieldResolver(resolver) {
arguments = Character_FormattedDescription_Arguments.of(context) { format("detailed") }
objectValue = Character.of(context) {
name("Padmé Amidala")
birthYear("46BBY")
eyeColor("brown")
hairColor("brown")
}
}
assertEquals("Padmé Amidala (born 46BBY) - brown eyes, brown hair", result)
}
Select the field with different arguments in one operation. The aliases distinguish the two results, including the includeDetails branches.
@Test
void resolvesConditionalSelectionsAndNamedFragments() throws Exception {
JsonNode response =
execute(
"""
query {
allCharacters(limit: 1) {
basicProfile: characterProfile(includeDetails: false)
detailedProfile: characterProfile(includeDetails: true)
appearanceDescription
formattedDescription(format: "detailed")
characterStats(minAge: 18, maxAge: 80)
richSummary
}
}
""",
Map.of());
assertNoErrors(response);
JsonNode character = response.at("/data/allCharacters/0");
assertThat(character.path("basicProfile").asText())
.isEqualTo("Character Profile: Luke Skywalker (basic info only)");
assertThat(character.path("detailedProfile").asText())
.contains("Born: 19BBY", "Height: 172cm", "Mass: 77.0kg");
assertThat(character.path("appearanceDescription").asText())
.isEqualTo("Luke Skywalker has blue eyes and blond hair");
assertThat(character.path("formattedDescription").asText())
.isEqualTo("Luke Skywalker (born 19BBY) - blue eyes, blond hair");
assertThat(character.path("characterStats").asText())
.contains("Age range: 18-80", "Species: Human");
assertThat(character.path("richSummary").asText()).contains("appears in 3 films");
}
View full file on GitHub (lines 242-273)
Field batch resolver¶
CharacterFilmCountResolver is a batching resolver (@resolver(isBatching: true)) that counts how many films each character appears in. The Kotlin test uses runFieldBatchResolver, passing a list of Character objects as objectValues. Call .get() on each result to extract the value.
@Test
fun `runFieldBatchResolver returns film count for each character in batch`(): Unit =
runBlocking {
val results = runFieldBatchResolver(CharacterFilmCountResolver(characterFilmsRepository)) {
objectValues = characterRepository.findAll().take(3).map { char ->
Character.of(context) { id(globalIDFor(Character.Reflection, char.id)) }
}
}
assertEquals(3, results.size)
results.forEach { fv -> assertEquals(3, fv.get()) }
}
The Java integration suite selects filmCount alongside relationships in a real request. To verify a batching optimization, additionally instrument the repository and assert the number of bulk calls; a correct response alone does not prove the batching behavior.
@Test
void resolvesFilmographyAndUniverseInOneApplication() throws Exception {
JsonNode response =
execute(
"""
query {
allCharacters(limit: 1) {
id
name
displaySummary
filmCount
homeworld { name }
species { name }
}
allFilms(limit: 1) {
id
title
summary
characterCountSummary
characters { name }
dataSource
}
allPlanets(limit: 1) { name }
allStarships(limit: 1) { name model }
allVehicles(limit: 1) { name model }
}
""",
Map.of());
assertNoErrors(response);
assertThat(response.at("/data/allCharacters/0/name").asText()).isEqualTo("Luke Skywalker");
assertThat(response.at("/data/allCharacters/0/homeworld/name").asText()).isEqualTo("Tatooine");
assertThat(response.at("/data/allFilms/0/title").asText()).isEqualTo("A New Hope");
assertThat(response.at("/data/allFilms/0/characters")).hasSize(5);
assertThat(response.at("/data/allFilms/0/dataSource").asText())
.isEqualTo("film-archive-service:1");
assertThat(response.at("/data/allPlanets/0/name").asText()).isEqualTo("Tatooine");
assertThat(response.at("/data/allStarships/0/name").asText()).isEqualTo("Millennium Falcon");
assertThat(response.at("/data/allVehicles/0/name").asText()).isEqualTo("Speeder bike");
}
View full file on GitHub (lines 30-69)
Node resolver¶
FilmNodeResolver fetches a Film by its GlobalID. The Kotlin test uses runNodeResolver and builds the ID with globalIDFor(Film.Reflection, internalId). The id property on the spec is required.
@Test
fun `film by id returns the correct Film using node resolver`(): Unit =
runBlocking {
val ref = filmsRepository.getAllFilms().first()
val result = runNodeResolver(FilmNodeResolver(filmsRepository)) {
id = globalIDFor(Film.Reflection, ref.id)
selections = filmSelections("openingCrawl")
}
assertNotNull(result)
assertEquals(ref.openingCrawl, result.getOpeningCrawlOrThrow())
Fetch IDs from a query first, then use them in node(id:). The Java test covers the batched character resolver and the single film resolver.
@Test
void resolvesCharacterAndFilmNodes() throws Exception {
JsonNode ids =
execute("query { allCharacters(limit: 1) { id } allFilms(limit: 1) { id } }", Map.of());
assertNoErrors(ids);
String characterId = ids.at("/data/allCharacters/0/id").asText();
String filmId = ids.at("/data/allFilms/0/id").asText();
JsonNode response =
execute(
"""
query {
character: node(id: "%s") {
... on Character { name birthYear eyeColor }
}
film: node(id: "%s") {
... on Film { title episodeID director }
}
}
"""
.formatted(characterId, filmId),
Map.of());
assertNoErrors(response);
assertThat(response.at("/data/character/name").asText()).isEqualTo("Luke Skywalker");
assertThat(response.at("/data/film/title").asText()).isEqualTo("A New Hope");
assertThat(response.at("/data/film/episodeID").asInt()).isEqualTo(4);
}
View full file on GitHub (lines 71-98)
Node batch resolver¶
CharacterNodeResolver supports batch resolution. The Kotlin test uses runNodeBatchResolver, passing a list of GlobalIDs as ids. Call .get() on each result to extract the resolved object.
The same operation selects two independent nodes, including the batched CharacterNodeResolver. Add several character IDs when testing map completeness; Java batch resolvers must return an entry for each original context.
@Test
void resolvesCharacterAndFilmNodes() throws Exception {
JsonNode ids =
execute("query { allCharacters(limit: 1) { id } allFilms(limit: 1) { id } }", Map.of());
assertNoErrors(ids);
String characterId = ids.at("/data/allCharacters/0/id").asText();
String filmId = ids.at("/data/allFilms/0/id").asText();
JsonNode response =
execute(
"""
query {
character: node(id: "%s") {
... on Character { name birthYear eyeColor }
}
film: node(id: "%s") {
... on Film { title episodeID director }
}
}
"""
.formatted(characterId, filmId),
Map.of());
assertNoErrors(response);
assertThat(response.at("/data/character/name").asText()).isEqualTo("Luke Skywalker");
assertThat(response.at("/data/film/title").asText()).isEqualTo("A New Hope");
assertThat(response.at("/data/film/episodeID").asInt()).isEqualTo(4);
}
View full file on GitHub (lines 71-98)
Mutation resolver¶
CreateCharacterMutation creates a new character. The Kotlin test uses runMutationFieldResolver, building the input and arguments objects with CreateCharacterInput.of(context) and Mutation_CreateCharacter_Arguments.of(context).
@Test
fun `runMutationFieldResolver CreateCharacter creates and returns new character`(): Unit =
runBlocking {
val resolver = CreateCharacterMutation(characterRepository, adminAccess)
val input = CreateCharacterInput.of(context) {
name("Ahsoka Tano")
birthYear("36BBY")
}
val args = Mutation_CreateCharacter_Arguments.of(context) { input(input) }
val result = runMutationFieldResolver(resolver) {
arguments = args
}
assertNotNull(result)
assertEquals("Ahsoka Tano", result!!.getNameOrThrow())
assertEquals("36BBY", result.getBirthYearOrThrow())
}
The Java test checks denied access, creates a character with the admin header, then executes a typed submutation and verifies the result. The suite also covers duplicate relationships, deletion cleanup, and invalid IDs.
@Test
void enforcesMutationSecurityAndExecutesTypedOperations() throws Exception {
JsonNode queryResponse = execute("query { characterSummaryByName(name: \"Luke\") }", Map.of());
assertNoErrors(queryResponse);
assertThat(queryResponse.at("/data/characterSummaryByName").asText())
.isEqualTo("Luke Skywalker (19BBY)");
String mutation = "mutation { createCharacter(input: { name: \"Ahsoka Tano\" }) { id name } }";
JsonNode denied = execute(mutation, Map.of());
assertThat(denied.path("errors").isArray()).isTrue();
JsonNode created = execute(mutation, Map.of("security-access", "admin"));
assertNoErrors(created);
assertThat(created.at("/data/createCharacter/name").asText()).isEqualTo("Ahsoka Tano");
String characterId = created.at("/data/createCharacter/id").asText();
JsonNode renamed =
execute(
"mutation { renameCharacterSummary(id: \"%s\", name: \"Ahsoka\") }"
.formatted(characterId),
Map.of("security-access", "admin"));
assertNoErrors(renamed);
assertThat(renamed.at("/data/renameCharacterSummary").asText())
.isEqualTo("Ahsoka (Unknown birth year)");
}
View full file on GitHub (lines 299-323)