Class 9: Testing
Duration: 30 minutes Difficulty: Intermediate Prerequisites: Completed Classes 1-8, basic JUnit knowledge
What You'll Learn
By the end of this class, you will:
- Write unit tests for resolvers
- Use GraphQlTester for integration tests
- Test mutations with variables
- Verify error handling
- Test subscriptions
Testing Strategy
┌─────────────────────────────────────────────────────────────────────┐
│ TESTING PYRAMID │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ /\ │
│ / \ │
│ / E2E \ Few - Slow - Expensive │
│ /──────\ │
│ / \ │
│ / Integration\ Medium - Test full flow │
│ /──────────────\ │
│ / \ │
│ / Unit Tests \ Many - Fast - Isolated │
│ /────────────────────\ │
│ │
│ GraphQL Testing: │
│ - Unit: Test resolver logic with mocked dependencies │
│ - Integration: Test GraphQL endpoint with GraphQlTester │
│ - E2E: Test full app with real database (optional) │
│ │
└─────────────────────────────────────────────────────────────────────┘
Step 1: Add Test Dependencies
Your pom.xml should already have these from Spring Initializr:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.graphql</groupId>
<artifactId>spring-graphql-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-test</artifactId>
<scope>test</scope>
</dependency>
Step 2: Unit Testing Resolvers
Unit tests focus on resolver logic with mocked dependencies:
📁 src/test/java/com/example/moviedb/controller/MovieControllerTest.java
package com.example.moviedb.controller;
import com.example.moviedb.model.Genre;
import com.example.moviedb.model.Movie;
import com.example.moviedb.repository.ActorRepository;
import com.example.moviedb.repository.DirectorRepository;
import com.example.moviedb.repository.MovieRepository;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import java.util.List;
import java.util.Optional;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.when;
@ExtendWith(MockitoExtension.class)
class MovieControllerTest {
@Mock
private MovieRepository movieRepository;
@Mock
private DirectorRepository directorRepository;
@Mock
private ActorRepository actorRepository;
private MovieController controller;
@BeforeEach
void setUp() {
controller = new MovieController(movieRepository, directorRepository, actorRepository);
}
@Test
void movie_existingId_returnsMovie() {
// Given
Movie expectedMovie = new Movie("1", "Test Movie", 2024, Genre.ACTION,
8.5, 120, "Test plot", false, "1", List.of());
when(movieRepository.findById("1")).thenReturn(Optional.of(expectedMovie));
// When
Movie result = controller.movie("1");
// Then
assertThat(result).isNotNull();
assertThat(result.getTitle()).isEqualTo("Test Movie");
assertThat(result.getReleaseYear()).isEqualTo(2024);
}
@Test
void movie_nonExistingId_throwsNotFoundException() {
// Given
when(movieRepository.findById("999")).thenReturn(Optional.empty());
// When/Then
org.junit.jupiter.api.Assertions.assertThrows(
com.example.moviedb.exception.NotFoundException.class,
() -> controller.movie("999")
);
}
@Test
void movies_noFilter_returnsAllMovies() {
// Given
List<Movie> allMovies = List.of(
new Movie("1", "Movie 1", 2024, Genre.ACTION, 8.0, 120, null, false, "1", List.of()),
new Movie("2", "Movie 2", 2023, Genre.DRAMA, 7.5, 140, null, false, "2", List.of())
);
when(movieRepository.findAll()).thenReturn(allMovies);
// When
List<Movie> result = controller.movies(null);
// Then
assertThat(result).hasSize(2);
}
@Test
void movies_withGenreFilter_returnsFilteredMovies() {
// Given
List<Movie> actionMovies = List.of(
new Movie("1", "Action Movie", 2024, Genre.ACTION, 8.0, 120, null, false, "1", List.of())
);
when(movieRepository.findByGenre(Genre.ACTION)).thenReturn(actionMovies);
// When
List<Movie> result = controller.movies(Genre.ACTION);
// Then
assertThat(result).hasSize(1);
assertThat(result.get(0).getGenre()).isEqualTo(Genre.ACTION);
}
@Test
void searchMovies_matchingTitle_returnsMovies() {
// Given
List<Movie> matchingMovies = List.of(
new Movie("1", "The Dark Knight", 2008, Genre.ACTION, 9.0, 152, null, false, "1", List.of())
);
when(movieRepository.searchByTitle("dark")).thenReturn(matchingMovies);
// When
List<Movie> result = controller.searchMovies("dark");
// Then
assertThat(result).hasSize(1);
assertThat(result.get(0).getTitle()).contains("Dark");
}
}
Step 3: Integration Testing with GraphQlTester
Integration tests verify the entire GraphQL stack:
📁 src/test/java/com/example/moviedb/MovieGraphQLIntegrationTest.java
package com.example.moviedb;
import com.example.moviedb.model.Genre;
import com.example.moviedb.model.Movie;
import com.example.moviedb.repository.MovieRepository;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.graphql.tester.AutoConfigureHttpGraphQlTester;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.graphql.test.tester.HttpGraphQlTester;
import static org.assertj.core.api.Assertions.assertThat;
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@AutoConfigureHttpGraphQlTester
class MovieGraphQLIntegrationTest {
@Autowired
private HttpGraphQlTester graphQlTester;
@Test
void queryMovies_returnsAllMovies() {
graphQlTester
.document("""
query {
movies {
id
title
releaseYear
}
}
""")
.execute()
.path("movies")
.entityList(Movie.class)
.hasSizeGreaterThan(0);
}
@Test
void queryMovie_existingId_returnsMovie() {
graphQlTester
.document("""
query GetMovie($id: ID!) {
movie(id: $id) {
id
title
genre
rating
}
}
""")
.variable("id", "1")
.execute()
.path("movie.title")
.entity(String.class)
.isEqualTo("The Shawshank Redemption");
}
@Test
void queryMovie_nonExistingId_returnsError() {
graphQlTester
.document("""
query GetMovie($id: ID!) {
movie(id: $id) {
title
}
}
""")
.variable("id", "99999")
.execute()
.errors()
.satisfy(errors -> {
assertThat(errors).hasSize(1);
assertThat(errors.get(0).getMessage()).contains("not found");
});
}
@Test
void queryMovies_filterByGenre_returnsFilteredMovies() {
graphQlTester
.document("""
query MoviesByGenre($genre: Genre!) {
movies(genre: $genre) {
title
genre
}
}
""")
.variable("genre", "SCIFI")
.execute()
.path("movies[*].genre")
.entityList(String.class)
.satisfies(genres -> {
genres.forEach(genre -> assertThat(genre).isEqualTo("SCIFI"));
});
}
@Test
void queryMovieWithDirector_returnsNestedData() {
graphQlTester
.document("""
query {
movie(id: "1") {
title
director {
name
nationality
}
}
}
""")
.execute()
.path("movie.title")
.entity(String.class)
.isEqualTo("The Shawshank Redemption")
.path("movie.director.name")
.entity(String.class)
.isEqualTo("Frank Darabont");
}
@Test
void searchMovies_partialMatch_returnsResults() {
graphQlTester
.document("""
query SearchMovies($title: String!) {
searchMovies(title: $title) {
title
}
}
""")
.variable("title", "the")
.execute()
.path("searchMovies")
.entityList(Movie.class)
.hasSizeGreaterThan(0)
.satisfies(movies -> {
movies.forEach(movie ->
assertThat(movie.getTitle().toLowerCase()).contains("the")
);
});
}
}
Step 4: Testing Mutations
📁 src/test/java/com/example/moviedb/MutationIntegrationTest.java
package com.example.moviedb;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.graphql.tester.AutoConfigureHttpGraphQlTester;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.graphql.test.tester.HttpGraphQlTester;
import org.springframework.security.test.context.support.WithMockUser;
import static org.assertj.core.api.Assertions.assertThat;
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@AutoConfigureHttpGraphQlTester
class MutationIntegrationTest {
@Autowired
private HttpGraphQlTester graphQlTester;
@Test
@WithMockUser(roles = "ADMIN")
void createMovie_validInput_createsAndReturnsMovie() {
String createdId = graphQlTester
.document("""
mutation CreateMovie($input: CreateMovieInput!) {
createMovie(input: $input) {
id
title
releaseYear
genre
rating
}
}
""")
.variable("input", """
{
"title": "Test Movie",
"releaseYear": 2024,
"genre": "ACTION",
"rating": 8.5,
"directorId": "1"
}
""")
.execute()
.path("createMovie.title")
.entity(String.class)
.isEqualTo("Test Movie")
.path("createMovie.id")
.entity(String.class)
.get();
assertThat(createdId).isNotNull();
}
@Test
@WithMockUser(roles = "ADMIN")
void createMovie_invalidRating_returnsValidationError() {
graphQlTester
.document("""
mutation CreateMovie($input: CreateMovieInput!) {
createMovie(input: $input) {
id
}
}
""")
.variable("input", """
{
"title": "Test Movie",
"releaseYear": 2024,
"genre": "ACTION",
"rating": 15.0,
"directorId": "1"
}
""")
.execute()
.errors()
.satisfy(errors -> {
assertThat(errors).hasSize(1);
assertThat(errors.get(0).getMessage()).contains("Rating");
});
}
@Test
@WithMockUser(roles = "USER") // Not ADMIN or EDITOR
void createMovie_insufficientPermissions_returnsError() {
graphQlTester
.document("""
mutation {
createMovie(input: {
title: "Unauthorized Movie"
releaseYear: 2024
genre: ACTION
directorId: "1"
}) {
id
}
}
""")
.execute()
.errors()
.satisfy(errors -> {
assertThat(errors).isNotEmpty();
assertThat(errors.get(0).getMessage()).containsIgnoringCase("denied");
});
}
@Test
@WithMockUser(roles = "ADMIN")
void updateMovie_validInput_updatesMovie() {
graphQlTester
.document("""
mutation UpdateMovie($id: ID!, $input: UpdateMovieInput!) {
updateMovie(id: $id, input: $input) {
id
rating
inTheaters
}
}
""")
.variable("id", "1")
.variable("input", """
{
"rating": 9.9,
"inTheaters": true
}
""")
.execute()
.path("updateMovie.rating")
.entity(Double.class)
.isEqualTo(9.9)
.path("updateMovie.inTheaters")
.entity(Boolean.class)
.isEqualTo(true);
}
@Test
@WithMockUser(roles = "ADMIN")
void deleteMovie_existingMovie_returnsSuccess() {
// First create a movie to delete
String movieId = graphQlTester
.document("""
mutation {
createMovie(input: {
title: "Movie To Delete"
releaseYear: 2024
genre: DRAMA
directorId: "1"
}) {
id
}
}
""")
.execute()
.path("createMovie.id")
.entity(String.class)
.get();
// Then delete it
graphQlTester
.document("""
mutation DeleteMovie($id: ID!) {
deleteMovie(id: $id) {
success
message
deletedId
}
}
""")
.variable("id", movieId)
.execute()
.path("deleteMovie.success")
.entity(Boolean.class)
.isEqualTo(true)
.path("deleteMovie.deletedId")
.entity(String.class)
.isEqualTo(movieId);
}
}
Step 5: Testing with Specific Assertions
📁 src/test/java/com/example/moviedb/DetailedAssertionsTest.java
package com.example.moviedb;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.graphql.tester.AutoConfigureHttpGraphQlTester;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.graphql.test.tester.HttpGraphQlTester;
import java.util.Map;
import static org.assertj.core.api.Assertions.assertThat;
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@AutoConfigureHttpGraphQlTester
class DetailedAssertionsTest {
@Autowired
private HttpGraphQlTester graphQlTester;
@Test
void verifyResponseStructure() {
graphQlTester
.document("""
query {
movie(id: "1") {
id
title
releaseYear
genre
rating
director {
id
name
}
actors {
id
name
}
}
}
""")
.execute()
// Verify each field individually
.path("movie.id").entity(String.class).isEqualTo("1")
.path("movie.title").entity(String.class).isNotNull()
.path("movie.releaseYear").entity(Integer.class).satisfies(year -> {
assertThat(year).isGreaterThan(1900);
assertThat(year).isLessThanOrEqualTo(2030);
})
.path("movie.genre").entity(String.class).isNotNull()
.path("movie.director").entity(Map.class).satisfies(director -> {
assertThat(director).containsKey("id");
assertThat(director).containsKey("name");
})
.path("movie.actors").entityList(Map.class).hasSizeGreaterThan(0);
}
@Test
void verifyListOrdering() {
graphQlTester
.document("""
query {
movies {
id
releaseYear
}
}
""")
.execute()
.path("movies[0].id")
.entity(String.class)
.isEqualTo("1")
.path("movies[1].id")
.entity(String.class)
.isEqualTo("2");
}
@Test
void verifyJsonPath() {
graphQlTester
.document("""
query {
movie(id: "3") {
title
director {
name
}
}
}
""")
.execute()
.path("$.data.movie.director.name")
.entity(String.class)
.isEqualTo("Christopher Nolan");
}
}
Step 6: Testing Subscriptions
📁 src/test/java/com/example/moviedb/SubscriptionIntegrationTest.java
package com.example.moviedb;
import com.example.moviedb.model.Movie;
import com.example.moviedb.service.MovieEventPublisher;
import com.example.moviedb.model.Genre;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.graphql.tester.AutoConfigureGraphQlTester;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.graphql.test.tester.GraphQlTester;
import reactor.test.StepVerifier;
import java.time.Duration;
import java.util.List;
@SpringBootTest
@AutoConfigureGraphQlTester
class SubscriptionIntegrationTest {
@Autowired
private GraphQlTester graphQlTester;
@Autowired
private MovieEventPublisher eventPublisher;
@Test
void subscriptionReceivesPublishedEvents() {
// Start subscription
var subscription = graphQlTester
.document("""
subscription {
movieAdded {
id
title
genre
}
}
""")
.executeSubscription()
.toFlux("movieAdded", Movie.class);
// Publish an event
Movie testMovie = new Movie("test-1", "Subscription Test Movie", 2024,
Genre.ACTION, 8.0, 120, null, false, "1", List.of());
// Verify subscription receives the event
StepVerifier.create(subscription.take(1))
.then(() -> eventPublisher.publishMovieAdded(testMovie))
.assertNext(movie -> {
assert movie.getTitle().equals("Subscription Test Movie");
assert movie.getGenre() == Genre.ACTION;
})
.verifyComplete();
}
@Test
void filteredSubscription_receivesOnlyMatchingEvents() {
var subscription = graphQlTester
.document("""
subscription MoviesByGenre($genre: Genre!) {
movieAddedByGenre(genre: $genre) {
title
genre
}
}
""")
.variable("genre", "SCIFI")
.executeSubscription()
.toFlux("movieAddedByGenre", Movie.class);
Movie scifiMovie = new Movie("test-2", "Sci-Fi Movie", 2024,
Genre.SCIFI, 8.0, 120, null, false, "1", List.of());
Movie actionMovie = new Movie("test-3", "Action Movie", 2024,
Genre.ACTION, 8.0, 120, null, false, "1", List.of());
StepVerifier.create(subscription.take(1).timeout(Duration.ofSeconds(5)))
.then(() -> {
eventPublisher.publishMovieAdded(actionMovie); // Should be filtered out
eventPublisher.publishMovieAdded(scifiMovie); // Should be received
})
.assertNext(movie -> {
assert movie.getGenre() == Genre.SCIFI;
})
.verifyComplete();
}
}
Step 7: Test Data Builders
Create test data builders for cleaner tests:
📁 src/test/java/com/example/moviedb/TestDataBuilder.java
package com.example.moviedb;
import com.example.moviedb.model.Genre;
import com.example.moviedb.model.Movie;
import java.util.List;
public class TestDataBuilder {
public static MovieBuilder movie() {
return new MovieBuilder();
}
public static class MovieBuilder {
private String id = "test-" + System.currentTimeMillis();
private String title = "Test Movie";
private int releaseYear = 2024;
private Genre genre = Genre.ACTION;
private Double rating = 7.5;
private Integer runtime = 120;
private String plot = "Test plot";
private boolean inTheaters = false;
private String directorId = "1";
private List<String> actorIds = List.of();
public MovieBuilder id(String id) {
this.id = id;
return this;
}
public MovieBuilder title(String title) {
this.title = title;
return this;
}
public MovieBuilder releaseYear(int year) {
this.releaseYear = year;
return this;
}
public MovieBuilder genre(Genre genre) {
this.genre = genre;
return this;
}
public MovieBuilder rating(Double rating) {
this.rating = rating;
return this;
}
public MovieBuilder withActors(String... actorIds) {
this.actorIds = List.of(actorIds);
return this;
}
public Movie build() {
return new Movie(id, title, releaseYear, genre, rating,
runtime, plot, inTheaters, directorId, actorIds);
}
}
}
Usage:
Movie testMovie = TestDataBuilder.movie()
.title("Custom Title")
.genre(Genre.DRAMA)
.rating(9.0)
.build();
Exercises
Exercise 1: Test Error Messages
Write a test that verifies the exact error message and extension data when a movie is not found.
Exercise 2: Test Pagination
If you implement pagination (next class), write tests for:
- First page with limit
- Cursor-based pagination
- Edge cases (empty results)
Exercise 3: Performance Testing
Write a test that verifies the N+1 fix by counting database calls when fetching movies with directors.
Summary
In this class, you learned:
✅ Unit testing resolvers with mocks
✅ Integration testing with HttpGraphQlTester
✅ Testing queries with variables
✅ Testing mutations and verifying side effects
✅ Testing error scenarios
✅ Testing subscriptions with StepVerifier
✅ Using @WithMockUser for security testing
What's Next?
In Class 10: Pagination & Filtering, we'll learn:
- Cursor-based pagination
- Connection pattern (edges/nodes)
- Complex filtering
- Sorting options
Time to handle large datasets properly!