Vous êtes sur la page 1sur 13

Tester une API REST Spring MVC

avec le Spring TestContext Framework

Par Nordwin Hoff

- Soat

Date de publication : 24 octobre 2016

Ce tutoriel montre comment utiliser les outils fournis par le Spring TestContext Framework
et jUnit pour tester une API REST Spring MVC.
Pour ragir au contenu de cet article, un espace de dialogue vous est propos sur le forum
Commentez.

Tester une API REST Spring MVC avec le Spring TestContext Framework par Nordwin Hoff - Soat

I - Introduction..............................................................................................................................................................3
II - Notre cas de test, un CRUD................................................................................................................................. 3
III - Les tests d'intgration........................................................................................................................................... 7
III-A - Notre premier test........................................................................................................................................ 7
III-B - a se complique...................................................................................................................................... 8
III-C - Encore un peu plus................................................................................................................................ 9
III-D - Les TestExecutionListener......................................................................................................................... 11
III-E - Un mot sur les transactions.......................................................................................................................13
IV - Le mot de la fin.................................................................................................................................................. 13
V - Conclusion........................................................................................................................................................... 13

-2Copyright 2016 Nordwin HOFF. Aucune reproduction, mme partielle, ne peut tre faite de ce site et de l'ensemble de son contenu : textes, documents, images, etc. sans l'autorisation
expresse de l'auteur. Sinon vous encourez selon la loi jusqu' trois ans de prison et jusqu' 300 000 de dommages et intrts.
http://soat.developpez.com/tutoriels/spring/tester-api-rest-testcontext/

Tester une API REST Spring MVC avec le Spring TestContext Framework par Nordwin Hoff - Soat

I - Introduction
Avec l'adoption croissante des architectures SPA-REST (Single Page Application - Representational State Transfer)
dope par les frameworks populaires comme AngularJS pour la partie client et Node.js pour la partie serveur, la
ncessit d'assurer la robustesse de ces applications est plus forte que jamais. On observe une volution des
technologies plus anciennes telles que Java avec notamment le trs populaire Spring pour s'adapter ces nouveaux
besoins. Et comme, ici, nous faisons du Java, nous allons garantir cette robustesse grce la mise en place de tests
d'intgration. Par tests d'intgration, il faut comprendre un ensemble de tests qui vrifient le bon fonctionnement du
webservice au travers de toutes les couches, puisqu'on ne mock aucun composant du systme lui-mme. La finalit
tant de les intgrer un systme d'intgration continue via des outils comme Maven, pour garantir l'intgrit des
dveloppements.
La premire tape pour garantir une bonne qualit de code est de mettre en place des tests unitaires. Ici mme, sur
ce blog, nous avons de trs bons articles qui traitent le sujet :

10 trucs pour rater ses tests unitaires ;


DbUnit migration et tests.

Ces incontournables font trs souvent partie de la DoD (Definition of Done) dans le fonctionnement agile et sont
mme au cur de la mthodologie TDD (Tests Driven Development). Cependant, on peut rapidement se retrouver
face des difficults d'implmentation (code legacy aux dpendances fortement couples, dlais tendus, etc.) et il
peut alors tre intressant de les complter avec des tests de porte plus large.
Les tests d'intgration vont nous permettre, non plus de tester une rgle de gestion de manire unitaire qui pourrait
correspondre une mthode seule, mais plutt un service en entier. Ce dernier peut tre constitu de nombreuses
rgles de gestion et donc mcaniquement couvrir une plus grande partie du code. On pourra dtecter plus facilement
les impacts indirects de modifications. Par exemple, une mthode appele dans deux services A et B peut, suite
une volution, crer une rgression dans A si le dveloppement et les TU sont focaliss sur le service B. Les TU
passeront au vert alors que les tests d'intgration peuvent tomber en erreur.
Ici nous allons utiliser les outils fournis par le Spring TestContext Framework et jUnit dont la dpendance Maven est :
1. <dependency>
2.
<groupId>org.springframework</groupId>
3.
<artifactId>spring-test</artifactId>
4.
<version>4.2.3</version>
5.
<scope>test</scope>
6. </dependency>

Il
fournit
principalement
les
annotations
@WebAppConfiguration,
@ContextConfiguration
et
@TestExecutionListeners ainsi que les classes et interfaces AbstractTestExecutionListener, WebApplicationContext,
MockMvc, MockMvcBuilders, SpringJUnit4ClassRunner.
Le code de cet article est disponible tape par tape sur le github de SOAT. Le code complet se situe sur le master.

II - Notre cas de test, un CRUD


Afin de construire nos tests d'intgration, nous allons prendre l'application suivante :
une API REST sur Json qui permet de crer, lire, supprimer ou mettre jour des commandes constitues de liste de
produits. On ajoute une gestion des commandes non trouves ou contenant plus de produits que la limite impose,
arbitrairement 2. Toutes les donnes sont enregistres dans une base de donnes MongoDB. L'accs cette BDD
se fait au travers des modules spring data et spring data mongo.
L'API lie l'URL /order est construite selon le modle classique en couches avec :

