1. Présentation et objectifs
Le but est de continuer le développement de notre architecture "à la microservice".
Pour rappel, dans cette architecture, chaque composant a son rôle précis :
-
la servlet reçoit les requêtes HTTP, et les envoie au bon controller (rôle de point d’entrée de l’application)
-
le contrôleur implémente une méthode Java par route HTTP, récupère les paramètres, et appelle le service (rôle de routage)
-
le service implémente le métier de notre microservice
-
le repository représente les accès aux données (avec potentiellement une base de données)
Et pour s’amuser un peu, nous allons réaliser un micro-service qui nous renvoie des données sur les dresseurs de Pokemon !
Nous allons développer :
-
un repository d’accès aux données de Trainers (à partir d’une base de données)
-
un service d’accès aux données
-
annoter ces composants avec les annotations de Spring et les tester
-
créer un contrôleur spring pour gérer nos requêtes HTTP / REST
-
charger quelques données
Nous repartons de zéro pour ce TP ! |
Voici le schéma du code qu’il faut écrire :
2. GitLab
Identifiez-vous sur GitLab, et cliquez sur le lien suivant pour créer votre repository git: GitLab classroom
Clonez ensuite votre repository git sur votre poste !
À partir de ce TP, votre repository nouvellement créé contiendra au moins un squelette de projet contenant :
|
3. Le pom.xml
Modifiez le fichier pom.xml à la racine du projet
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
<project>
<modelVersion>4.0.0</modelVersion>
<groupId>fr.univ-lille.alom</groupId>
<artifactId>trainer-api</artifactId> (1)
<version>0.1.0</version>
<packaging>jar</packaging> (2)
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.3.4</version> (2)
</parent>
<properties>
<java.version>21</java.version> (3)
</properties>
<dependencies>
<!-- spring-boot web-->
<dependency>
<groupId>org.springframework.boot</groupId> (2)
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- testing --> (4)
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
</dependency>
</dependencies>
<build> (5)
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
1 | Modifiez votre artifactId |
2 | Cette fois, on utilise directement spring-boot pour construire un jar |
3 | en java 21… |
4 | On positionne spring-boot-starter-test qui nous importe JUnit et Mockito ! |
5 | La partie build utilise le spring-boot-maven-plugin |
Notre projet est prêt !
4. Les classes du domaine
Nous allons manipuler, dans ce microservice, des dresseurs de Pokemon (Trainer), ainsi que leur équipe de Pokemons préférée (id de pokémon type + niveau).
Nous allons donc commencer par écrire deux classes Java pour représenter nos données : Trainer
et PokemonTeamMember
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class Trainer { (1)
private String name; (2)
private List<PokemonTeamMember> team; (3)
public Trainer() {
}
public Trainer(String name) {
this.name = name;
}
[...] (4)
}
1 | Notre classe de dresseur de Pokemon |
2 | Son nom |
3 | La liste de ses pokemons |
4 | Les getters/setters habituels (à générer avec Alt+Inser !) |
Vous pouvez utiliser des records à cette étape ! |
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class PokemonTeamMember {
private int pokemonTypeId; (1)
private int level; (2)
public PokemonTeamMember() {
}
public PokemonTeamMember(int pokemonTypeId, int level) {
this.pokemonTypeId = pokemonTypeId;
this.level = level;
}
[...] (4)
}
1 | le numéro de notre Pokemon dans le Pokedex (référence au service pokemon-type-api !) |
2 | le niveau de notre Pokemon ! |
Ajouter l’interface du TrainerPort !
1
2
3
// TODO
public interface TrainerPort {
}
Attention, ici, nous ne développerons pas l’implémentation du port, mais juste une interface qui servira par la suite. Il faudra lui ajouter quelques méthodes. |
5. Le service métier
Maintenant que nous avons un port, il est temps de développer un service qui consomme notre port !
5.1. Le test unitaire
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
class TrainerServiceImplTest {
@Test
void getAllTrainers_shouldCallThePort() {
var trainerPort = mock(TrainerPort.class);
var trainerService = new TrainerServiceImpl(trainerPort);
trainerService.getAllTrainers();
verify(trainerPort).findAll();
}
@Test
void getTrainer_shouldCallThePort() {
var trainerPort = mock(TrainerPort.class);
var trainerService = new TrainerServiceImpl(trainerPort);
trainerService.getTrainer("Ash");
verify(trainerPort).findById("Ash");
}
@Test
void createTrainer_shouldCallThePort() {
var trainerPort = mock(TrainerPort.class);
var trainerService = new TrainerServiceImpl(trainerPort);
var ash = new Trainer();
trainerService.createTrainer(ash);
verify(trainerPort).save(ash);
}
}
5.2. L’implémentation
L’interface Java
1
2
3
4
5
6
public interface TrainerService {
Iterable<Trainer> getAllTrainers();
Trainer getTrainer(String name);
Trainer createTrainer(Trainer trainer);
}
et son implémentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// TODO
class TrainerServiceImpl implements TrainerService { (1)
private TrainerPort trainerPort;
public TrainerServiceImpl(TrainerPort trainerPort) {
this.trainerPort = trainerPort;
}
@Override
public Iterable<Trainer> getAllTrainers() {
// TODO
}
@Override
public Trainer getTrainer(String name) {
// TODO
}
@Override
public Trainer createTrainer(Trainer trainer) {
// TODO
}
}
1 | à implémenter ! |
Nous utilisons l’injection de dépendances entre le service et le port, car nous allons implémenter 2 versions de ce port, une pour des bases de données SQL (JPA), et une pour des bases de données documents (MongoDb). |
6. Le repository JPA
Lors du TP précédent, nous avions écrit un repository qui utilisait un fichier JSON
comme source de données.
Cette semaine, nous utiliserons directement une base de données, SQL, embarquée dans un premier temps.
Nous commençons les développements avec une base de données embarquée, puis nous testerons ensuite une base de données dans un container Docker. |
Cette base de données est H2. H2 est écrit en Java, implémente le standard SQL, et peut fonctionner directement en mémoire !
6.1. L’ajout de la dépendance spring-boot-data-jpa et H2
Ajoutez les dépendances suivantes dans votre pom.xml
-
spring-boot-starter-data-jpa
-
h2 (en scope test)
6.2. Les classes d’entité
Dans un package fr.univ_lille.alom.trainers.jpa
, créez les classes d’entité suivantes :
-
TrainerEntity
: correspond à l’objet du domaineTrainer
, et contient les mêmes attributs -
PokemonTeamMemberEntity
: correspond à l’objet du domainPokemonTeamMember
, et contient les mêmes attributs, plus un identifiant unique
Nous ne pouvons pas utiliser les
Les records ne respectent pas ces conditions, et donc on ne peut pas les utiliser pour le moment 😔. |
6.3. Les test unitaires
Implémentez les tests unitaires suivants :
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
package fr.univ_lille.alom.trainers.jpa;
import org.junit.jupiter.api.Test;
import jakarta.persistence.*;
import static org.junit.jupiter.api.Assertions.*;
class TrainerEntityTest {
@Test
void trainerEntity_shouldBeAnEntity(){
assertNotNull(TrainerEntity.class.getAnnotation(Entity.class)); (1)
}
@Test
void trainerEntityName_shouldBeAnId() throws NoSuchFieldException {
assertNotNull(TrainerEntity.class.getDeclaredField("name").getAnnotation(Id.class)); (2)
}
@Test
void trainerEntityTeam_shouldBeAElementCollection() throws NoSuchFieldException {
assertNotNull(TrainerEntity.class.getDeclaredField("team").getAnnotation(OneToMany.class)); (3)
}
}
1 | Notre classe TrainerEntity doit être annotée @Entity pour être reconnue par JPA |
2 | Chaque classe annotée @Entity doit déclarer un de ses champs comme étant un @Id . Dans le cas du Trainer ,
le champ name est idéal |
3 | La relation entre TrainerEntity et PokemonTeamMemberEntity doit également être annotée. Ici, un TrainerEntity possède une collection de PokemonTeamMemberEntity . |
1
2
3
4
5
6
7
8
9
10
11
12
13
class PokemonTeamMemberEntityTest {
@Test
void pokemonTeamMemberEntity_shouldBeAnEntity(){
assertNotNull(PokemonTeamMemberEntity.class.getAnnotation(Entity.class)); (1)
}
@Test
void pokemonTeamMemberEntity_shouldHaveAnId(){
assertNotNull(PokemonTeamMemberEntity.class.getDeclaredField("id").getAnnotation(Id.class)); (2)
}
}
1 | Notre classe PokemonTeamMemberEntity doit aussi être annotée @Entity pour être reconnue par JPA |
2 | Une entité JPA doit avoir un champ @Id . |
6.4. L’interface du repository JPA
Créez une interface de repository JPA nommée TrainerJpaRepository
.
Pour vous aider, voici deux liens intéressants :
|
Ajoutez un test pour cette interface de repository :
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
package fr.univ_lille.alom.trainers.jpa;
import static org.junit.jupiter.api.Assertions.*;
@DataJpaTest (1)
class TrainerEntityJpaRepositoryTest {
@Autowired (2)
private TrainerEntityJpaRepository repository;
@Test
void trainerJpaRepository_shouldExtendsCrudRepository() throws NoSuchMethodException {
assertTrue(CrudRepository.class.isAssignableFrom(TrainerEntityJpaRepository.class)); (3)
}
@Test
void trainerJpaRepositoryShouldBeInstanciedBySpring(){
assertNotNull(repository);
}
@Test
void testSave(){ (4)
var ash = new TrainerEntity("Ash");
repository.save(ash);
var saved = repository.findById(ash.getName()).orElse(null);
assertEquals("Ash", saved.getName());
}
@Test
void testSaveWithPokemons(){ (5)
var misty = new TrainerEntity("Misty");
var staryu = new PokemonTeamMemberEntity(120, 18);
var starmie = new PokemonTeamMemberEntity(121, 21);
misty.setTeam(List.of(staryu, starmie));
repository.save(misty);
var saved = repository.findById(misty.getName()).orElse(null);
assertEquals("Misty", saved.getName());
assertEquals(2, saved.getTeam().size());
}
}
1 | On utilise un @DataJpaTest test, qui va démarrer spring (uniquement la partie gestion des repositories et base de données). |
2 | On utilise l’injection de dépendances spring dans notre test ! |
3 | On valide que notre repository hérite du CrudRepository proposé par spring. |
4 | On test la sauvegarde simple |
5 | et la sauvegarde avec des objets en cascade ! |
Ce type de test, appelé test d’intégration, a pour but de valider que l’application se construit bien. Le démarrage de spring étant plus long que le simple couple JUnit/Mockito, on utilise souvent ces tests uniquement sur la partie repository |
Notre test sera exécuté avec une instance de base de données H2 instanciée à la volée ! |
6.5. L’exécution de notre test
Pour s’exécuter, notre test unitaire a besoin d’une application Spring-Boot !
Vérifiez que vous avez bien une classe TrainerApiApplication.java
, sinon créez la :
1
2
3
4
5
6
7
8
@SpringBootApplication (1)
public class TrainerApiApplication {
public static void main(String... args){ (2)
SpringApplication.run(TrainerApiApplication.class, args);
}
}
1 | On annote la classe comme étant le point d’entrée de notre application |
2 | On implémente un main pour démarrer notre application ! |
7. L’adapter JPA
Il ne manque pas quelque chose ?
Les interfaces TrainerPort
et TrainerEntityJpaRepository
sont différentes.
Implémentez dans le package fr.univ_lille.alom.trainers.jpa
une classe TrainerJpaAdapter
qui implémente TrainerPort
.
Cette classe devra :
-
recevoir en injection de dépendance l’interface
TrainerEntityJpaRepository
-
être elligible à l’injection de dépendance, en étant annotée
@Component
par exemple -
implémenter les méthodes de
TrainerPort
-
transformer les instances de
Trainer
enTrainerEntity
et inversement là où c’est nécessaire
la transformation peut aussi être faite dans une méthode ou une classe consacrée, soyez créatifs. |
8. Le controlleur
Implémentons un contrôleur afin d’exposer nos Trainers en HTTP/REST/JSON.
8.1. Le test unitaire
Le contrôleur est simple et s’inspire de ce que nous avons fait au TP précédent.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
class TrainerControllerTest {
@Mock
private TrainerService trainerService;
@InjectMocks
private TrainerController trainerController;
@BeforeEach
void setup(){
MockitoAnnotations.initMocks(this);
}
@Test
void getAllTrainers_shouldCallTheService() {
trainerController.getAllTrainers();
verify(trainerService).getAllTrainers();
}
@Test
void getTrainer_shouldCallTheService() {
trainerController.getTrainer("Ash");
verify(trainerService).getTrainer("Ash");
}
}
8.2. L’implémentation
Compléter l’implémentation du controller :
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class TrainerController {
private final TrainerService trainerService;
TrainerController(TrainerService trainerService){
this.trainerService = trainerService;
}
Iterable<Trainer> getAllTrainers(){
// TODO (1)
}
Trainer getTrainer(String name){
// TODO (1)
}
}
1 | Implémentez ! |
8.3. L’ajout des annotations Spring
Ajoutez les méthodes de test suivantes dans la classe TrainerControllerTest
:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
@Test
void trainerController_shouldBeAnnotated(){
var controllerAnnotation =
TrainerController.class.getAnnotation(RestController.class);
assertNotNull(controllerAnnotation);
var requestMappingAnnotation =
TrainerController.class.getAnnotation(RequestMapping.class);
assertArrayEquals(new String[]{"/trainers"}, requestMappingAnnotation.value());
}
@Test
void getAllTrainers_shouldBeAnnotated() throws NoSuchMethodException {
var getAllTrainers =
TrainerController.class.getDeclaredMethod("getAllTrainers");
var getMapping = getAllTrainers.getAnnotation(GetMapping.class);
assertNotNull(getMapping);
assertArrayEquals(new String[]{"/"}, getMapping.value());
}
@Test
void getTrainer_shouldBeAnnotated() throws NoSuchMethodException {
var getTrainer =
TrainerController.class.getDeclaredMethod("getTrainer", String.class);
var getMapping = getTrainer.getAnnotation(GetMapping.class);
var pathVariableAnnotation = getTrainer.getParameters()[0].getAnnotation(PathVariable.class);
assertNotNull(getMapping);
assertArrayEquals(new String[]{"/{name}"}, getMapping.value());
assertNotNull(pathVariableAnnotation);
}
Modifiez votre classe TrainerController
pour faire passer les tests !
8.4. L’exécution de notre projet !
Pour exécuter notre projet, nous devons simplement lancer la classe TrainerApiApplication
écrite plus haut.
Mais avant cela, modifions quelques propriétés de spring !
8.4.1. Personnalisation de Spring-Boot
Nous voulons un peu plus de logs pour bien comprendre ce que fait spring-boot.
Pour ce faire, nous allons monter le niveau de logs au niveau TRACE
.
Créer un fichier application.properties
dans le répertoire src/main/resources
.
1
2
3
4
# on demande un niveau de logs TRACE a spring-web
logging.level.web=TRACE
# on modifie le port par defaut du tomcat !
server.port=8081
Le répertoire src/main/resources est ajouté au classpath Java par IntelliJ, lors de l’exécution, et par Maven lors
de la construction de notre jar !
|
La liste des properties supportées est décrite dans la documentation de spring ici
8.4.2. Ajout de données au démarrage
Comme notre application ne contient aucune donnée au démarrage, nous allons en charger quelques-unes "en dur" pour commencer.
Ajoutez le code suivant dans la classe TrainerApiApplication
:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@Bean (2)
@Autowired (3)
public CommandLineRunner demo(TrainerPort port) { (1)
return (args) -> { (4)
var ash = new Trainer("Ash");
var pikachu = new PokemonTeamMember(25, 18);
ash.setTeam(List.of(pikachu));
var misty = new Trainer("Misty");
var staryu = new PokemonTeamMember(120, 18);
var starmie = new PokemonTeamMember(121, 21);
misty.setTeam(List.of(staryu, starmie));
// save a couple of trainers
port.save(ash); (5)
port.save(misty);
};
}
1 | On implémente un CommandLineRunner pour exécuter des commandes au démarrage de notre application |
2 | On utilise l’annotation @Bean sur notre méthode, pour en déclarer le retour comme étant un bean spring ! |
3 | On utilise l’injection de dépendance sur notre méthode ! |
4 | CommandLineRunner est une @FunctionnalInterface, on en fait une expression lambda. |
5 | On initialise quelques données ! |
8.4.3. Exécution
Démarrez le main, et observez les logs (j’ai réduit la quantité de logs pour qu’elle s’affiche correctement ici) :
. ____ _ __ _ _
/\\ / ___'_ __ _ _(_)_ __ __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
\\/ ___)| |_)| | | | | || (_| | ) ) ) ) (1)
' |____| .__|_| |_|_| |_\__, | / / / /
=========|_|==============|___/=/_/_/_/
:: Spring Boot :: (v2.1.2.RELEASE)
[main] [..] : Starting TrainerApi on jwittouck-N14xWU with PID 23154 (/home/jwittouck/workspaces/alom/alom-2020-2021/tp/trainer-api/target/classes started by jwittouck in /home/jwittouck/workspaces/alom/alom-2020-2021)
[main] [..] : No active profile set, falling back to default profiles: default
[main] [..] : Bootstrapping Spring Data repositories in DEFAULT mode.
[main] [..] : Finished Spring Data repository scanning in 47ms. Found 1 repository interfaces.
[main] [..] : Bean 'org.springframework.transaction.annotation.ProxyTransactionManagementConfiguration' of type [org.springframework.transaction.annotation.ProxyTransactionManagementConfiguration$$EnhancerBySpringCGLIB$$ff9e9081] is not eligible for getting processed by all BeanPostProcessors (for example: not eligible for auto-proxying)
[main] [..] : Tomcat initialized with port(s): 8081 (http) (2)
[main] [..] : Starting service [Tomcat] (2)
[main] [..] : Starting Servlet engine: [Apache Tomcat/9.0.14]
[main] [..] : The APR based Apache Tomcat Native library which allows optimal performance in production environments was not found on the java.library.path: [/usr/java/packages/lib:/usr/lib64:/lib64:/lib:/usr/lib]
[main] [..] : Initializing Spring embedded WebApplicationContext
[main] [..] : Published root WebApplicationContext as ServletContext attribute with name [org.springframework.web.context.WebApplicationContext.ROOT]
[main] [..] : Root WebApplicationContext: initialization completed in 1487 ms
[main] [..] : Added existing Servlet initializer bean 'dispatcherServletRegistration'; order=2147483647, resource=class path resource [org/springframework/boot/autoconfigure/web/servlet/DispatcherServletAutoConfiguration$DispatcherServletRegistrationConfiguration.class]
[main] [..] : Created Filter initializer for bean 'characterEncodingFilter'; order=-2147483648, resource=class path resource [org/springframework/boot/autoconfigure/web/servlet/HttpEncodingAutoConfiguration.class]
[main] [..] : Created Filter initializer for bean 'hiddenHttpMethodFilter'; order=-10000, resource=class path resource [org/springframework/boot/autoconfigure/web/servlet/WebMvcAutoConfiguration.class]
[main] [..] : Created Filter initializer for bean 'formContentFilter'; order=-9900, resource=class path resource [org/springframework/boot/autoconfigure/web/servlet/WebMvcAutoConfiguration.class]
[main] [..] : Created Filter initializer for bean 'requestContextFilter'; order=-105, resource=class path resource [org/springframework/boot/autoconfigure/web/servlet/WebMvcAutoConfiguration$WebMvcAutoConfigurationAdapter.class]
[main] [..] : Mapping filters: characterEncodingFilter urls=[/*], hiddenHttpMethodFilter urls=[/*], formContentFilter urls=[/*], requestContextFilter urls=[/*]
[main] [..] : Mapping servlets: dispatcherServlet urls=[/]
[main] [..] : HikariPool-1 - Starting...
[main] [..] : HikariPool-1 - Start completed.
[main] [..] : HHH000204: Processing PersistenceUnitInfo [
name: default
...]
[main] [..] : HHH000412: Hibernate Core {5.3.7.Final} (3)
[main] [..] : HHH000206: hibernate.properties not found
[main] [..] : HCANN000001: Hibernate Commons Annotations {5.0.4.Final}
[main] [..] : HHH000400: Using dialect: org.hibernate.dialect.H2Dialect
[main] [..] : HHH000476: Executing import script 'org.hibernate.tool.schema.internal.exec.ScriptSourceInputNonExistentImpl@1ef93e01'
[main] [..] : Initialized JPA EntityManagerFactory for persistence unit 'default'
[main] [..] : Mapped [/**/favicon.ico] onto ResourceHttpRequestHandler [class path resource [META-INF/resources/], class path resource [resources/], class path resource [static/], class path resource [public/], ServletContext resource [/], class path resource []]
[main] [..] : Patterns [/**/favicon.ico] in 'faviconHandlerMapping'
[main] [..] : Initializing ExecutorService 'applicationTaskExecutor'
[main] [..] : ControllerAdvice beans: 0 @ModelAttribute, 0 @InitBinder, 1 RequestBodyAdvice, 1 ResponseBodyAdvice
[main] [..] : spring.jpa.open-in-view is enabled by default. Therefore, database queries may be performed during view rendering. Explicitly configure spring.jpa.open-in-view to disable this warning
[main] [..] :
c.m.a.t.t.c.TrainerController: (4)
{GET /trainers/}: getAllTrainers()
{GET /trainers/{name}}: getTrainer(String)
[main] [..] :
o.s.b.a.w.s.e.BasicErrorController:
{ /error, produces [text/html]}: errorHtml(HttpServletRequest,HttpServletResponse)
{ /error}: error(HttpServletRequest)
[main] [..] : 4 mappings in 'requestMappingHandlerMapping'
[main] [..] : Detected 0 mappings in 'beanNameHandlerMapping'
[main] [..] : Mapped [/webjars/**] onto ResourceHttpRequestHandler ["classpath:/META-INF/resources/webjars/"]
[main] [..] : Mapped [/**] onto ResourceHttpRequestHandler ["classpath:/META-INF/resources/", "classpath:/resources/", "classpath:/static/", "classpath:/public/", "/"]
[main] [..] : Patterns [/webjars/**, /**] in 'resourceHandlerMapping'
[main] [..] : ControllerAdvice beans: 0 @ExceptionHandler, 1 ResponseBodyAdvice
[main] [..] : Tomcat started on port(s): 8081 (http) with context path ''
[main] [..] : Started TrainerApi in 3.622 seconds (JVM running for 4.512)
1 | Wao! |
2 | On voit que un Tomcat est démarré, comme la dernière fois.
Mais cette fois-ci, il utilise bien le port 8081 comme demandé dans le fichier application.properties |
3 | Le nom Hibernate vous dit quelque chose? spring-data utilise hibernate comme implémentation de la norme JPA ! |
4 | On voit également nos controlleurs ! |
On peut maintenant tester les URLs suivantes:
8.5. Le test d’intégration
Comme pour le TP précédent, nous allons compléter nos développements avec un test d’intégration.
Créez le test suivant:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
class TrainerControllerIntegrationTest {
@LocalServerPort
private int port;
@Autowired
private TestRestTemplate restTemplate;
@Autowired
private TrainerController controller;
@Test
void trainerController_shouldBeInstanciated(){
assertNotNull(controller);
}
@Test
void getTrainer_withNameAsh_shouldReturnAsh() {
var ash = this.restTemplate.getForObject("http://localhost:" + port + "/trainers/Ash", Trainer.class);
assertNotNull(ash);
assertEquals("Ash", ash.getName());
assertEquals(1, ash.getTeam().size());
assertEquals(25, ash.getTeam().get(0).getPokemonTypeId());
assertEquals(18, ash.getTeam().get(0).getLevel());
}
@Test
void getAllTrainers_shouldReturnAshAndMisty() {
var trainers = this.restTemplate.getForObject("http://localhost:" + port + "/trainers/", Trainer[].class);
assertNotNull(trainers);
assertEquals(2, trainers.length);
assertEquals("Ash", trainers[0].getName());
assertEquals("Misty", trainers[1].getName());
}
}
9. Utilisation d’une base de données PostgreSQL dans un container docker
Démarrez une base de données PG avec Docker :
docker container run \
-p 6543:5432 \ (1)
-e POSTGRES_PASSWORD=mysecretpassword \ (2)
postgres (3)
1 | On mappe le port 6543 de la machine locale vers le port 5432 du container |
2 | on utilise un mot de passe sécurisé |
3 | on utilise l’image docker postgres officielle. |
9.1. Configuration pour spring-boot
Nous allons utiliser votre base de données nouvellement créée pour votre application !
Modifiez votre pom.xml
:
-
Ajoutez une dépendance à
postgresql
(qui contiendra le driver JDBC postgresql) -
On positionne cette dépendance en scope
runtime
, car ce driver n’est nécessaire qu’à l’exécution
1
2
3
4
5
6
7
8
9
10
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.postgresql</groupId>
<artifactId>postgresql</artifactId>
<scope>runtime</scope>
</dependency>
Modifiez votre fichier application.properties
pour y renseigner les informations de connexion à votre base de données :
1
2
3
4
5
6
7
8
9
10
# utilisation de vos parametres de connexion (1)
spring.datasource.url=jdbc:postgresql://localhost:6543/postgres
spring.datasource.username=postgres
spring.datasource.password=mysecretpassword
# personnalisation de hibernate (2)
spring.jpa.hibernate.ddl-auto=update
# personnalisation du pool de connexions (3)
spring.datasource.hikari.maximum-pool-size=1
1 | Renseignez les paramètre de connexion à votre base de donnée (remplacez les valeurs de mon exemple) |
2 | L’utilisation du paramètre spring.jpa.hibernate.ddl-auto permet à hibernate de générer le schéma de base de données au démarrage de l’application. |
3 | par défault, spring-boot utilise le pool de connexion HikariCP pour gérer les connexions à la base de données. Comme le nombre de connexions est limité dans notre environnement, nous précisions que la taille maximale du pool est 1. |
Dans le fichier src/test/resources/application.properties
, forcez les tests à utiliser la base de données h2 avec les properties suivantes :
.src/test/resources/application.properties
spring.datasource.url=jdbc:h2:mem:test
Pour rappel, la liste des propriétés acceptées par spring-boot peut se trouver dans leur documentation.
Le paramètre spring.jpa.hibernate.ddl-auto
peut prendre les valeurs suivantes :
-
create : le schéma est créé au démarrage de l’application, toutes les données existantes sont écrasées
-
create-drop : le schéma est créé au démarrage de l’application, puis supprimé à son extinction (utile en développement)
-
update : le schéma de la base de données est mis à jour si nécessaire, les données ne sont pas impactées
-
validate : le schéma de la base de données est vérifié au démarrage
Dans IntelliJ, vous pouvez également vous connecter à votre base de données, utilisez le plugin Database Tools & SQL .
|
10. L’adapter Mongodb
Maintenant que le travail pour JPA est terminé, on implémente la connexion à la base de données MongoDB !
10.1. L’ajout de la dépendance spring-boot-data-mongodb
Ajoutez la dépendance spring-boot-starter-data-mongodb
dans votre pom.xml
10.2. Les classes de documents
Dans un package fr.univ_lille.alom.trainers.mongo
, créez les classes de documents suivantes :
-
TrainerDocument
: correspond à l’objet du domaineTrainer
, et contient les mêmes attributs -
PokemonTeamMemberDocument
: correspond à l’objet du domainPokemonTeamMember
, et contient les mêmes attributs
Ajoutez sur la classe TrainerDocument
l’annotation @Document
pour indiquer qu’il s’agit d’une classe de document MongoDb.
10.3. L’interface du repository MongoDB
Créez une interface de repository Mongo nommée TrainerMongoRepository
, ajoutez les méthodes similaires à ce qui avait été fait avec le TrainerEntityJpaRepository
.
10.4. L’adapter MongoDb
Comme ça a été fait pour faire le lien entre TrainerPort
et TrainerEntityJpaRepository
, implémentez dans le package fr.univ_lille.alom.trainers.mongo
une classe TrainerMongoAdapter
qui implémente TrainerPort
.
Cette classe devra :
-
recevoir en injection de dépendance l’interface
TrainerMongoRepository
-
implémenter les méthodes de
TrainerPort
-
transformer les instances de
Trainer
enTrainerDocument
et inversement là où c’est nécessaire
la transformation peut aussi être faite dans une méthode ou une classe consacrée, soyez créatifs. |
11. Utilisation d’une base de données MongoDB dans un container docker
Démarrez une base de données PG avec Docker :
docker container run \
-p 38128:27017 \ (1)
-e MONGO_INITDB_ROOT_USERNAME=root \ (2)
-e MONGO_INITDB_ROOT_PASSWORD=monpasswordsupersecret \ (2)
mongo (3)
1 | On mappe le port 38128 de la machine locale vers le port 27017 du container |
2 | on utilise un user et un mot de passe sécurisé |
3 | on utilise l’image docker mongo officielle. |
12. Configuration de Spring Boot pour MongoDb
Configurez les properties MongoDb pour Spring Boot
1
2
3
4
5
6
# utilisation de vos parametres de connexion (1)
spring.data.mongodb.host=
spring.data.mongodb.port=
spring.data.mongodb.database=admin
spring.data.mongodb.username=
spring.data.mongodb.password=
Ajoutez dans le package fr.univ_lille.alom.trainers.mongo
une classe MongoDbConfiguration
, qui sera annotée @Configuration
et @EnableMongoRepositories
.
Démarrez votre application, et observez ce qu’il se passe (ça ne doit pas démarrer).
On utilisera plus tard (dans 2 ou 3 séances) des profils Spring pour pouvoir choisir quelle implémentation utiliser, JPA ou Mongo, en attendant, supprimez l’annotation @Component
du TrainerJpaAdapter
pour utiliser la version MongoDb. Vous devez également supprimer les properties liées à JPA.
Créez un fichier application-jpa.properties
et déplacez-les properties JPA dedans :
1
2
3
4
5
6
7
8
9
10
# utilisation de vos parametres de connexion (1)
spring.datasource.url=jdbc:postgresql://localhost:6543/postgres
spring.datasource.username=postgres
spring.datasource.password=mysecretpassword
# personnalisation de hibernate (2)
spring.jpa.hibernate.ddl-auto=update
# personnalisation du pool de connexions (3)
spring.datasource.hikari.maximum-pool-size=1
Ajoutez également ces paramètres d’annotation dans votre classe TrainerApiApplication
:
@SpringBootApplication(exclude = {
DataSourceAutoConfiguration.class,
DataSourceTransactionManagerAutoConfiguration.class,
HibernateJpaAutoConfiguration.class
})
public class TrainerApiApplication {}
Si vous avez tout bien implémenté, on est proche d’une architecture hexagonale ! |
13. Pour aller plus loin
-
Implémentez la création et la mise à jour d’un
Trainer
(route en POST/PUT) + Tests unitaires et tests d’intégration
POST /trainers/ { "name": "Bug Catcher", "team": [ {"pokemonTypeId": 13, "level": 6}, {"pokemonTypeId": 10, "level": 6} ] }
-
Implémentez la suppression d’un
Trainer
(route en DELETE) + Tests unitaires et tests d’intégration
DELETE /trainers/Bug%20Catcher