Skip to content

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:

USE_VIADUCT_SNAPSHOT_REPO=true ./gradlew test

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.

@Test
fun `DisplayNameResolver returns name correctly`(): Unit =
    runBlocking {
        val resolver = CharacterDisplayNameResolver()

        val result = runFieldResolver(resolver) {
            objectValue = Character.of(context) { name("Leia Organa") }
        }

        assertEquals("Leia Organa", result)
    }

View full file on GitHub

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)
    }

View full file on GitHub

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()) }
    }

View full file on GitHub

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())

View full file on GitHub

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.

@Test
fun `CharacterBatchNodeResolver resolves multiple ids`() =
    runBlocking {
        val results = runNodeBatchResolver(CharacterNodeResolver(characterRepository)) {
            ids = listOf("1", "2").map { globalIDFor(Character.Reflection, it) }
        }

        assertEquals(2, results.size)
    }

View full file on GitHub

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())
    }

View full file on GitHub

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)