-3Copyright 2016 Nordwin HOFF. Aucune reproduction, mme partielle, ne peut tre faite de ce site et de l'ensemble de son contenu : textes, documents, images, etc. sans l'autorisation
expresse de l'auteur. Sinon vous encourez selon la loi jusqu' trois ans de prison et jusqu' 300 000 de dommages et intrts.
http://soat.developpez.com/tutoriels/spring/tester-api-rest-testcontext/

Tester une API REST Spring MVC avec le Spring TestContext Framework par Nordwin Hoff - Soat

une couche de webservice via le controller OrderWebService :


1. package fr.soat.java.webservices;
2.
3. import fr.soat.java.dto.OrderDto;
4. import fr.soat.java.dto.ProductDto;
5. import fr.soat.java.exceptions.OrderNotFoundException;
6. import fr.soat.java.exceptions.TooManyProductsException;
7. import fr.soat.java.payload.Order;
8. import fr.soat.java.payload.Product;
9. import fr.soat.java.payload.wrappers.ResponseWrapper;
10. import fr.soat.java.services.IOrderService;
11. import org.springframework.beans.factory.annotation.Autowired;
12. import org.springframework.web.bind.annotation.*;
13.
14. @RestController
15. @RequestMapping(value= "/order")
16. public class OrderWebService {
17.
18.
@Autowired
19.
private IOrderService orderService;
20.
21.
@RequestMapping(value = "/{orderId}", method = RequestMethod.GET)
22.
public ResponseWrapper<Order> getOrder(
23.
@PathVariable String orderId) throws OrderNotFoundException{
24.
ResponseWrapper<Order> response = new ResponseWrapper<>();
25.
response.setData(fromOrderDto(orderService.getOrder(orderId)));
26.
return response;
27.
}
28.
29.
@RequestMapping(method = RequestMethod.POST)
30.
public ResponseWrapper<Order> saveOrder(
31.
@RequestBody Order order) throws TooManyProductsException {
32.
ResponseWrapper<Order> response = new ResponseWrapper<>();
33.
response.setData(fromOrderDto(orderService.saveOrder(toOrderDto(order))));
34.
return response;
35.
}
36.
37.
@RequestMapping(value = "/{orderId}", method = RequestMethod.PUT)
38.
public ResponseWrapper<Order> updateOrder(
39.
@PathVariable String orderId,
40.
@RequestBody Order order) throws TooManyProductsException {
41.
order.setId(orderId);
42.
ResponseWrapper<Order> response = new ResponseWrapper<>();
43.
response.setData(fromOrderDto(orderService.updateOrder(toOrderDto(order))));
44.
return response;
45.
}
46.
47.
@RequestMapping(value = "/{orderId}", method = RequestMethod.DELETE)
48.
public ResponseWrapper<Order> deleteOrder(
49.
@PathVariable String orderId) {
50.
ResponseWrapper<Order> response = new ResponseWrapper<>();
51.
orderService.deleteOrder(orderId);
52.
response.setData(null);
53.
return response;
54.
}
55.
56.
private Order fromOrderDto(OrderDto dto) {
57.
Order order = new Order();
58.
order.setCreationDate(dto.getCreationDate());
59.
order.setId(dto.getId());
60.
order.setModificationDate(dto.getModificationDate());
61.
dto.getProducts().forEach((productDto ->
order.getProducts().add(fromProductDto(productDto))));
62.
return order;
63.
}
64.
65.
private Product fromProductDto(ProductDto dto) {
66.
Product product = new Product();
67.
product.setName(dto.getName());
68.
return product;

-4Copyright 2016 Nordwin HOFF. Aucune reproduction, mme partielle, ne peut tre faite de ce site et de l'ensemble de son contenu : textes, documents, images, etc. sans l'autorisation
expresse de l'auteur. Sinon vous encourez selon la loi jusqu' trois ans de prison et jusqu' 300 000 de dommages et intrts.
http://soat.developpez.com/tutoriels/spring/tester-api-rest-testcontext/

Tester une API REST Spring MVC avec le Spring TestContext Framework par Nordwin Hoff - Soat

69.
}
70.
71.
private OrderDto toOrderDto(Order order) {
72.
OrderDto dto = new OrderDto();
73.
dto.setCreationDate(order.getCreationDate());
74.
dto.setId(order.getId());
75.
dto.setModificationDate(order.getModificationDate());
76.
order.getProducts().forEach((product ->
dto.getProducts().add(toProductDto(product))));
77.
return dto;
78.
}
79.
80.
private ProductDto toProductDto(Product product) {
81.
ProductDto dto = new ProductDto();
82.
dto.setName(product.getName());
83.
return dto;
84.
}
85. }

