Introduction
GraphQL testing in Java involves testing both GraphQL servers and clients to ensure proper query execution, error handling, and performance. This guide covers comprehensive testing strategies using popular Java testing frameworks and tools.
Dependencies and Setup
1. Maven Dependencies
<properties>
<spring-boot.version>3.2.0</spring-boot.version>
<graphql-java.version>21.3</graphql-java.version>
<graphql-java-kickstart.version>15.0.0</graphql-java-kickstart.version>
<junit.version>5.10.1</junit.version>
<testcontainers.version>1.19.3</testcontainers.version>
</properties>
<dependencies>
<!-- Spring Boot GraphQL -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-graphql</artifactId>
<version>${spring-boot.version}</version>
</dependency>
<!-- Spring Boot Web -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<version>${spring-boot.version}</version>
</dependency>
<!-- GraphQL Java -->
<dependency>
<groupId>com.graphql-java</groupId>
<artifactId>graphql-java</artifactId>
<version>${graphql-java.version}</version>
</dependency>
<!-- GraphQL Java Tools -->
<dependency>
<groupId>com.graphql-java-kickstart</groupId>
<artifactId>graphql-java-tools</artifactId>
<version>${graphql-java-kickstart.version}</version>
</dependency>
<!-- GraphQL Test Utilities -->
<dependency>
<groupId>com.graphql-java-kickstart</groupId>
<artifactId>graphql-spring-boot-starter-test</artifactId>
<version>${graphql-java-kickstart.version}</version>
<scope>test</scope>
</dependency>
<!-- Testing -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<version>${spring-boot.version}</version>
<scope>test</scope>
</dependency>
<!-- TestContainers -->
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>junit-jupiter</artifactId>
<version>${testcontainers.version}</version>
<scope>test</scope>
</dependency>
<!-- HTTP Client for Testing -->
<dependency>
<groupId>com.squareup.okhttp3</groupId>
<artifactId>okhttp</artifactId>
<version>4.12.0</version>
<scope>test</scope>
</dependency>
<!-- JSON Assertions -->
<dependency>
<groupId>org.skyscreamer</groupId>
<artifactId>jsonassert</artifactId>
<version>1.5.1</version>
<scope>test</scope>
</dependency>
<!-- Awaitility for Async Testing -->
<dependency>
<groupId>org.awaitility</groupId>
<artifactId>awaitility</artifactId>
<version>4.2.0</version>
<scope>test</scope>
</dependency>
</dependencies>
GraphQL Server Implementation
2. GraphQL Schema Definition
# schema.graphqls
type Query {
users: [User!]!
user(id: ID!): User
posts(userId: ID, first: Int, after: String): PostConnection!
searchUsers(query: String!): [User!]!
}
type Mutation {
createUser(input: CreateUserInput!): User!
updateUser(id: ID!, input: UpdateUserInput!): User!
deleteUser(id: ID!): Boolean!
createPost(input: CreatePostInput!): Post!
}
type Subscription {
postCreated: Post!
userUpdated: User!
}
type User {
id: ID!
name: String!
email: String!
age: Int
posts: [Post!]!
createdAt: String!
updatedAt: String!
}
type Post {
id: ID!
title: String!
content: String!
author: User!
comments: [Comment!]!
published: Boolean!
createdAt: String!
}
type Comment {
id: ID!
content: String!
author: User!
post: Post!
createdAt: String!
}
type PostConnection {
edges: [PostEdge!]!
pageInfo: PageInfo!
}
type PostEdge {
node: Post!
cursor: String!
}
type PageInfo {
hasNextPage: Boolean!
hasPreviousPage: Boolean!
startCursor: String
endCursor: String
}
input CreateUserInput {
name: String!
email: String!
age: Int
}
input UpdateUserInput {
name: String
email: String
age: Int
}
input CreatePostInput {
title: String!
content: String!
authorId: ID!
published: Boolean = false
}
3. Data Models
package com.example.graphql.model;
import java.time.LocalDateTime;
import java.util.List;
public class User {
private String id;
private String name;
private String email;
private Integer age;
private List<Post> posts;
private LocalDateTime createdAt;
private LocalDateTime updatedAt;
// Constructors
public User() {}
public User(String id, String name, String email, Integer age) {
this.id = id;
this.name = name;
this.email = email;
this.age = age;
this.createdAt = LocalDateTime.now();
this.updatedAt = LocalDateTime.now();
}
// Getters and Setters
public String getId() { return id; }
public void setId(String id) { this.id = id; }
public String getName() { return name; }
public void setName(String name) { this.name = name; }
public String getEmail() { return email; }
public void setEmail(String email) { this.email = email; }
public Integer getAge() { return age; }
public void setAge(Integer age) { this.age = age; }
public List<Post> getPosts() { return posts; }
public void setPosts(List<Post> posts) { this.posts = posts; }
public LocalDateTime getCreatedAt() { return createdAt; }
public void setCreatedAt(LocalDateTime createdAt) { this.createdAt = createdAt; }
public LocalDateTime getUpdatedAt() { return updatedAt; }
public void setUpdatedAt(LocalDateTime updatedAt) { this.updatedAt = updatedAt; }
}
package com.example.graphql.model;
import java.time.LocalDateTime;
import java.util.List;
public class Post {
private String id;
private String title;
private String content;
private User author;
private List<Comment> comments;
private boolean published;
private LocalDateTime createdAt;
// Constructors
public Post() {}
public Post(String id, String title, String content, User author, boolean published) {
this.id = id;
this.title = title;
this.content = content;
this.author = author;
this.published = published;
this.createdAt = LocalDateTime.now();
}
// Getters and Setters
public String getId() { return id; }
public void setId(String id) { this.id = id; }
public String getTitle() { return title; }
public void setTitle(String title) { this.title = title; }
public String getContent() { return content; }
public void setContent(String content) { this.content = content; }
public User getAuthor() { return author; }
public void setAuthor(User author) { this.author = author; }
public List<Comment> getComments() { return comments; }
public void setComments(List<Comment> comments) { this.comments = comments; }
public boolean isPublished() { return published; }
public void setPublished(boolean published) { this.published = published; }
public LocalDateTime getCreatedAt() { return createdAt; }
public void setCreatedAt(LocalDateTime createdAt) { this.createdAt = createdAt; }
}
package com.example.graphql.model;
import java.time.LocalDateTime;
public class Comment {
private String id;
private String content;
private User author;
private Post post;
private LocalDateTime createdAt;
// Constructors, getters, and setters
public Comment() {}
public Comment(String id, String content, User author, Post post) {
this.id = id;
this.content = content;
this.author = author;
this.post = post;
this.createdAt = LocalDateTime.now();
}
public String getId() { return id; }
public void setId(String id) { this.id = id; }
public String getContent() { return content; }
public void setContent(String content) { this.content = content; }
public User getAuthor() { return author; }
public void setAuthor(User author) { this.author = author; }
public Post getPost() { return post; }
public void setPost(Post post) { this.post = post; }
public LocalDateTime getCreatedAt() { return createdAt; }
public void setCreatedAt(LocalDateTime createdAt) { this.createdAt = createdAt; }
}
package com.example.graphql.model;
import java.util.List;
public class PostConnection {
private List<PostEdge> edges;
private PageInfo pageInfo;
// Constructors, getters, and setters
public PostConnection() {}
public PostConnection(List<PostEdge> edges, PageInfo pageInfo) {
this.edges = edges;
this.pageInfo = pageInfo;
}
public List<PostEdge> getEdges() { return edges; }
public void setEdges(List<PostEdge> edges) { this.edges = edges; }
public PageInfo getPageInfo() { return pageInfo; }
public void setPageInfo(PageInfo pageInfo) { this.pageInfo = pageInfo; }
}
package com.example.graphql.model;
public class PostEdge {
private Post node;
private String cursor;
// Constructors, getters, and setters
public PostEdge() {}
public PostEdge(Post node, String cursor) {
this.node = node;
this.cursor = cursor;
}
public Post getNode() { return node; }
public void setNode(Post node) { this.node = node; }
public String getCursor() { return cursor; }
public void setCursor(String cursor) { this.cursor = cursor; }
}
package com.example.graphql.model;
public class PageInfo {
private boolean hasNextPage;
private boolean hasPreviousPage;
private String startCursor;
private String endCursor;
// Constructors, getters, and setters
public PageInfo() {}
public PageInfo(boolean hasNextPage, boolean hasPreviousPage, String startCursor, String endCursor) {
this.hasNextPage = hasNextPage;
this.hasPreviousPage = hasPreviousPage;
this.startCursor = startCursor;
this.endCursor = endCursor;
}
public boolean isHasNextPage() { return hasNextPage; }
public void setHasNextPage(boolean hasNextPage) { this.hasNextPage = hasNextPage; }
public boolean isHasPreviousPage() { return hasPreviousPage; }
public void setHasPreviousPage(boolean hasPreviousPage) { this.hasPreviousPage = hasPreviousPage; }
public String getStartCursor() { return startCursor; }
public void setStartCursor(String startCursor) { this.startCursor = startCursor; }
public String getEndCursor() { return endCursor; }
public void setEndCursor(String endCursor) { this.endCursor = endCursor; }
}
4. GraphQL Resolvers
package com.example.graphql.resolver;
import com.example.graphql.model.*;
import graphql.kickstart.tools.GraphQLMutationResolver;
import graphql.kickstart.tools.GraphQLQueryResolver;
import graphql.kickstart.tools.GraphQLSubscriptionResolver;
import org.reactivestreams.Publisher;
import org.springframework.stereotype.Component;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Sinks;
import java.time.Duration;
import java.util.List;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
import java.util.stream.Collectors;
@Component
public class UserResolver implements GraphQLQueryResolver, GraphQLMutationResolver, GraphQLSubscriptionResolver {
private final ConcurrentHashMap<String, User> users = new ConcurrentHashMap<>();
private final ConcurrentHashMap<String, Post> posts = new ConcurrentHashMap<>();
private final Sinks.Many<Post> postSink = Sinks.many().multicast().onBackpressureBuffer();
private final Sinks.Many<User> userSink = Sinks.many().multicast().onBackpressureBuffer();
public UserResolver() {
// Initialize with some test data
initializeTestData();
}
// Query Resolvers
public List<User> users() {
return List.copyOf(users.values());
}
public User user(String id) {
return users.get(id);
}
public PostConnection posts(String userId, Integer first, String after) {
List<Post> allPosts = posts.values().stream()
.filter(post -> userId == null || post.getAuthor().getId().equals(userId))
.filter(Post::isPublished)
.collect(Collectors.toList());
// Simple pagination implementation
int startIndex = after != null ? Integer.parseInt(after) : 0;
int endIndex = first != null ? Math.min(startIndex + first, allPosts.size()) : allPosts.size();
List<PostEdge> edges = allPosts.subList(startIndex, endIndex).stream()
.map(post -> new PostEdge(post, String.valueOf(endIndex)))
.collect(Collectors.toList());
PageInfo pageInfo = new PageInfo(
endIndex < allPosts.size(),
startIndex > 0,
String.valueOf(startIndex),
String.valueOf(endIndex)
);
return new PostConnection(edges, pageInfo);
}
public List<User> searchUsers(String query) {
return users.values().stream()
.filter(user -> user.getName().toLowerCase().contains(query.toLowerCase()) ||
user.getEmail().toLowerCase().contains(query.toLowerCase()))
.collect(Collectors.toList());
}
// Mutation Resolvers
public User createUser(CreateUserInput input) {
String id = UUID.randomUUID().toString();
User user = new User(id, input.getName(), input.getEmail(), input.getAge());
users.put(id, user);
return user;
}
public User updateUser(String id, UpdateUserInput input) {
User user = users.get(id);
if (user == null) {
throw new RuntimeException("User not found: " + id);
}
if (input.getName() != null) {
user.setName(input.getName());
}
if (input.getEmail() != null) {
user.setEmail(input.getEmail());
}
if (input.getAge() != null) {
user.setAge(input.getAge());
}
user.setUpdatedAt(java.time.LocalDateTime.now());
// Notify subscribers
userSink.tryEmitNext(user);
return user;
}
public Boolean deleteUser(String id) {
return users.remove(id) != null;
}
public Post createPost(CreatePostInput input) {
String id = UUID.randomUUID().toString();
User author = users.get(input.getAuthorId());
if (author == null) {
throw new RuntimeException("Author not found: " + input.getAuthorId());
}
Post post = new Post(id, input.getTitle(), input.getContent(), author, input.isPublished());
posts.put(id, post);
// Notify subscribers
postSink.tryEmitNext(post);
return post;
}
// Subscription Resolvers
public Publisher<Post> postCreated() {
return postSink.asFlux();
}
public Publisher<User> userUpdated() {
return userSink.asFlux();
}
private void initializeTestData() {
User user1 = new User("1", "John Doe", "[email protected]", 30);
User user2 = new User("2", "Jane Smith", "[email protected]", 25);
users.put(user1.getId(), user1);
users.put(user2.getId(), user2);
Post post1 = new Post("1", "First Post", "Content of first post", user1, true);
Post post2 = new Post("2", "Second Post", "Content of second post", user2, true);
posts.put(post1.getId(), post1);
posts.put(post2.getId(), post2);
}
// Input Types
public static class CreateUserInput {
private String name;
private String email;
private Integer age;
// Getters and Setters
public String getName() { return name; }
public void setName(String name) { this.name = name; }
public String getEmail() { return email; }
public void setEmail(String email) { this.email = email; }
public Integer getAge() { return age; }
public void setAge(Integer age) { this.age = age; }
}
public static class UpdateUserInput {
private String name;
private String email;
private Integer age;
// Getters and Setters
public String getName() { return name; }
public void setName(String name) { this.name = name; }
public String getEmail() { return email; }
public void setEmail(String email) { this.email = email; }
public Integer getAge() { return age; }
public void setAge(Integer age) { this.age = age; }
}
public static class CreatePostInput {
private String title;
private String content;
private String authorId;
private boolean published = false;
// Getters and Setters
public String getTitle() { return title; }
public void setTitle(String title) { this.title = title; }
public String getContent() { return content; }
public void setContent(String content) { this.content = content; }
public String getAuthorId() { return authorId; }
public void setAuthorId(String authorId) { this.authorId = authorId; }
public boolean isPublished() { return published; }
public void setPublished(boolean published) { this.published = published; }
}
}
Testing Framework
5. Base Test Configuration
package com.example.graphql.test.config;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.web.server.LocalServerPort;
import org.springframework.test.context.DynamicPropertyRegistry;
import org.springframework.test.context.DynamicPropertySource;
import org.testcontainers.containers.GenericContainer;
import org.testcontainers.junit.jupiter.Testcontainers;
@Testcontainers
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
public abstract class BaseGraphQLTest {
@LocalServerPort
protected int port;
protected String getGraphQLEndpoint() {
return "http://localhost:" + port + "/graphql";
}
// TestContainers setup for external dependencies
static GenericContainer<?> redis = new GenericContainer<>("redis:7-alpine")
.withExposedPorts(6379);
@DynamicPropertySource
static void configureProperties(DynamicPropertyRegistry registry) {
redis.start();
registry.add("spring.redis.host", redis::getHost);
registry.add("spring.redis.port", redis::getFirstMappedPort);
}
}
6. GraphQL Test Client
package com.example.graphql.test.client;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import okhttp3.*;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
import java.io.IOException;
import java.util.Map;
import java.util.concurrent.TimeUnit;
@Component
public class GraphQLTestClient {
private static final Logger log = LoggerFactory.getLogger(GraphQLTestClient.class);
private final OkHttpClient httpClient;
private final ObjectMapper objectMapper;
private final String graphqlEndpoint;
private static final MediaType JSON = MediaType.parse("application/json; charset=utf-8");
public GraphQLTestClient(String graphqlEndpoint) {
this.graphqlEndpoint = graphqlEndpoint;
this.objectMapper = new ObjectMapper();
this.httpClient = new OkHttpClient.Builder()
.connectTimeout(10, TimeUnit.SECONDS)
.readTimeout(30, TimeUnit.SECONDS)
.writeTimeout(10, TimeUnit.SECONDS)
.build();
}
public GraphQLResponse executeQuery(String query) throws IOException {
return executeQuery(query, null, null);
}
public GraphQLResponse executeQuery(String query, Map<String, Object> variables) throws IOException {
return executeQuery(query, variables, null);
}
public GraphQLResponse executeQuery(String query, Map<String, Object> variables,
Map<String, String> headers) throws IOException {
GraphQLRequest request = new GraphQLRequest(query, variables);
String requestBody = objectMapper.writeValueAsString(request);
Request.Builder requestBuilder = new Request.Builder()
.url(graphqlEndpoint)
.post(RequestBody.create(requestBody, JSON));
if (headers != null) {
headers.forEach(requestBuilder::addHeader);
}
Request httpRequest = requestBuilder.build();
try (Response response = httpClient.newCall(httpRequest).execute()) {
if (response.body() != null) {
String responseBody = response.body().string();
log.debug("GraphQL Response: {}", responseBody);
return new GraphQLResponse(responseBody, response.code(), objectMapper);
} else {
throw new IOException("Empty response body");
}
}
}
public GraphQLResponse executeMutation(String mutation, Map<String, Object> variables) throws IOException {
return executeQuery(mutation, variables);
}
public static class GraphQLRequest {
private String query;
private Map<String, Object> variables;
private String operationName;
public GraphQLRequest(String query, Map<String, Object> variables) {
this.query = query;
this.variables = variables;
}
// Getters and Setters
public String getQuery() { return query; }
public void setQuery(String query) { this.query = query; }
public Map<String, Object> getVariables() { return variables; }
public void setVariables(Map<String, Object> variables) { this.variables = variables; }
public String getOperationName() { return operationName; }
public void setOperationName(String operationName) { this.operationName = operationName; }
}
public static class GraphQLResponse {
private final String rawResponse;
private final int statusCode;
private final JsonNode jsonNode;
private final ObjectMapper objectMapper;
public GraphQLResponse(String rawResponse, int statusCode, ObjectMapper objectMapper) throws IOException {
this.rawResponse = rawResponse;
this.statusCode = statusCode;
this.objectMapper = objectMapper;
this.jsonNode = objectMapper.readTree(rawResponse);
}
public boolean isSuccessful() {
return statusCode >= 200 && statusCode < 300 && !hasErrors();
}
public boolean hasErrors() {
return jsonNode.has("errors");
}
public JsonNode getData() {
return jsonNode.get("data");
}
public JsonNode getErrors() {
return jsonNode.get("errors");
}
public String getRawResponse() {
return rawResponse;
}
public int getStatusCode() {
return statusCode;
}
public <T> T getDataAs(Class<T> type) throws IOException {
return objectMapper.treeToValue(getData(), type);
}
public String getFirstErrorMessage() {
if (hasErrors()) {
JsonNode firstError = getErrors().get(0);
return firstError.get("message").asText();
}
return null;
}
}
}
Comprehensive Test Suites
7. Query Testing
package com.example.graphql.test.query;
import com.example.graphql.test.client.GraphQLTestClient;
import com.example.graphql.test.config.BaseGraphQLTest;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import java.io.IOException;
import java.util.Map;
import static org.assertj.core.api.Assertions.assertThat;
class QueryTest extends BaseGraphQLTest {
@Autowired
private GraphQLTestClient graphQLTestClient;
@BeforeEach
void setUp() {
// Reset test data or set up initial state
}
@Test
void testGetAllUsers() throws IOException {
String query = """
query {
users {
id
name
email
age
}
}
""";
GraphQLTestClient.GraphQLResponse response = graphQLTestClient.executeQuery(query);
assertThat(response.isSuccessful()).isTrue();
assertThat(response.hasErrors()).isFalse();
assertThat(response.getData().get("users")).isNotNull();
assertThat(response.getData().get("users").isArray()).isTrue();
}
@Test
void testGetUserById() throws IOException {
String query = """
query GetUser($id: ID!) {
user(id: $id) {
id
name
email
posts {
id
title
}
}
}
""";
Map<String, Object> variables = Map.of("id", "1");
GraphQLTestClient.GraphQLResponse response = graphQLTestClient.executeQuery(query, variables);
assertThat(response.isSuccessful()).isTrue();
assertThat(response.getData().get("user").get("id").asText()).isEqualTo("1");
assertThat(response.getData().get("user").get("name").asText()).isEqualTo("John Doe");
}
@Test
void testGetUserNotFound() throws IOException {
String query = """
query {
user(id: "nonexistent") {
id
name
}
}
""";
GraphQLTestClient.GraphQLResponse response = graphQLTestClient.executeQuery(query);
assertThat(response.isSuccessful()).isTrue();
assertThat(response.getData().get("user")).isNull();
}
@Test
void testPostsPagination() throws IOException {
String query = """
query GetPosts($first: Int, $after: String) {
posts(first: $first, after: $after) {
edges {
node {
id
title
author {
name
}
}
cursor
}
pageInfo {
hasNextPage
hasPreviousPage
startCursor
endCursor
}
}
}
""";
Map<String, Object> variables = Map.of("first", 1, "after", "0");
GraphQLTestClient.GraphQLResponse response = graphQLTestClient.executeQuery(query, variables);
assertThat(response.isSuccessful()).isTrue();
assertThat(response.getData().get("posts").get("edges")).isNotNull();
assertThat(response.getData().get("posts").get("pageInfo").get("hasNextPage").asBoolean()).isTrue();
}
@Test
void testSearchUsers() throws IOException {
String query = """
query SearchUsers($query: String!) {
searchUsers(query: $query) {
id
name
email
}
}
""";
Map<String, Object> variables = Map.of("query", "john");
GraphQLTestClient.GraphQLResponse response = graphQLTestClient.executeQuery(query, variables);
assertThat(response.isSuccessful()).isTrue();
assertThat(response.getData().get("searchUsers").isArray()).isTrue();
assertThat(response.getData().get("searchUsers").get(0).get("name").asText()).containsIgnoringCase("john");
}
}
8. Mutation Testing
package com.example.graphql.test.mutation;
import com.example.graphql.test.client.GraphQLTestClient;
import com.example.graphql.test.config.BaseGraphQLTest;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import java.io.IOException;
import java.util.Map;
import static org.assertj.core.api.Assertions.assertThat;
class MutationTest extends BaseGraphQLTest {
@Autowired
private GraphQLTestClient graphQLTestClient;
@Test
void testCreateUser() throws IOException {
String mutation = """
mutation CreateUser($input: CreateUserInput!) {
createUser(input: $input) {
id
name
email
age
createdAt
}
}
""";
Map<String, Object> variables = Map.of(
"input", Map.of(
"name", "Test User",
"email", "[email protected]",
"age", 28
)
);
GraphQLTestClient.GraphQLResponse response = graphQLTestClient.executeMutation(mutation, variables);
assertThat(response.isSuccessful()).isTrue();
assertThat(response.getData().get("createUser").get("name").asText()).isEqualTo("Test User");
assertThat(response.getData().get("createUser").get("email").asText()).isEqualTo("[email protected]");
assertThat(response.getData().get("createUser").get("id")).isNotNull();
}
@Test
void testUpdateUser() throws IOException {
String mutation = """
mutation UpdateUser($id: ID!, $input: UpdateUserInput!) {
updateUser(id: $id, input: $input) {
id
name
email
age
updatedAt
}
}
""";
Map<String, Object> variables = Map.of(
"id", "1",
"input", Map.of(
"name", "Updated Name",
"age", 35
)
);
GraphQLTestClient.GraphQLResponse response = graphQLTestClient.executeMutation(mutation, variables);
assertThat(response.isSuccessful()).isTrue();
assertThat(response.getData().get("updateUser").get("name").asText()).isEqualTo("Updated Name");
assertThat(response.getData().get("updateUser").get("age").asInt()).isEqualTo(35);
}
@Test
void testUpdateUserNotFound() throws IOException {
String mutation = """
mutation UpdateUser($id: ID!, $input: UpdateUserInput!) {
updateUser(id: $id, input: $input) {
id
name
}
}
""";
Map<String, Object> variables = Map.of(
"id", "nonexistent",
"input", Map.of("name", "New Name")
);
GraphQLTestClient.GraphQLResponse response = graphQLTestClient.executeMutation(mutation, variables);
assertThat(response.hasErrors()).isTrue();
assertThat(response.getFirstErrorMessage()).contains("User not found");
}
@Test
void testDeleteUser() throws IOException {
String mutation = """
mutation DeleteUser($id: ID!) {
deleteUser(id: $id)
}
""";
Map<String, Object> variables = Map.of("id", "2");
GraphQLTestClient.GraphQLResponse response = graphQLTestClient.executeMutation(mutation, variables);
assertThat(response.isSuccessful()).isTrue();
assertThat(response.getData().get("deleteUser").asBoolean()).isTrue();
}
@Test
void testCreatePost() throws IOException {
String mutation = """
mutation CreatePost($input: CreatePostInput!) {
createPost(input: $input) {
id
title
content
published
author {
id
name
}
}
}
""";
Map<String, Object> variables = Map.of(
"input", Map.of(
"title", "New Post",
"content", "This is a test post",
"authorId", "1",
"published", true
)
);
GraphQLTestClient.GraphQLResponse response = graphQLTestClient.executeMutation(mutation, variables);
assertThat(response.isSuccessful()).isTrue();
assertThat(response.getData().get("createPost").get("title").asText()).isEqualTo("New Post");
assertThat(response.getData().get("createPost").get("author").get("id").asText()).isEqualTo("1");
}
}
9. Error Handling Testing
package com.example.graphql.test.error;
import com.example.graphql.test.client.GraphQLTestClient;
import com.example.graphql.test.config.BaseGraphQLTest;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import java.io.IOException;
import java.util.Map;
import static org.assertj.core.api.Assertions.assertThat;
class ErrorHandlingTest extends BaseGraphQLTest {
@Autowired
private GraphQLTestClient graphQLTestClient;
@Test
void testInvalidQuerySyntax() throws IOException {
String invalidQuery = """
query {
users {
id
name
invalidField
}
}
""";
GraphQLTestClient.GraphQLResponse response = graphQLTestClient.executeQuery(invalidQuery);
assertThat(response.hasErrors()).isTrue();
assertThat(response.getFirstErrorMessage()).contains("invalidField");
}
@Test
void testValidationErrors() throws IOException {
String mutation = """
mutation CreateUser($input: CreateUserInput!) {
createUser(input: $input) {
id
name
}
}
""";
// Missing required fields
Map<String, Object> variables = Map.of("input", Map.of());
GraphQLTestClient.GraphQLResponse response = graphQLTestClient.executeMutation(mutation, variables);
assertThat(response.hasErrors()).isTrue();
assertThat(response.getFirstErrorMessage()).contains("Validation error");
}
@Test
void testFieldLevelErrors() throws IOException {
String query = """
query {
users {
id
name
posts {
id
title
invalidNestedField
}
}
}
""";
GraphQLTestClient.GraphQLResponse response = graphQLTestClient.executeQuery(query);
assertThat(response.hasErrors()).isTrue();
// Should still get partial data
assertThat(response.getData().get("users")).isNotNull();
}
@Test
void testAuthenticationErrors() throws IOException {
String query = """
query {
users {
id
name
}
}
""";
Map<String, String> headers = Map.of("Authorization", "Bearer invalid-token");
GraphQLTestClient.GraphQLResponse response = graphQLTestClient.executeQuery(query, null, headers);
// This depends on your authentication setup
// assertThat(response.getStatusCode()).isEqualTo(401);
}
}
10. Performance and Load Testing
package com.example.graphql.test.performance;
import com.example.graphql.test.client.GraphQLTestClient;
import com.example.graphql.test.config.BaseGraphQLTest;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import static org.assertj.core.api.Assertions.assertThat;
import static org.awaitility.Awaitility.await;
class PerformanceTest extends BaseGraphQLTest {
@Autowired
private GraphQLTestClient graphQLTestClient;
@Test
void testConcurrentQueries() throws Exception {
int concurrentRequests = 10;
ExecutorService executor = Executors.newFixedThreadPool(concurrentRequests);
List<CompletableFuture<GraphQLTestClient.GraphQLResponse>> futures = new ArrayList<>();
AtomicInteger successCount = new AtomicInteger(0);
String query = """
query {
users {
id
name
email
}
}
""";
for (int i = 0; i < concurrentRequests; i++) {
CompletableFuture<GraphQLTestClient.GraphQLResponse> future = CompletableFuture.supplyAsync(() -> {
try {
GraphQLTestClient.GraphQLResponse response = graphQLTestClient.executeQuery(query);
if (response.isSuccessful()) {
successCount.incrementAndGet();
}
return response;
} catch (IOException e) {
throw new RuntimeException(e);
}
}, executor);
futures.add(future);
}
CompletableFuture<Void> allFutures = CompletableFuture.allOf(
futures.toArray(new CompletableFuture[0])
);
allFutures.get(30, TimeUnit.SECONDS);
assertThat(successCount.get()).isEqualTo(concurrentRequests);
executor.shutdown();
}
@Test
void testQueryPerformance() throws IOException {
String complexQuery = """
query {
users {
id
name
email
age
posts {
id
title
content
author {
name
email
}
comments {
id
content
author {
name
}
}
}
}
}
""";
long startTime = System.currentTimeMillis();
GraphQLTestClient.GraphQLResponse response = graphQLTestClient.executeQuery(complexQuery);
long endTime = System.currentTimeMillis();
long responseTime = endTime - startTime;
assertThat(response.isSuccessful()).isTrue();
assertThat(responseTime).isLessThan(1000); // Should respond within 1 second
}
@Test
void testNPlusOneQueryDetection() throws IOException {
// This query could cause N+1 if not properly optimized
String query = """
query {
users {
id
name
posts {
id
title
author {
name
posts {
id
title
}
}
}
}
}
""";
GraphQLTestClient.GraphQLResponse response = graphQLTestClient.executeQuery(query);
assertThat(response.isSuccessful()).isTrue();
// In a real scenario, you'd monitor database queries here
}
}
11. Subscription Testing
package com.example.graphql.test.subscription;
import com.example.graphql.test.client.GraphQLTestClient;
import com.example.graphql.test.config.BaseGraphQLTest;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.web.client.TestRestTemplate;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.ResponseEntity;
import java.io.IOException;
import java.util.Map;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicReference;
import static org.assertj.core.api.Assertions.assertThat;
import static org.awaitility.Awaitility.await;
class SubscriptionTest extends BaseGraphQLTest {
@Autowired
private GraphQLTestClient graphQLTestClient;
@Autowired
private TestRestTemplate restTemplate;
@Test
void testPostCreatedSubscription() throws Exception {
// This is a simplified test - real WebSocket testing would be more complex
String subscriptionQuery = """
subscription {
postCreated {
id
title
author {
name
}
}
}
""";
// In a real implementation, you'd use WebSocket client to test subscriptions
// For now, we'll test that the mutation triggers the subscription logic
String mutation = """
mutation CreatePost($input: CreatePostInput!) {
createPost(input: $input) {
id
title
}
}
""";
Map<String, Object> variables = Map.of(
"input", Map.of(
"title", "Subscription Test Post",
"content", "Testing subscription",
"authorId", "1",
"published", true
)
);
GraphQLTestClient.GraphQLResponse response = graphQLTestClient.executeMutation(mutation, variables);
assertThat(response.isSuccessful()).isTrue();
assertThat(response.getData().get("createPost").get("title").asText())
.isEqualTo("Subscription Test Post");
}
}
12. Integration Testing with TestContainers
package com.example.graphql.test.integration;
import com.example.graphql.test.client.GraphQLTestClient;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.web.server.LocalServerPort;
import org.springframework.test.context.DynamicPropertyRegistry;
import org.springframework.test.context.DynamicPropertySource;
import org.testcontainers.containers.PostgreSQLContainer;
import org.testcontainers.junit.jupiter.Container;
import org.testcontainers.junit.jupiter.Testcontainers;
import java.io.IOException;
import static org.assertj.core.api.Assertions.assertThat;
@Testcontainers
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
class DatabaseIntegrationTest {
@LocalServerPort
private int port;
@Container
static PostgreSQLContainer<?> postgres = new PostgreSQLContainer<>("postgres:15-alpine")
.withDatabaseName("testdb")
.withUsername("test")
.withPassword("test");
@DynamicPropertySource
static void configureProperties(DynamicPropertyRegistry registry) {
registry.add("spring.datasource.url", postgres::getJdbcUrl);
registry.add("spring.datasource.username", postgres::getUsername);
registry.add("spring.datasource.password", postgres::getPassword);
}
@Test
void testGraphQLWithRealDatabase() throws IOException {
GraphQLTestClient client = new GraphQLTestClient("http://localhost:" + port + "/graphql");
String query = """
query {
users {
id
name
email
}
}
""";
GraphQLTestClient.GraphQLResponse response = client.executeQuery(query);
assertThat(response.isSuccessful()).isTrue();
assertThat(response.hasErrors()).isFalse();
}
}
Test Utilities
13. GraphQL Test Utilities
package com.example.graphql.test.util;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.skyscreamer.jsonassert.JSONAssert;
import org.skyscreamer.jsonassert.JSONCompareMode;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
public class GraphQLTestUtils {
private static final ObjectMapper objectMapper = new ObjectMapper();
public static String createQuery(String operation, String fields) {
return String.format("query { %s { %s } }", operation, fields);
}
public static String createMutation(String operation, String input, String fields) {
return String.format("mutation { %s(input: %s) { %s } }", operation, input, fields);
}
public static Map<String, Object> createVariables(Map<String, Object> variables) {
return variables;
}
public static void assertNoErrors(JsonNode response) {
assertThat(response.has("errors")).isFalse();
}
public static void assertHasErrors(JsonNode response) {
assertThat(response.has("errors")).isTrue();
}
public static void assertResponseMatches(String actualJson, String expectedJson) throws Exception {
JSONAssert.assertEquals(expectedJson, actualJson, JSONCompareMode.LENIENT);
}
public static List<String> extractFieldValues(JsonNode data, String fieldPath) {
String[] path = fieldPath.split("\\.");
JsonNode currentNode = data;
for (String segment : path) {
if (currentNode.isArray()) {
// Handle arrays
List<String> results = new ArrayList<>();
for (JsonNode node : currentNode) {
results.addAll(extractFieldValues(node, String.join(".",
Arrays.copyOfRange(path, 1, path.length))));
}
return results;
} else {
currentNode = currentNode.get(segment);
}
}
if (currentNode != null && currentNode.isArray()) {
List<String> results = new ArrayList<>();
for (JsonNode node : currentNode) {
results.add(node.asText());
}
return results;
}
return List.of(currentNode != null ? currentNode.asText() : "");
}
public static class QueryBuilder {
private final StringBuilder query = new StringBuilder();
public QueryBuilder query(String operation) {
query.append("query { ").append(operation);
return this;
}
public QueryBuilder mutation(String operation) {
query.append("mutation { ").append(operation);
return this;
}
public QueryBuilder withField(String field) {
query.append(" ").append(field);
return this;
}
public QueryBuilder withFields(String... fields) {
for (String field : fields) {
query.append(" ").append(field);
}
return this;
}
public String build() {
return query.append(" } }").toString();
}
}
}
Configuration
14. Test Configuration
# application-test.yml spring: graphql: graphiql: enabled: true path: /graphiql schema: printer: enabled: true sql: init: platform: postgresql logging: level: com.example.graphql: DEBUG org.springframework.graphql: DEBUG graphql: servlet: enabled: true cors-enabled: true
Best Practices
- Test All Operations: Cover queries, mutations, and subscriptions
- Test Error Scenarios: Include validation errors, authentication errors, and business logic errors
- Performance Testing: Test query performance and N+1 query detection
- Security Testing: Test authentication and authorization
- Integration Testing: Test with real databases and external services
- Use Test Containers: For realistic integration testing
- Mock External Services: When testing in isolation
- Test Data Loading: Use consistent test data setup
- Concurrent Testing: Test for thread safety and performance under load
- Schema Testing: Validate GraphQL schema compliance
Conclusion
This comprehensive GraphQL testing framework provides:
- Query Testing: Test all GraphQL query operations
- Mutation Testing: Test create, update, and delete operations
- Error Handling: Test various error scenarios
- Performance Testing: Test query performance and load handling
- Subscription Testing: Test real-time updates
- Integration Testing: Test with real databases using TestContainers
- Test Utilities: Helper methods for common testing patterns
The framework enables robust testing of GraphQL APIs in Java applications, ensuring reliability, performance, and correctness of your GraphQL implementation.
Pyroscope Profiling in Java
Explains how to use Pyroscope for continuous profiling in Java applications, helping developers analyze CPU and memory usage patterns to improve performance and identify bottlenecks.
https://macronepal.com/blog/pyroscope-profiling-in-java/
OpenTelemetry Metrics in Java: Comprehensive Guide
Provides a complete guide to collecting and exporting metrics in Java using OpenTelemetry, including counters, histograms, gauges, and integration with monitoring tools. (MACRO NEPAL)
https://macronepal.com/blog/opentelemetry-metrics-in-java-comprehensive-guide/
OTLP Exporter in Java: Complete Guide for OpenTelemetry
Explains how to configure OTLP exporters in Java to send telemetry data such as traces, metrics, and logs to monitoring systems using HTTP or gRPC protocols. (MACRO NEPAL)
https://macronepal.com/blog/otlp-exporter-in-java-complete-guide-for-opentelemetry/
Thanos Integration in Java: Global View of Metrics
Explains how to integrate Thanos with Java monitoring systems to create a scalable global metrics view across multiple Prometheus instances.
https://macronepal.com/blog/thanos-integration-in-java-global-view-of-metrics
Time Series with InfluxDB in Java: Complete Guide (Version 2)
Explains how to manage time-series data using InfluxDB in Java applications, including storing, querying, and analyzing metrics data.
https://macronepal.com/blog/time-series-with-influxdb-in-java-complete-guide-2
Time Series with InfluxDB in Java: Complete Guide
Provides an overview of integrating InfluxDB with Java for time-series data handling, including monitoring applications and managing performance metrics.
https://macronepal.com/blog/time-series-with-influxdb-in-java-complete-guide
Implementing Prometheus Remote Write in Java (Version 2)
Explains how to configure Java applications to send metrics data to Prometheus-compatible systems using the remote write feature for scalable monitoring.
https://macronepal.com/blog/implementing-prometheus-remote-write-in-java-a-complete-guide-2
Implementing Prometheus Remote Write in Java: Complete Guide
Provides instructions for sending metrics from Java services to Prometheus servers, enabling centralized monitoring and real-time analytics.
https://macronepal.com/blog/implementing-prometheus-remote-write-in-java-a-complete-guide
Building a TileServer GL in Java: Vector and Raster Tile Server
Explains how to build a TileServer GL in Java for serving vector and raster map tiles, useful for geographic visualization and mapping applications.
https://macronepal.com/blog/building-a-tileserver-gl-in-java-vector-and-raster-tile-server
Indoor Mapping in Java
Explains how to create indoor mapping systems in Java, including navigation inside buildings, spatial data handling, and visualization techniques.