Skip to content

Testing

The DGS framework allows you to write lightweight tests that partially bootstrap the framework, just enough to run queries.

Example

Before writing tests, make sure that JUnit is enabled. If you created a project with Spring Initializr this configuration should already be there.

dependencies {
    testImplementation 'org.springframework.boot:spring-boot-starter-test'
    testImplementation 'com.netflix.graphql.dgs:graphql-dgs-spring-graphql-starter-test'
}

test {
    useJUnitPlatform()
}
tasks.withType<Test> {
    useJUnitPlatform()
}
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-test</artifactId>
    <scope>test</scope>
</dependency>

<dependency>
    <groupId>com.netflix.graphql.dgs</groupId>
    <artifactId>graphql-dgs-spring-graphql-starter-test</artifactId>
    <scope>test</scope>
</dependency>

Create a test class with the following contents to test the ShowsDatafetcher from the getting started example.

import com.netflix.graphql.dgs.DgsQueryExecutor;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;

import java.util.List;
import com.netflix.graphql.dgs.test.EnableDgsTest;

import static org.assertj.core.api.Assertions.assertThat;

@SpringBootTest(classes = {ShowsDatafetcher.class})
@EnableDgsTest
class ShowsDatafetcherTest {

    @Autowired
    DgsQueryExecutor dgsQueryExecutor;

    @Test
    void shows() {
        List<String> titles = dgsQueryExecutor.executeAndExtractJsonPath(
                " { shows { title releaseYear }}",
                "data.shows[*].title");

        assertThat(titles).contains("Ozark");
    }
}
import com.netflix.graphql.dgs.DgsQueryExecutor
import com.netflix.graphql.dgs.test.EnableDgsTest
import org.assertj.core.api.Assertions.assertThat
import org.junit.jupiter.api.Test
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.boot.test.context.SpringBootTest

@SpringBootTest(classes = [ShowsDataFetcher::class])
@EnableDgsTest
class ShowsDataFetcherTest {

    @Autowired
    lateinit var dgsQueryExecutor: DgsQueryExecutor

    @Test
    fun shows() {
        val titles : List<String> = dgsQueryExecutor.executeAndExtractJsonPath("""
            {
                shows {
                    title
                    releaseYear
                }
            }
        """.trimIndent(), "data.shows[*].title")

        assertThat(titles).contains("Ozark")
    }
}

The @SpringBootTest annotation makes this a Spring test. If you do not specify classes explicitly, Spring will start all components on the classpath. For a small application this is fine, but for applications with components that are "expensive" to start we can speed up the test by only adding the classes we need for the test. In this example that's just the ShowsDatafetcher. You also need components from the DGS framework itself, so that you can run a real GraphQL query in your test. Add the @EnableDgsTest test slice annotation. This annotation effectively adds a list of autoconfiguration classes that start as part of the test.

Testing data fetchers that use WebMVC annotations such as @RequestHeader

@EnableDgsTest is designed to not require a web stack, to keep tests as fast and simple as possible. In some scenarios you do need web functionality. In that case you should use the @EnableDgsMockMvcTest, which also sets up the components to use MockMvc.

To execute queries, inject DgsQueryExecutor in the test. This interface has several methods to execute a query and get back the result. It executes the exact same code as a query on the /graphql endpoint would, but you won’t have to deal with HTTP in your tests. The DgsQueryExecutor methods accept JSON paths, so that the methods can easily extract just the data from the response that you’re interested in. The DgsQueryExecutor also includes methods (e.g. executeAndExtractJsonPathAsObject) to deserialize the result to a Java class, which uses Jackson under the hood. The JSON paths are supported by the open source JsonPath library.

Write a few more tests, for example to verify the behavior with using the titleFilter of ShowsDatafetcher. You can run the tests from the IDE, or from Gradle/Maven, just like any JUnit test.

Building GraphQL Queries for Tests

In the examples shown previously, we handcrafted the query string. This is simple enough for queries that are small and straightforward. However, constructing longer query strings can be tedious, specially in Java without support for multi-line Strings. For this, we can use the GraphQLQueryRequest to build the graphql request in combination with the code generation plugin to generate the classes needed to use the request builder. This provides a convenient type-safe way to build your queries.

To set up code generation to generate the required classes to use for building your queries, follow the instructions here.

Now we can write a test that uses GraphQLQueryRequest to build the query and extract the response using GraphQLResponse.