une couche service via la classe OrderService :


1. package fr.soat.java.services.impl;
2.
3. import fr.soat.java.dao.IOrderRepository;
4. import fr.soat.java.dto.OrderDto;
5. import fr.soat.java.dto.ProductDto;
6. import fr.soat.java.exceptions.OrderNotFoundException;
7. import fr.soat.java.exceptions.TooManyProductsException;
8. import fr.soat.java.model.OrderEntity;
9. import fr.soat.java.model.ProductEntity;
10. import fr.soat.java.services.IOrderService;
11. import org.springframework.beans.factory.annotation.Autowired;
12. import org.springframework.stereotype.Service;
13.
14. import java.time.Instant;
15.
16. @Service
17. public class OrderService implements IOrderService {
18.
19.
@Autowired
20.
private IOrderRepository orderRepository;
21.
22.
private static final int NB_MAX_PRODUCTS = 2;
23.
24.
@Override
25.
public OrderDto saveOrder(OrderDto orderDto) throws TooManyProductsException {
26.
if (orderDto.getProducts().size() > NB_MAX_PRODUCTS) {
27.
throw new TooManyProductsException();
28.
}
29.
orderDto.setCreationDate(Instant.now().toString());
30.
return fromOrderEntity(orderRepository.save(toOrderEntity(orderDto)));
31.
}
32.
33.
@Override
34.
public OrderDto updateOrder(OrderDto orderDto) throws TooManyProductsException {
35.
orderDto.setModificationDate(Instant.now().toString());
36.
return saveOrder(orderDto);
37.
}
38.
39.
@Override
40.
public OrderDto getOrder(String orderId) throws OrderNotFoundException{
41.
OrderEntity found = orderRepository.findOne(orderId);
42.
if(found == null){
43.
throw new OrderNotFoundException();
44.
}
45.
return fromOrderEntity(found);
46.
}
47.
48.
@Override
49.
public void deleteOrder(String orderId) {

-5Copyright 2016 Nordwin HOFF. Aucune reproduction, mme partielle, ne peut tre faite de ce site et de l'ensemble de son contenu : textes, documents, images, etc. sans l'autorisation
expresse de l'auteur. Sinon vous encourez selon la loi jusqu' trois ans de prison et jusqu' 300 000 de dommages et intrts.
http://soat.developpez.com/tutoriels/spring/tester-api-rest-testcontext/

Tester une API REST Spring MVC avec le Spring TestContext Framework par Nordwin Hoff - Soat

50.
orderRepository.delete(orderId);
51.
}
52.
53.
private OrderDto fromOrderEntity(OrderEntity entity) {
54.
OrderDto order = new OrderDto();
55.
order.setCreationDate(entity.getCreationDate());
56.
order.setId(entity.get_id());
57.
order.setModificationDate(entity.getModificationDate());
58.
entity.getProductList().forEach((productEntity ->
order.getProducts().add(fromProductEntity(productEntity))));
59.
return order;
60.
}
61.
62.
private ProductDto fromProductEntity(ProductEntity entity) {
63.
ProductDto product = new ProductDto();
64.
product.setName(entity.getName());
65.
return product;
66.
}
67.
68.
private OrderEntity toOrderEntity(OrderDto order) {
69.
OrderEntity entity = new OrderEntity();
70.
entity.setCreationDate(order.getCreationDate());
71.
entity.set_id(order.getId());
72.
entity.setModificationDate(order.getModificationDate());
73.
order.getProducts().forEach((product ->
entity.getProductList().add(toProductEntity(product))));
74.
return entity;
75.
}
76.
77.
private ProductEntity toProductEntity(ProductDto product) {
78.
ProductEntity entity = new ProductEntity();
79.
entity.setName(product.getName());
80.
return entity;
81.
}
82. }

une couche dao via le repository spring data IorderRepository :


1.
2.
3.
4.
5.
6.
7.
8.
9.

package fr.soat.java.dao;
import fr.soat.java.model.OrderEntity;
import org.springframework.data.mongodb.repository.MongoRepository;
import org.springframework.stereotype.Repository;
@Repository
public interface IOrderRepository extends MongoRepository<OrderEntity, String> {
}

On enverra les payloads au format suivant :


1. {
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13. }

id : "orderId",
creationDate : "20151204",
modificationDate: "20151231",
products: [
{
name: "tv"
},
{
name: "phone"
}
]

et les rponses au mme format dans un wrapper :


1. {
2.

status : "OK",

-6Copyright 2016 Nordwin HOFF. Aucune reproduction, mme partielle, ne peut tre faite de ce site et de l'ensemble de son contenu : textes, documents, images, etc. sans l'autorisation
expresse de l'auteur. Sinon vous encourez selon la loi jusqu' trois ans de prison et jusqu' 300 000 de dommages et intrts.
http://soat.developpez.com/tutoriels/spring/tester-api-rest-testcontext/

Tester une API REST Spring MVC avec le Spring TestContext Framework par Nordwin Hoff - Soat

3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16. }

