The purpose of this flash tip is to provide a minimalistic quickstart code for setting up a module using
- Spring Boot
- GraphQL
- PostgreSQL
- TestContainers
The code is working, and the test is green, but as you can imagine, the logic is very trivial because it is supposed to provide only a starting point.
The code is available here on GitHub. Hereafter, I will be referring to that.
Maven Project definition
The pom.xml includes the required dependencies. Note the following, which I often find very useful
- dependencies versions, are declared as properties. In that way, we can manage them at a single, central point.
- The release plugin triggers, through a dedicated profile, the build and publishing of the module Docker image. At the end of the release, there will be the artifact in the Maven repository and the Docker image in the container registry, both versioned. Note it is commented on because the code doesn’t include the credentials for reading/writing a container registry.
- The build-and-publish profile can also be triggered standalone: in this way, Maven will publish a SNAPSHOT Docker image.
build-and-publish-images
...
org.codehaus.mojo
exec-maven-plugin
3.0.0
docker-build
...
docker-login
...
docker-push
...
GraphQL Schema
The GraphQL schema is in /src/main/resources/graphql/schema.graphqls. Spring Boot automatically reads it. If your domain is huge, it can be split into multiple files. Indeed, that is not the case where we have just one type and one query field.
JPA Entity and Spring Data Repository
The domain model consists of a single, simple entity, a Bass; beside the entity, the corresponding Spring Data Repository automatically provides the common data access patterns.
Note the usage of Lombok, a great tool for reducing the code boilerplate.
Controller
The API endpoint is a Spring Controller, which maps the schema’s query field(s) and provides the corresponding implementation.
@Controller
public class ShopController {
...
}
Test: ApplicationInitializer
The ApplicationInitializer is a Spring callback at the beginning of the test suite. The module uses TestContainers to spin up the required middleware used in the tests, PostgreSQL.
public class MiddlewareSetup implements ApplicationContextInitializer {
public static DockerComposeContainer SUBSYSTEMS;
@Override
public void initialize(ConfigurableApplicationContext context) {
if (SUBSYSTEMS == null) {
var dockerCompose = new File(System.getProperty("user.dir"), "src/test/resources/docker/docker-compose.yml");
SUBSYSTEMS =
new DockerComposeContainer(dockerCompose)
.withExposedService(
"postgres_1",
5432,
Wait.forListeningPort().withStartupTimeout(Duration.ofMinutes(5)));
SUBSYSTEMS.start();
}
}
}
Test: Docker compose
Note the volume definition: it uses a script for initializing and prepopulating the test database.
version: '3.3'
services:
postgres:
image: library/postgres:14.5
environment:
- POSTGRES_USER=shop_owner
- POSTGRES_PASSWORD=029348hdhj
- POSTGRES_DB=bass_shop
command: postgres -c max_connections=300 -c log_min_messages=LOG
volumes:
- ./populate_shop.sql:/docker-entrypoint-initdb.d/populate_shop.sql
ports:
- "5432:5432"
tty: true
Test: Superclass
The IntegrationTestCaseSuperLayer includes annotations and “autowired” components we need in all test cases.
@SpringBootTest(
webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT,
classes = ElectricBassShop.class)
@ContextConfiguration(initializers = MiddlewareSetup.class)
@AutoConfigureMockMvc
@AutoConfigureGraphQlTester
@ActiveProfiles("it")
public abstract class IntegrationTestCaseSuperLayer {
@Autowired
protected HttpGraphQlTester tester;
@Autowired
protected MockMvc mvc;
}
Test Case
And finally, here’s our trivial test case.
public class ShopTestCase extends IntegrationTestCaseSuperLayer {
@Test
void findSomeBasses() {
String query =
"query {" +
" instruments {" +
" id" +
" brand" +
" model" +
" fretless" +
" strings" +
" }" +
"}";
var basses =
tester.document(query)
.execute()
.path("data.instruments[*]")
.entityList(Bass.class)
.get();
assertThat(basses).asList().hasSize(3);
// Not so robust/complete as test...
var fenderJazz =
of(basses.iterator())
.map(Iterator::next)
.map(Bass.class::cast)
.orElseThrow();
assertThat(fenderJazz.getStrings()).isEqualTo(4);
assertThat(fenderJazz.getModel()).isEqualTo("Jazz Vintage 62");
assertThat(fenderJazz.getBrand()).isEqualTo("Fender");
assertThat(fenderJazz.isFretless()).isFalse();
}
}