@Test
public void showsWithQueryApi() {
    GraphQLQueryRequest graphQLQueryRequest = new GraphQLQueryRequest(
            new ShowsGraphQLQuery.Builder().titleFilter("Oz").build(),
            new ShowsProjectionRoot().title()
    );

    List<String> titles = dgsQueryExecutor.executeAndExtractJsonPath(graphQLQueryRequest.serialize(), "data.shows[*].title");
    assertThat(titles).containsExactly("Ozark");
}
@Test
fun showsWithQueryApi() {
    val graphQLQueryRequest = GraphQLQueryRequest(
        ShowsGraphQLQuery.Builder()
            .titleFilter("Oz")
            .build(),
        ShowsProjectionRoot().title())

    val titles = dgsQueryExecutor.executeAndExtractJsonPath<List<String>>(graphQLQueryRequest.serialize(), "data.shows[*].title")
    assertThat(titles).containsExactly("Ozark")
}

The GraphQLQueryRequest is available as part of the graphql-client module and is used to build the query string, and wrap the response respectively. You can also refer to the GraphQLClient JavaDoc for more details on the list of supported methods.

Mocking External Service Calls in Tests

It’s not uncommon for a data fetcher to talk to external systems such as a database or a gRPC service. If it does so within a test, this adds two problems:

  1. It adds latency; your tests are going to run slower when they make a lot of external calls.
  2. It adds flakiness: Did your code introduce a bug, or did something go wrong in the external system?

In many cases it’s better to mock these external services. Spring already has good support for doing so with the @Mockbean annotation, which you can leverage in your DGS tests.

Example

Let's update the Shows example to load shows from an external data source, instead of just returning a fixed list. For the sake of the example we'll just move the fixed list of shows to a new class that we'll annotate @Service. The data fetcher is updated to use the injected ShowsService.

public interface ShowsService {
    List<Show> shows();
}

@Service
public class ShowsServiceImpl implements ShowsService {
    @Override
    public List<Show> shows() {
        return List.of(
            new Show("Stranger Things", 2016),
            new Show("Ozark", 2017),
            new Show("The Crown", 2016),
            new Show("Dead to Me", 2019),
            new Show("Orange is the New Black", 2013)
        );
    }
}
interface ShowsService {
    fun shows(): List<ShowsDataFetcher.Show>
}

@Service
class BasicShowsService : ShowsService {
    override fun shows(): List<ShowsDataFetcher.Show> {
        return listOf(
            ShowsDataFetcher.Show("Stranger Things", 2016),
            ShowsDataFetcher.Show("Ozark", 2017),
            ShowsDataFetcher.Show("The Crown", 2016),
            ShowsDataFetcher.Show("Dead to Me", 2019),
            ShowsDataFetcher.Show("Orange is the New Black", 2013)
        )
    }
}

@DgsComponent
class ShowsDataFetcher {
    @Autowired
    lateinit var showsService: ShowsService

    @DgsData(parentType = "Query", field = "shows")
    fun shows(@InputArgument("titleFilter") titleFilter: String?): List<Show> {
        return if (titleFilter != null) {
            showsService.shows().filter { it.title.contains(titleFilter) }
        } else {
            showsService.shows()
        }
    }
}

For the sake of the example the shows are still in-memory, imagine that the service would actually call out to an external data store. Let's try to mock this service in the test!

@SpringBootTest(classes = {ShowsDataFetcher.class})
@EnableDgsTest
public class ShowsDataFetcherTests {

    @Autowired
    DgsQueryExecutor dgsQueryExecutor;

    @MockBean
    ShowsService showsService;

    @BeforeEach
    public void before() {
        Mockito.when(showsService.shows()).thenAnswer(invocation -> List.of(new Show("mock title", 2020)));
    }

    @Test
    public void showsWithQueryApi() {
        GraphQLQueryRequest graphQLQueryRequest = new GraphQLQueryRequest(
                new ShowsGraphQLQuery.Builder().build(),
                new ShowsProjectionRoot().title()
        );

        List<String> titles = dgsQueryExecutor.executeAndExtractJsonPath(graphQLQueryRequest.serialize(), "data.shows[*].title");
        assertThat(titles).containsExactly("mock title");
    }
}
@SpringBootTest(classes = [ShowsDataFetcher::class])
@EnableDgsTest
class ShowsDataFetcherTest {

    @Autowired
    lateinit var
    dgsQueryExecutor:DgsQueryExecutor

    @MockBean
    lateinit var
    showsService:ShowsService

    @BeforeEach

    fun before() {
        Mockito.`when`(showsService.shows()).thenAnswer {
            listOf(ShowsDataFetcher.Show("mock title", 2020))
        }
    }

    @Test
    fun shows() {
        val titles :List<String> =dgsQueryExecutor.executeAndExtractJsonPath("""
                    {
                        shows {
                            title
                            releaseYear
                        }
                    }
                """.trimIndent(), "data.shows[*].title")

        assertThat(titles).contains("mock title")
    }
}

Testing Exceptions

