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();
  }

}