L’article montre comment construire une application Spring Boot exposant un service REST en étant guidé par un test d’intégration.
On va développer un service simple de mise à jour et de recherche de personnes. La démarche TDD est applicable.
Spring Boot permet d’utiliser les versions des spécifications de Java EE 7, à savoir JPA 2.1 (hibernate provider), JAX-RS 2 (jersey provider) et Bean Validation.
Le code source complet est disponible dans Github
Configuration maven
Ajout des dépendances Spring Boot starter web, jersey, data-jpa et test.
Ajout du Spring Boot maven plugin pour gérer le packaging et l’exécution « in-place ».
<parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>1.2.3.RELEASE</version> </parent> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-jersey</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-jpa</artifactId> </dependency> <dependency> <groupId>com.h2database</groupId> <artifactId>h2</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> <dependency> <groupId>org.assertj</groupId> <artifactId>assertj-core</artifactId> <version>3.0.0</version> <scope>test</scope> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> </plugins> </build>
Configuration de l’application REST
Mise en place de la configuration jersey et du bootstrap de l’application.
@SpringBootApplication public class RestApplication { @Bean public ResourceConfig jerseyConfig() { ResourceConfig resourceConfig = new ResourceConfig(); resourceConfig.property(ServerProperties.BV_SEND_ERROR_IN_RESPONSE, true); resourceConfig.packages(RestApplication.class.getPackage().getName()); return resourceConfig; } public static void main(String[] args) { SpringApplication.run(RestApplication.class, args); } }
Test d’intégration du service REST
Au lancement du test, l’application est servie par un Tomcat sur un port aléatoire non utilisé.
Spring Data JPA exporte le schéma SQL dans la base de données mémoire H2.
Il n’y a plus qu’à écrire des tests basés sur des clients REST.
package com.giovanetti; | |
import org.junit.Before; | |
import org.junit.Test; | |
import org.junit.runner.RunWith; | |
import org.springframework.beans.factory.annotation.Value; | |
import org.springframework.boot.test.IntegrationTest; | |
import org.springframework.boot.test.SpringApplicationConfiguration; | |
import org.springframework.boot.test.TestRestTemplate; | |
import org.springframework.http.HttpStatus; | |
import org.springframework.http.ResponseEntity; | |
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; | |
import org.springframework.test.context.web.WebAppConfiguration; | |
import org.springframework.web.client.RestTemplate; | |
import javax.inject.Inject; | |
import javax.ws.rs.core.UriBuilder; | |
import java.util.ArrayList; | |
import java.util.Arrays; | |
import java.util.List; | |
import java.util.Map; | |
import static org.assertj.core.api.Assertions.assertThat; | |
import static org.assertj.core.api.Assertions.tuple; | |
@RunWith(SpringJUnit4ClassRunner.class) | |
@SpringApplicationConfiguration(classes = RestApplication.class) | |
@WebAppConfiguration | |
@IntegrationTest({"server.port=0"}) //random unassigned port | |
public class PersonneServiceIT { | |
@Value("${local.server.port}") //actual random port | |
private int port; | |
@Inject | |
private PersonneRepository personneRepository; | |
private String baseUrl = "http://localhost:"; | |
private RestTemplate template; | |
@Before | |
public void setUp() { | |
this.baseUrl = "http://localhost:" + port; | |
template = new TestRestTemplate(); | |
personneRepository.deleteAll(); | |
personneRepository.save(new Personne("prenom1","nom1")); | |
personneRepository.save(new Personne("prenom2","nom2")); | |
} | |
@Test | |
public void findByNom() { | |
List<Personne> response = Arrays.asList(template.getForObject(UriBuilder. | |
fromUri(baseUrl).path("/personnes/search").queryParam("nom", "nom1").build(), Personne[].class)); | |
assertThat(response).hasSize(1).extracting("nom", "prenom") | |
.contains(tuple("nom1", "prenom1")); | |
} | |
@Test | |
public void findByNom_MinSizeViolation() { | |
ResponseEntity<ArrayList> response = template.getForEntity(UriBuilder. | |
fromUri(baseUrl).path("/personnes/search").queryParam("nom", "x").build(), ArrayList.class); | |
assertThat(response.getStatusCode()).isEqualTo(HttpStatus.BAD_REQUEST); | |
checkSizeViolation(response.getBody()); | |
} | |
@Test | |
public void postPersonne() { | |
ResponseEntity response = template.postForEntity(UriBuilder. | |
fromUri(baseUrl).path("/personnes").build(), new Personne("prenom3", "nom3"), null); | |
assertThat(response.getStatusCode()).isEqualTo(HttpStatus.CREATED); | |
assertThat(personneRepository.findAll()).hasSize(3) | |
.extracting("nom", "prenom") | |
.contains(tuple("nom3", "prenom3")); | |
} | |
@Test | |
public void postPersonne_MinSizeViolation() { | |
ResponseEntity<ArrayList> response = template.postForEntity(UriBuilder. | |
fromUri(baseUrl).path("/personnes").build(), new Personne("prenom3", "x"), ArrayList.class); | |
assertThat(response.getStatusCode()).isEqualTo(HttpStatus.BAD_REQUEST); | |
checkSizeViolation(response.getBody()); | |
} | |
private void checkSizeViolation(ArrayList<Map> body) { | |
assertThat(body).hasSize(1).hasOnlyElementsOfType(Map.class); | |
assertThat(body.get(0)).containsEntry("messageTemplate", | |
"{javax.validation.constraints.Size.message}"); | |
} | |
} |
Entity Personne JPA
@Entity public class Personne { private Personne() {} public Personne(String prenom, String nom) { this.prenom = prenom; this.nom = nom; } @Id @GeneratedValue(strategy= GenerationType.AUTO) @JsonIgnore private long id; private String prenom; @Size(min=2) private String nom; public String getPrenom() { return prenom; } public String getNom() { return nom; } }
Repository Personne Spring Data JPA
Rien de plus à écrire dans le cas d’un CRUD.
public interface PersonneRepository extends CrudRepository<Personne,Long> { List<Personne> findPersonneByNom(String nom); }
Service REST
A noter que JAX-RS 2 s’intègre à Bean Validation de manière à déclencher les validations automatiquement lors des appels REST.
@Path("/personnes") @Produces({MediaType.APPLICATION_JSON}) public class PersonneService { @Inject private PersonneRepository personneRepository; @GET @Path("/search") public Iterable<Personne> findPersonnesByNom(@Size(min=2) @QueryParam("nom") String nom) { return personneRepository.findPersonneByNom(nom); } @POST public Response savePersonne(@Valid Personne personne) { personneRepository.save(personne); return Response.status(Response.Status.CREATED).build(); } }