The tests you wrote so far are mostly happy paths. Failure scenarios are also easy to test. We use the mocked example from above to force an exception.

@Test
void showsWithException() {
Mockito.when(showsService.shows()).thenThrow(new RuntimeException("nothing to see here"));
    ExecutionResult result = dgsQueryExecutor.execute(" { shows { title releaseYear }}");
    assertThat(result.getErrors()).isNotEmpty();
    assertThat(result.getErrors().get(0).getMessage()).isEqualTo("java.lang.RuntimeException: nothing to see here");
}
@Test
fun showsWithException() {
    Mockito.`when`(showsService.shows()).thenThrow(RuntimeException("nothing to see here"))

    val result = dgsQueryExecutor.execute("""
        {
            shows {
                title
                releaseYear
            }
        }
    """.trimIndent())

    assertThat(result.errors).isNotEmpty
    assertThat(result.errors[0].message).isEqualTo("java.lang.RuntimeException: nothing to see here")
}

When an error happens while executing the query, the errors are wrapped in a QueryException. This allows you to easily inspect the error. The message of the QueryException is the concatenation of all the errors. The getErrors() method gives access to the individual errors for further inspection.

Testing with Spring MockMvc and HttpGraphQlTester

While testing queries without the web layer is the preferred approach for most tests, it is sometimes useful to include the web layer as well. For example, to test integration of filters, security and such. It's useful to have a least one such a test as a "smoke test" as part of your testing strategy. While for a smoketest you probably want to bootstrap all application components (by using @SpringBootTest without arguments), instead of test slices, you can use HttpGrahpQlTester in either scenario.

@SpringBootTest(classes = {ShowsDataFetcher.class})
@EnableDgsMockMvcTest //Enable DGS and MockMvc
@AutoConfigureHttpGraphQlTester //Enable HttpGraphQlTester
public class SmokeTestWithSpringGraphQL {
    @Autowired
    private HttpGraphQlTester graphQlTester;

    @Test
    void testShows() {
        @Language("GraphQL")
        var query = """
                query {
                    shows {
                        title
                    }
                }
                """;

        graphQlTester.document(query)
                .execute()
                .path("shows[*].title")
                .entityList(String.class)
                .containsExactly("The Last Dance", "Cheer", "GLOW");
    }
}

Testing with MockMvc directly

While it's easier to use HttpGraphQlTester as shown above, you can also write a test using MockMvc directly and crafting the GraphQl request by hand. You can easily send a GraphQL request using MockMvc by creating a wrapper object and using Jackson for JSON serialization.

@SpringBootTest
@AutoConfigureMockMvc
public class SmokeTestWithRequest {
    @Autowired
    MockMvc mockMvc;

    @Autowired
    ObjectMapper objectMapper;

    @Test
    public void showsSmokeTest() throws Exception {
        @Language("GraphQL")
        var query = """                
                {
                    shows {
                        title
                    }
                }
                """;

        mockMvc.perform(post("/graphql")
                        .secure(true)
                        .content(objectMapper.writeValueAsBytes(new GraphqlRequest(query)))
                        .contentType(MediaType.APPLICATION_JSON)
                        .accept(MediaType.APPLICATION_JSON))
                .andExpect(jsonPath("data.shows").isArray())
                .andExpect(jsonPath("data.shows[0].title").exists());
    }

    record GraphqlRequest(String query){}
}

Testing with a real Client

Testing with a real client is typically not needed, and you should avoid such test in most cases. Running a server on a real port can be problematic in CI. The following test does show how to set this up with the DGS Java GraphQL Client and the server starting on a random port. Note that this example starts the whole application instead of just starting individual components.

import com.netflix.graphql.dgs.client.GraphQLResponse;
import com.netflix.graphql.dgs.client.MonoGraphQLClient;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.web.server.LocalServerPort;
import org.springframework.web.reactive.function.client.WebClient;

import java.util.List;

import static org.junit.jupiter.api.Assertions.assertTrue;

@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
class ShowsDatafetcherTest {
final MonoGraphQLClient monoGraphQLClient;

    public ShowsDatafetcherTest(@LocalServerPort Integer port) {
        WebClient webClient = WebClient.create("http://localhost:" + port.toString() + "/graphql");
        this.monoGraphQLClient = MonoGraphQLClient.createWithWebClient(webClient);
    }

    @Test
    void shows() {
        String query = "{ shows { title releaseYear }}";

        // Read more about executeQuery() at https://netflix.github.io/dgs/advanced/java-client/
        GraphQLResponse response =
                monoGraphQLClient.reactiveExecuteQuery(query).block();

        List<?> titles = response.extractValueAsObject("shows[*].title", List.class);

        assertTrue(titles.contains("Ozark"));
    }
}