Starter for building GraphQL applications with Spring GraphQL
—
Spring Boot GraphQL Starter provides comprehensive testing infrastructure for GraphQL applications, including test slices, mock configurations, and specialized testing utilities.
The @GraphQlTest annotation provides a focused test slice for GraphQL components.
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@BootstrapWith(GraphQlTestContextBootstrapper.class)
@ExtendWith(SpringExtension.class)
@OverrideAutoConfiguration(enabled = false)
@TypeExcludeFilters(GraphQlTypeExcludeFilter.class)
@AutoConfigureCache
@AutoConfigureGraphQl
@AutoConfigureGraphQlTester
@AutoConfigureJson
@ImportAutoConfiguration
public @interface GraphQlTest {
@AliasFor(annotation = ImportAutoConfiguration.class, attribute = "exclude")
Class<?>[] excludeAutoConfiguration() default {};
@AliasFor(annotation = ComponentScan.class, attribute = "useDefaultFilters")
boolean useDefaultFilters() default true;
String[] properties() default {};
}@GraphQlTest(controllers = BookController.class)
class BookControllerTest {
@Autowired
private GraphQlTester graphQlTester;
@MockBean
private BookService bookService;
@Test
void shouldReturnBooks() {
// Given
List<Book> books = List.of(
new Book("1", "Spring Boot in Action", "Craig Walls"),
new Book("2", "Spring GraphQL", "Josh Long")
);
when(bookService.findAll()).thenReturn(books);
// When & Then
graphQlTester
.documentName("get-books")
.execute()
.path("books")
.entityList(Book.class)
.hasSize(2)
.contains(books.get(0), books.get(1));
}
}Core testing utility for executing GraphQL operations and asserting results.
@AutoConfiguration
@ConditionalOnClass({ GraphQlTester.class, GraphQlSource.class })
@EnableConfigurationProperties(GraphQlTesterProperties.class)
public class GraphQlTesterAutoConfiguration {
@Bean
@ConditionalOnMissingBean
public GraphQlTester graphQlTester(GraphQlSource graphQlSource) {
return GraphQlTester.create(graphQlSource);
}
}public interface GraphQlTester {
// Execute with document name (from src/test/resources/graphql-test/)
RequestSpec documentName(String name);
// Execute with inline query
RequestSpec document(String document);
// Subscription testing
SubscriptionSpec subscription(String document);
interface RequestSpec {
RequestSpec variable(String name, Object value);
RequestSpec variables(Map<String, Object> variables);
RequestSpec operationName(String operationName);
RequestSpec headers(Consumer<HttpHeaders> headersConsumer);
ResponseSpec execute();
}
interface ResponseSpec {
ResponseSpec errors(Consumer<List<ResponseError>> errorsConsumer);
PathSpec path(String path);
EntitySpec<T> entity(Class<T> entityType);
}
}@GraphQlTest
class GraphQlControllerTests {
@Autowired
private GraphQlTester graphQlTester;
@Test
void shouldQueryBooks() {
graphQlTester
.document("{ books { id title author } }")
.execute()
.path("books")
.entityList(Book.class)
.hasSize(3)
.satisfies(books -> {
assertThat(books).extracting(Book::getTitle)
.contains("Spring Boot in Action");
});
}
@Test
void shouldQueryBookWithVariables() {
graphQlTester
.documentName("book-by-id")
.variable("id", "123")
.execute()
.path("book")
.entity(Book.class)
.satisfies(book -> {
assertThat(book.getId()).isEqualTo("123");
assertThat(book.getTitle()).isNotEmpty();
});
}
@Test
void shouldHandleErrors() {
graphQlTester
.document("{ nonExistentField }")
.execute()
.errors()
.expect(error -> error.getErrorType() == ErrorType.ValidationError);
}
}Testing GraphQL over HTTP with full web integration.
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@ImportAutoConfiguration
public @interface AutoConfigureHttpGraphQlTester {
String endpoint() default "/graphql";
}@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@AutoConfigureHttpGraphQlTester
class HttpGraphQlIntegrationTest {
@Autowired
private HttpGraphQlTester httpGraphQlTester;
@Test
void shouldExecuteQueryOverHttp() {
httpGraphQlTester
.document("{ books { id title } }")
.execute()
.path("books")
.entityList(Book.class)
.hasSize(2);
}
@Test
void shouldExecuteWithHeaders() {
httpGraphQlTester
.document("{ secureBooks { id title } }")
.headers(headers -> headers.setBearerAuth("token123"))
.execute()
.path("secureBooks")
.entityList(Book.class)
.hasSizeGreaterThan(0);
}
}Testing GraphQL subscriptions over WebSocket.
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@AutoConfigureWebSocketGraphQlTester
class WebSocketGraphQlTest {
@Autowired
private WebSocketGraphQlTester webSocketTester;
@Test
void shouldReceiveSubscriptionUpdates() {
Flux<Book> bookUpdates = webSocketTester
.document("subscription { bookUpdates { id title } }")
.executeSubscription()
.toFlux(Book.class);
StepVerifier.create(bookUpdates.take(3))
.expectNextCount(3)
.verifyComplete();
}
}@ConfigurationProperties("spring.graphql.test")
public class GraphQlTesterProperties {
private String endpoint = "/graphql";
private Duration timeout = Duration.ofSeconds(5);
private boolean prettyPrint = true;
// Getters and setters
}# application-test.yml
spring:
graphql:
schema:
locations: classpath:test-schemas/
inspection.enabled: false
graphiql:
enabled: true
test:
graphql:
timeout: 10s
pretty-print: true@TestConfiguration
public class GraphQlTestConfig {
@Bean
@Primary
public BookService mockBookService() {
BookService mockService = Mockito.mock(BookService.class);
when(mockService.findAll()).thenReturn(List.of(
new Book("1", "Test Book", "Test Author")
));
when(mockService.findById(anyString())).thenAnswer(invocation -> {
String id = invocation.getArgument(0);
return new Book(id, "Test Book " + id, "Test Author");
});
return mockService;
}
@Bean
@Primary
public DataFetcherExceptionResolver mockExceptionResolver() {
return (environment) -> {
if (environment.getException() instanceof TestException) {
return Mono.just(List.of(
GraphqlErrorBuilder.newError(environment)
.message("Test error")
.build()
));
}
return Mono.empty();
};
}
}# src/test/resources/graphql-test/test-schema.graphqls
type Query {
books: [Book!]!
book(id: ID!): Book
}
type Mutation {
addBook(input: BookInput!): Book!
}
type Book {
id: ID!
title: String!
author: String!
}
input BookInput {
title: String!
author: String!
}# src/test/resources/graphql-test/get-books.graphql
query GetBooks {
books {
id
title
author
}
}# src/test/resources/graphql-test/book-by-id.graphql
query BookById($id: ID!) {
book(id: $id) {
id
title
author
}
}@SpringBootTest
@TestPropertySource(properties = {
"spring.graphql.schema.locations=classpath:test-schemas/",
"spring.graphql.graphiql.enabled=false"
})
class GraphQlApplicationTests {
@Autowired
private TestRestTemplate restTemplate;
@Test
void shouldExecuteGraphQlQuery() {
String query = """
{
"query": "{ books { id title } }"
}
""";
ResponseEntity<String> response = restTemplate.postForEntity(
"/graphql",
new HttpEntity<>(query, createJsonHeaders()),
String.class
);
assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK);
assertThat(response.getBody()).contains("books");
}
private HttpHeaders createJsonHeaders() {
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
return headers;
}
}@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
class GraphQlLoadTest {
@Autowired
private HttpGraphQlTester httpTester;
@Test
void shouldHandleConcurrentRequests() {
int numberOfThreads = 10;
int requestsPerThread = 100;
CompletableFuture<?>[] futures = IntStream.range(0, numberOfThreads)
.mapToObj(i -> CompletableFuture.runAsync(() -> {
for (int j = 0; j < requestsPerThread; j++) {
httpTester
.document("{ books { id title } }")
.execute()
.path("books")
.entityList(Book.class);
}
}))
.toArray(CompletableFuture[]::new);
assertThatCode(() -> CompletableFuture.allOf(futures).get(30, TimeUnit.SECONDS))
.doesNotThrowAnyException();
}
}Install with Tessl CLI
npx tessl i tessl/maven-org-springframework-boot--spring-boot-starter-graphql