Le but de ce conseil rapide est de fournir un code de démarrage minimaliste pour configurer un module utilisant :
- Spring Boot
- GraphQL
- PostgreSQL
- TestContainers
Le code fonctionne, et le test est validé, mais, comme vous pouvez l’imaginer, la logique est très triviale car il s’agit uniquement de fournir un point de départ.
Le code est disponible ici sur GitHub. Je ferai référence à ce dépôt dans ce qui suit.
Définition du projet Maven
Le fichier pom.xml inclut les dépendances nécessaires. Notez les points suivants, que je trouve souvent très utiles :
- Les versions des dépendances sont déclarées en tant que propriétés. Cela permet de les gérer à un point unique et centralisé
- Le plugin release déclenche, via un profil dédié, la construction et la publication de l’image Docker du module. À la fin de la release, il y aura l’artifact dans le dépôt Maven et l’image Docker dans le registre de conteneurs, tous deux versionnés. Notez que cette fonctionnalité est commentée car le code n’inclut pas les identifiants pour lire/écrire dans un registre de conteneurs
- Le profil build-and-publish peut également être déclenché de manière autonome : ainsi, Maven publiera une image Docker en version SNAPSHOT
build-and-publish-images
...
org.codehaus.mojo
exec-maven-plugin
3.0.0
docker-build
...
docker-login
...
docker-push
...
GraphQL Schema
Le schéma GraphQL se trouve dans /src/main/resources/graphql/schema.graphqls. Spring Boot le lit automatiquement. Si votre domaine est vaste, le schéma peut être divisé en plusieurs fichiers. Cependant, ce n’est pas nécessaire ici, car nous avons uniquement un type et un champ de requête.
Entité JPA et Référentiel Spring Data
Le modèle de domaine se compose d’une seule entité simple, un Bass. En plus de l’entité, le référentiel Spring Data correspondant fournit automatiquement les modèles d’accès aux données courants.
Notez l’utilisation de Lombok, un excellent outil pour réduire le code répétitif.
Contrôleur
@Controller
public class ShopController {
...
}
Test : ApplicationInitializer
L’ApplicationInitializer est un rappel Spring au début de la suite de tests. Le module utilise TestContainers pour déployer le middleware requis dans les tests, en l’occurrence 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
Notez la définition du volume : elle utilise un script pour initialiser et pré-remplir la base de données de test.
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
L’IntegrationTestCaseSuperLayer inclut les annotations et les composants “autowired” dont nous avons besoin dans tous les cas de test.
@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;
}
Cas de Test
Et enfin, voici notre cas de test trivial.
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();
}
}