data : {
id : "orderId",
creationDate : "20151204",
modificationDate: "20151231",
products: [
{
name: "tv"
},
{
name: "phone"
}
]
}

L'article ne portant pas sur les dtails d'implmentation, j'invite le lecteur explorer les
documentations associes en cas de besoin.

III - Les tests d'intgration


III-A - Notre premier test
Commenons par crer notre classe OrderWebServiceTI avec les quelques annotations de base.
On a ici le test d'intgration le plus basique possible. On retrouve les annotations classiques des TU avec jUnit
@Before, @RunWith et @Test.
1.
2.
3.
4.

@RunWith(SpringJUnit4ClassRunner.class)
@WebAppConfiguration
@ContextConfiguration("/applicationContext.xml")
public class OrderWebServiceTest {

SpringJUnit4ClassRunner permet l'utilisation de spring-test pour l'excution du test, @WebAppConfiguration permet


de prcharger une configuration propre aux applications web (a tombe bien, on fait du Spring MVC !) et
@ContextConfiguration permet d'indiquer la configuration du contexte Spring. Ici, on veut charger toutes les
dpendances de l'application (dao, service, etc.).
Comme prconis par la documentation officielle, on charge la mock servlet partir du webcontext fourni par Spring
dans le before du test :
1. @Before
2. public void setUp() throws Exception {
3.
this.mockMvc = MockMvcBuilders.webAppContextSetup(this.wac).build();
4. }

Et enfin le test en lui-mme :


1. @Test
2. public void testSaveOrder() throws Exception {
3.
String payload = "{ \"products\": [{ \"name\": \"Mon produit\" }]}";
4.
MockHttpServletRequestBuilder
req = post(SERVICE_URI).contentType(MediaType.APPLICATION_JSON)
5.
.accept(MediaType.APPLICATION_JSON_UTF8)
6.
.content(payload);
7.
this.mockMvc.perform(req).andExpect(status().isOk());
8. }

Nous sommes dans le cas le plus simple. Il n'y a rien dans la base MongoDB. On fait donc une cration de commande
via le webservice POST /order. Notre requte au format json contient un produit. Elle est construite grce la mthode
post (importe de manire statique) de la classe MockMvcRequestBuilders. Cette classe permet de construire notre

-7Copyright 2016 Nordwin HOFF. Aucune reproduction, mme partielle, ne peut tre faite de ce site et de l'ensemble de son contenu : textes, documents, images, etc. sans l'autorisation
expresse de l'auteur. Sinon vous encourez selon la loi jusqu' trois ans de prison et jusqu' 300 000 de dommages et intrts.
http://soat.developpez.com/tutoriels/spring/tester-api-rest-testcontext/

Tester une API REST Spring MVC avec le Spring TestContext Framework par Nordwin Hoff - Soat

requte la demande. Une bonne partie des mthodes de cette classe renvoyant this , on peut chaner les
mthodes directement. Ici on prcise le content-type ainsi que l'accept. la place de renseigner directement les
strings adquats, on peut utiliser la classe MediaType directement fournie par Spring. Enfin, on utilise la mthode
content() pour prciser le corps de la requte (ce qui n'est bien sr pas ncessaire pour des oprations telles que
le GET, DELETE, etc.).
L'appel proprement dit se fait l'aide de la mthode perform() de l'objet mockMvc. Cette mthode renvoie un objet
sur lequel on peut chaner les mthodes. Ici on utilise andExpect() qui permet d'effectuer directement des assertions
sur la rponse. On vrifie, par exemple, que le HTTP status est 200. Cette mthode prend en paramtres des objets
de type ResultMatcher. MockMvcResultMatchers permet d'avoir accs un grand nombre d'assertions communes.
Charge au dveloppeur d'explorer toutes les possibilits.
Enfin, si on veut rcuprer le corps de la rponse de manire brute, on peut le faire grce aux mthodes andReturn()
et getResponse() qui permettent de le rcuprer sous forme binaire (getContentAsByteArray()) ou bien sous forme
de chane de caractres (getContentAsString()).
Il existe un grand nombre de mthodes complmentaires pour effectuer toutes les oprations d'assertions d'un coup,
mais je n'y ai jamais eu recours. Je prfre rcuprer la rponse brute et effectuer les assertions dans un second
temps. Cela me permet de choisir mon framework d'assertion.
Il est temps d'excuter notre test ! D'abord, on dmarre notre serveur de BDD mongo, et ensuite :
mvn clean test
On a bien nos trois tests excuts et qui se terminent sans erreur :
1.
2.
3.
4.
5.
6.
7.
8.
9.

Results: Tests run: 3, Failures: 0, Errors: 0, Skipped: 0


[INFO]
[INFO]
[INFO]
[INFO]
[INFO]
[INFO]
[INFO]

-----------------------------------------------------------------------BUILD SUCCESS
-----------------------------------------------------------------------Total time: 5.079s
Finished at: Wed Dec 02 09:57:01 CET 2015
Final Memory: 18M/221M
------------------------------------------------------------------------

Pour ceux qui aiment les logs, on peut voir qu'on a bien nos deux TU excuts et notre TI, comme en tmoignent
les logs de chargement du contexte Spring. D'ailleurs, pour les plus courageux, en mode debug, on voit bien chaque
tape du chargement du contexte de la mme manire que si l'on dployait la webapp dans un conteneur de servlet
ou serveur d'application. Le chargement du contexte prenant un peu de temps, on peut se rassurer en voyant qu'il
ne se charge qu'une seule fois par build et non une fois par test ou classe de tests.
Code disponible sur la branche step1 du repository git.

III-B - a se complique
Ajoutons un second test. Ou plutt, ajoutons deux tests qui couvriront le GET de notre CRUD. D'abord, testons le
cas o on ne trouve pas de commande en base. On cherche une commande qui aurait pour id test , qui n'existe
pas en base. On teste donc que le service renvoie bien un 404 (status().isNotFound()). L'objet req ne servant pas
grand-chose au-del de la lisibilit, on imbrique directement tous les appels de mthodes (avec une indentation
lisible s'il vous plait !).
Maintenant on aimerait tester que le GET remonte bien un objet existant en base. Comme on veut des tests
indpendants les uns des autres, on ne peut pas se baser sur le test du POST pour faire un GET juste aprs et vrifier
que la donne cre par le premier test est remonte par le second. On a donc besoin de rcuprer une DAO pour

-8Copyright 2016 Nordwin HOFF. Aucune reproduction, mme partielle, ne peut tre faite de ce site et de l'ensemble de son contenu : textes, documents, images, etc. sans l'autorisation
expresse de l'auteur. Sinon vous encourez selon la loi jusqu' trois ans de prison et jusqu' 300 000 de dommages et intrts.
http://soat.developpez.com/tutoriels/spring/tester-api-rest-testcontext/

Tester une API REST Spring MVC avec le Spring TestContext Framework par Nordwin Hoff - Soat

interagir avec la BDD. a tombe bien, on en a une dans le contexte Spring de l'application ! On peut donc l'ajouter
en tant que membre de notre classe de test, @Autowired par Spring :
1. @Autowired
2. private IOrderRepository dao;

On se cre galement un objet Order sauvegarder dans le setUp()


1. @Before
2. public void setUp() throws Exception {
3.
this.mockMvc = MockMvcBuilders.webAppContextSetup(this.wac).build();
4.
orderDataset = new OrderEntity();
5.
orderDataset = dao.save(orderDataset);
6. }

et la suppression de cet objet dans le tearDown() pour ne pas polluer la base.


1. @After
2. public void tearDown() throws Exception {
3.
dao.delete(orderDataset.get_id());
4. }

Notre test :
1. @Test
2. public void testGetNotFoundOrder() throws Exception {
3.
this.mockMvc.perform(get(SERVICE_URI + "/" + "test")
4.
.contentType(MediaType.APPLICATION_JSON)
5.
.accept(MediaType.APPLICATION_JSON_UTF8_VALUE)
6.
).andExpect(status().isNotFound());
7. }

On excute le tout avec Maven :


Results : Tests run: 5, Failures: 0, Errors: 0, Skipped: 0
Code disponible sur la branche step2 du repository git.

III-C - Encore un peu plus


Jusqu' maintenant, on a eu des cas trs simples qui, au-del du code HTTP, ne vrifient pas grand-chose, si ce
n'est qu'il n'y ait pas de crash des services.
tant donn que l'API de services renvoie les ressources avec lesquelles le client est en interaction (pour ceux qui
veulent le mot savant pour briller au prochain meetup, les oprations PUT et POST sont dites isomorphiques ),
on pourrait vrifier que les donnes renvoyes sont cohrentes en sortie de chaque service.
Pour ce faire, la documentation officielle prconise d'utiliser la librairie jsonpath. On peut donc ajouter notre
pom.xml :
1. <dependency>
2.
<groupId>com.jayway.jsonpath</groupId>
3.
<artifactId>json-path</artifactId>
4.
<version>${jsonpath.version}</version>
5.
<scope>test</scope>
6. </dependency>

-9Copyright 2016 Nordwin HOFF. Aucune reproduction, mme partielle, ne peut tre faite de ce site et de l'ensemble de son contenu : textes, documents, images, etc. sans l'autorisation
expresse de l'auteur. Sinon vous encourez selon la loi jusqu' trois ans de prison et jusqu' 300 000 de dommages et intrts.
http://soat.developpez.com/tutoriels/spring/tester-api-rest-testcontext/

Tester une API REST Spring MVC avec le Spring TestContext Framework par Nordwin Hoff - Soat

En fait, on n'est pas du tout oblig de l'utiliser. Personnellement, j'apprcie la facilit et la flexibilit d'utilisation grce
l'intgration avec Spring. Je vais donc montrer deux faons de traiter la rponse qui exploitent cette librairie.
Mais juste avant, regardons ce qu'on peut faire avec. La principale fonctionnalit est de pouvoir rcuprer une souspartie d'un fichier json l'aide de requtes composes d'expressions rgulires. La documentation propose une liste
d'exemples dont on retiendra ici principalement les plus simples. Le principe gnral est le suivant :
Le symbole '$' dsigne l'lment racine, toutes les requtes commencent donc avec celui-ci. Le symbole '.' permet
d'aller dans le niveau de profondeur suivant dans l'arborescence du json. Le symbole '..' permet de chercher un
lment, quel que soit son emplacement dans la hirarchie de l'lment en cours.
Un exemple avec nos commandes :
1. {
2.
3.
4.
5.
6.
7.
8.
9.
10. }

order: {
products: [{
name: "tv"
},
{
name: "phone"
}]
}

$.order.products on rcupre la liste des produits ;


$..products a le mme rsultat ;
$.order.products[0].name rcupre tv ;
$.order.products[*].name rcupre la liste ['tv','phone'] ;
$.order.products..name rcupre la mme liste.
Attention, si on ajoutait un attribut name l'objet order ayant la valeur nom de commande, la requte $..name
renverrait ['tv','phone','nom de commande'].
On peut galement utiliser des requtes qui possdent des assertions du type : je veux tous les produits dont le
nom commence par un 't'.
Ce qui a fini de me convaincre avec cette librairie, c'est galement le requteur web indiqu par la documentation,
qui permet de tester ses requtes sur son jeu de donnes en ligne.
Appliqu nos tests, on peut directement intgrer le jsonpath en paramtre de la mthode andExpect(). On va ajouter
la rcupration de l'id lors du POST pour vrifier que MongoDB a bien attribu un id notre commande et que
lorsqu'on GET une ressource par son id, c'est bien celle-ci qui est remonte par le service. En voici le test :
1. @Test
2. public void testGetOrder() throws Exception {
3.
this.mockMvc.perform(get(SERVICE_URI + "/" + orderDataset.get_id())
4.
.contentType(MediaType.APPLICATION_JSON)
5.
.accept(MediaType.APPLICATION_JSON_UTF8_VALUE)
6.
).andExpect(status().isOk())
7.
.andExpect(jsonPath("$.data.id").value(orderDataset.get_id()));
8. }

Code disponible sur la branche step3 du repository git.

- 10 Copyright 2016 Nordwin HOFF. Aucune reproduction, mme partielle, ne peut tre faite de ce site et de l'ensemble de son contenu : textes, documents, images, etc. sans l'autorisation
expresse de l'auteur. Sinon vous encourez selon la loi jusqu' trois ans de prison et jusqu' 300 000 de dommages et intrts.
http://soat.developpez.com/tutoriels/spring/tester-api-rest-testcontext/

Tester une API REST Spring MVC avec le Spring TestContext Framework par Nordwin Hoff - Soat

Une autre faon de faire consiste utiliser jsonpath pour uniquement rcuprer un POJO correspondant la
commande, ce qui donne :
1. @Test
2. public void testGetOrder() throws Exception {
3.
String jsonResponse = this.mockMvc.perform(get(SERVICE_URI + "/" + orderDataset.get_id())
4.
.contentType(MediaType.APPLICATION_JSON)
5.
.accept(MediaType.APPLICATION_JSON_UTF8_VALUE)
6.
).andExpect(status().isOk()).andReturn().getResponse().getContentAsString();
7.
Order responseOrder = JsonPath.parse(jsonResponse).read("$.data",Order.class);
8.
Assert.assertNotNull(responseOrder.getId());
9.
Assert.assertFalse(responseOrder.getId().isEmpty());
10. }

On voit qu'ici, on rcupre la rponse sous forme de String, on utilise JsonPath indpendamment de Spring MVC et
surtout, on fait des assertions directement sur les POJO.
Personnellement, je prfre cette solution, car en cas de rponse volumineuse ou cas de tests compliqus, lors des
assertions, je pourrais rutiliser du code peut-tre dj prsent dans les packages utilitaires de mon application, voire
des librairies tierces qui savent manipuler des POJO. En fait, j'enlve la contrainte du json pour traiter la donne. De
plus, si je veux utiliser une autre librairie qui manipule le json comme jackson, je peux tout fait le faire.
Code disponible sur la branche step3-2 du repository git.

III-D - Les TestExecutionListener


Nous avons donc un ensemble de tests pour nos webservices. Pour l'instant, on s'est concentr sur les tests en
tant que tels, cependant, si l'on regarde la classe dans son intgralit, on observe que tous les jeux de donnes
enregistrs dans la base de donnes sont excuts dans la fonction setUp(). Or celle-ci est excute avant chaque
mthode de test (d au @Before jUnit). On effectue donc des oprations inutiles en base. Par exemple, pour un test
de mise jour de prospect, on cre le jeu de donnes correspondant autant de fois qu'il y a de tests. Ce n'est pas
une approche trs intuitive, ni trs efficace.
Pour corriger cela, on peut envisager deux solutions. La premire est de dplacer le code du setUp() correspondant
aux diffrents cas au dbut de chaque mthode de test. Cela peut faire grossir les mthodes de tests en fonction de
la taille des jeux de donnes, on perd en lisibilit, mais on cantonne toutes les variables au test, la logique d'excution
est plus simple.
La deuxime solution est de dplacer toute la cration des jeux de donnes en base dans une mthode excute
une seule fois avant l'ensemble des tests. L'avantage est qu'on gagne en lisibilit entre les jeux de donnes et les
tests. L'inconvnient est que, pour faciliter certaines assertions, on peut tre oblig de garder une rfrence vers
chaque objet en base en tant qu'attribut de la classe de test.
Libre chacun de choisir, mais nous allons continuer avec la deuxime solution, pour explorer un peu plus les
possibilits de Spring test
Tout d'abord, on pourrait avoir envie d'utiliser l'annotation @BeforeClass fournie par jUnit, mais celle-ci prsente
une limitation, notamment lorsqu'on travaille avec un framework qui instancie les objets pour nous, comme Spring.
Comme les mthodes annotes @BeforeClass doivent tre statiques, tous les objets que l'on veut rfrencer dans
la classe et qui sont initialiss cet endroit doivent tre statiques. Conceptuellement, on ne le veut pas forcment.
De plus, @Autowired ne fonctionne pas sur des attributs statiques. En fait, c'est normal, puisqu'au chargement de
la classe de test, le contexte Spring n'existe pas encore. On ne peut donc pas rcuprer la DAO paramtre dans
l'application. cet instant, elle vaudrait null.
C'est maintenant que rentrent en scne les TestExecutionListener. Comme pour tous les listeners, cela permet
d'excuter du code certains moments lors de l'excution des tests. Avant de crer notre propre listener, ils sont

- 11 Copyright 2016 Nordwin HOFF. Aucune reproduction, mme partielle, ne peut tre faite de ce site et de l'ensemble de son contenu : textes, documents, images, etc. sans l'autorisation
expresse de l'auteur. Sinon vous encourez selon la loi jusqu' trois ans de prison et jusqu' 300 000 de dommages et intrts.
http://soat.developpez.com/tutoriels/spring/tester-api-rest-testcontext/

Tester une API REST Spring MVC avec le Spring TestContext Framework par Nordwin Hoff - Soat

l ds qu'on utilise la classe SpringJUnit4ClassRunner pour lancer les tests. Par dfaut, Spring utilise les listeners
suivants dans cet ordre (extrait de la documentation officielle) :

ServletTestExecutionListener : configures Servlet API mocks for a WebApplicationContext


DependencyInjectionTestExecutionListener : provides dependency injection for the test instance
DirtiesContextTestExecutionListener : handles the @DirtiesContext annotation
TransactionalTestExecutionListener : provides transactional test execution with default rollback semantics
SqlScriptsTestExecutionListener : executes SQL scripts configured via the @Sql annotation

Dans notre cas, les trois derniers ne sont pas utiles.


Pour accder notre DAO avant les tests, on va faire de notre classe de test un listener. Pour cela, deux choses
faire :
1
2

tendre la classe AbstractTestExecutionListener qui fournit les mthodes surcharger ;


Ajouter l'annotation @TestExecutionListeners pour prciser quels listeners nous intressent. Ici la classe
de test se rfrence elle-mme et on prcise qu'on l'ajoute la liste des listeners par dfaut l'aide du flag
MergeMode.MERGE_WITH_DEFAULTS.
1. @TestExecutionListeners(listeners = OrderWebServiceTest.class, mergeMode =
TestExecutionListeners.MergeMode.MERGE_WITH_DEFAULTS)
2. public class OrderWebServiceTest extends AbstractTestExecutionListener {

Pour une version de Spring antrieure 4.1, l'enum MergeMode n'existe pas. Il faut donc
dclarer l'ensemble des listeners souhaits, car par dfaut, la liste passe l'annotation annule
et remplace la liste des listeners par dfaut.
On ajoute la surcharge de la mthode beforeTestClass() :
1. @Override
2. public void beforeTestClass(TestContext testContext) throws Exception {
3.
getOrderDataset = dao.save(new OrderEntity());
4.
updateOrderDataset = dao.save(new OrderEntity());
5.
deleteOrderDataset = dao.save(new OrderEntity());
6. }

On excute et surprise ! Une bonne vielle NPE sur la rfrence notre DAO dans la mthode beforeTestClass().
Ce n'est pas normal, on est dans un contexte Spring l me direz-vous. Sauf que si l'on regarde la
classe DependencyInjectionTestExecutionListener qui active l'injection de dpendance, on se rend compte qu'elle
n'implmente pas la mthode beforeTestClass(). On est donc oblig de rcuprer la DAO la main depuis le contexte
Spring pass en paramtre :
1. @Override
2. public void beforeTestClass(TestContext testContext) throws Exception {
3.
dao = testContext.getApplicationContext().getBean(IOrderRepository.class);
4.
getOrderDataset = dao.save(new OrderEntity());
5.
updateOrderDataset = dao.save(new OrderEntity());
6.
deleteOrderDataset = dao.save(new OrderEntity());
7. }

Maintenant nos tests s'excutent sans encombre.


Autre dtail qui peut paratre bizarre, nos objets Dataset sont dclars en static pour pouvoir garder la
rfrence cre dans le beforeTestClass() dans le @Test. L'explication vient du fonctionnement de jUnit. Ce dernier
instancie la classe de test une fois par test, alors que la mthode beforeTestClass() est excute une seule fois avant
l'excution du premier test. On est donc oblig de dclarer static ces jeux de donnes.

- 12 Copyright 2016 Nordwin HOFF. Aucune reproduction, mme partielle, ne peut tre faite de ce site et de l'ensemble de son contenu : textes, documents, images, etc. sans l'autorisation
expresse de l'auteur. Sinon vous encourez selon la loi jusqu' trois ans de prison et jusqu' 300 000 de dommages et intrts.
http://soat.developpez.com/tutoriels/spring/tester-api-rest-testcontext/

Tester une API REST Spring MVC avec le Spring TestContext Framework par Nordwin Hoff - Soat

Cela fait rflchir sur l'intrt des listeners. Ici, on ne s'en sert que pour accder un objet cr par le contexte Spring
avant l'excution des tests. La raison premire des listeners est de factoriser du code dont on aurait besoin dans
plusieurs classes de test. Une description plus pousse est disponible entre les possibilits offertes par Spring test
en comparaison jUnit dans ce post stackOverflow, le tout expliqu par l'auteur du framework Spring test.
Code disponible sur la branche step4 du repository git.

III-E - Un mot sur les transactions


Dans cet article, le cas de test communique avec une base de donnes MongoDB. Seulement, en MongoDB, il n'y
a pas de transaction. La seule faon de les grer est de mettre en place la mthode du two phase commit , qui
consiste implmenter les tats des transactions la main.
Si on avait une base de donnes SQL type Oracle la place de MongoDB, on pourrait utiliser quelques
annotations fournies par Spring test comme @Rollback, @Commit,@BeforeTransaction, @AfterTransaction ou
encore @TransactionConfiguration (obsolte au profit des deux premires depuis Spring 4.2). L'utilisation se fait
via le TransactionalTestExecutionListener et les annotations comme @Transactional, qui permettent de grer les
transactions. D'une manire gnrale, le sujet n'est pas propre aux tests et ne sera donc pas dtaill ici.

IV - Le mot de la fin
Spring nous offre ici un moyen avanc de tirer parti de ses outils lors de la mise en place de tests. On a l'avantage
de pouvoir tester des fonctionnalits indpendamment de leur implmentation, cas de figure qui peut survenir si la
dette technique est trop leve pour entreprendre un chantier de refactoring. On peut galement tester une API de
webservices sans mock et sans s'encombrer d'un conteneur de servlet ou un serveur d'application. En revanche,
on prend le risque d'augmenter le temps de build avec une phase de tests plus longue, puisque c'est l'application
relle qui est lance. De plus, une bonne matrise du fonctionnement de jUnit et Spring est requise pour comprendre
l'intrication des deux frameworks. Il faudra donc bien rflchir aux fonctionnalits tester par ce mode pour viter
l'effet usine gaz .
Pour une mise en uvre dcolle de l'excution des tests unitaire, il peut tre intressant d'exploiter le plugin maven
failsafe . Il fonctionne globalement comme surefire et fournit les goals integration-test et verify . La
description est disponible ici.

V - Conclusion
Cet article a t publi avec l'aimable autorisation de la socit Soat.
Nous tenons remercier f-leb pour sa relecture orthographique et Malick SECK pour la mise au gabarit.

- 13 Copyright 2016 Nordwin HOFF. Aucune reproduction, mme partielle, ne peut tre faite de ce site et de l'ensemble de son contenu : textes, documents, images, etc. sans l'autorisation
expresse de l'auteur. Sinon vous encourez selon la loi jusqu' trois ans de prison et jusqu' 300 000 de dommages et intrts.
http://soat.developpez.com/tutoriels/spring/tester-api-rest-testcontext/

Vous aimerez peut-être aussi