Les implémentations JAX-RS (2)
Dans l'article précédent nous avions abordé la mise en œuvre des implémentations JAX-RS Jersey et RESTEasy ainsi que celles de CXF et Restlet.
Nous nous intéresserons dans cette seconde partie aux tests unitaires. Avant d'entrer dans le détail je souhaiterais attirer votre attention sur le fait que toutes ces implémentations offrent la possibilité de faire tourner des tests dans des serveurs embarqués, ce qui tient du test d'intégration et non du test unitaire.
Voyons maintenant dans quelle mesure chaque implémentation offre la possibilité de mocker le serveur.
CXF
Grosse déception ici : CXF n'offre pas de framework de test permettant de mocker le serveur...
On est donc limité à priori à tester nos services dans le serveur embarqué Jetty...
Il est cependant possible de s'en sortir en utilisant les classes de test de Spring :
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations = { "classpath:testMockContext.xml" })
public class TestMockService {
@Autowired
private ServletTransportFactory servletTransportFactory;
@Test
public void testGet1() throws IOException, ServletException {
MockHttpServletRequest request = new MockHttpServletRequest("GET", "/someResource/1");
request.setContent(new byte[]{});
MockHttpServletResponse response = new MockHttpServletResponse();
ServletDestination destinationForPath = servletTransportFactory.getDestinationForPath("/");
destinationForPath.invoke(new MockServletContext(), request, response);
assertEquals(HttpServletResponse.SC_OK, response.getStatus());
assertEquals("{\"someBean\":[{\"someAttribute\":\"someValue\"}]}",
response.getContentAsString());
}
}
MockHttpServletRequest, MockHttpServletResponse et MockServletContext sont des classes de spring-test. Afin d'avoir accès à la ServletTransportFactory il vous faudra importer le fichier cxf-servlet.xml dans votre configuration Spring de test :
<import resource="classpath:META-INF/cxf/cxf-servlet.xml"/>
Enfin, l'appel à setContent, ligne 10, est nécessaire pour ne pas provoquer de NullPointerException de la part de CXF qui effectue un appel à getInputStream sur la requête sans vérifier si la valeur de retour de la méthode est null ou non.
Jersey
Jersey met à disposition du développeur un framework de test plutôt complet. La documentation est malheureusement un peu avare d'exemples et si l'utilisation de Spring avec les serveurs embarqués ne pose pas de problèmes particuliers, c'est une autre histoire dès lors que l'on souhaite utiliser le module jersey-test-framework-inmemory.
Afin d'utiliser le framework de test de Jersey, vos classes de test doivent en principe hériter de JerseyTest qui fait tout le travail d'initialisation et de configuration dans son constructeur qui n'a du coup pas accès aux beans Spring.
Afin d'avoir un test qui tourne avec le module de test inmemory et Spring, le résultat final ressemblera du coup à peu près à ceci :
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations = { "classpath:WEB-INF/applicationContext.xml" })
public class TestServiceInMemory {
private JerseyTest jersey;
@Autowired
SomeService someService;
@Before
public void before() throws Exception {
jersey = new JerseyTest(new InMemoryTestContainerFactory()) {
protected com.sun.jersey.test.framework.AppDescriptor configure() {
DefaultResourceConfig rc = new DefaultResourceConfig();
rc.getSingletons().add(someService);
rc.getClasses().add(org.codehaus.jackson.jaxrs.JacksonJsonProvider.class);
return new LowLevelAppDescriptor.Builder(rc).contextPath("poc-jersey").build();
};
};
jersey.setUp();
}
@Test
public void testGet1() {
String result = jersey.resource().path("/someResource/1").get(String.class);
assertEquals("{\"someBean\":[{\"someAttribute\":\"someValue\"}]}", result);
}
@After
public void after() throws Exception {
jersey.tearDown();
}
}
RESTEasy
Tout comme Jersey, RESTEasy fournit tout un framework de test. Là aussi la documentation est plutôt succincte, mais en revanche son intégration avec Spring s'est avérée nettement plus simple voire intuitive.
A titre d'exemple voici le même test que précédemment adapté au framework de test de RESTEasy :
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations = { "classpath:WEB-INF/applicationContext.xml" })
public class TestMockedService {
private Dispatcher dispatcher = MockDispatcherFactory.createDispatcher();
@Autowired
private SomeService someService;
@Before
public void before() {
dispatcher.getRegistry().addSingletonResource(someService);
}
@Test
public void testGet1() throws URISyntaxException {
MockHttpRequest request = MockHttpRequest.get("/someResource/1");
MockHttpResponse response = new MockHttpResponse();
dispatcher.invoke(request, response);
assertEquals(HttpServletResponse.SC_OK, response.getStatus());
assertEquals("{\"someBean\":[{\"someAttribute\":\"someValue\"}]}", response.getContentAsString());
}
}
On appréciera la simplicité du code. Mon cœur qui balançait jusqu'à présent entre Jersey et RESTEasy penche cette fois définitivement en faveur de RESTEasy.
Restlet
Restlet est un peu à part puisqu'il propose sa propre API qui ne nécessite pas de tourner dans un serveur d'application. De fait il est relativement facile d'écrire des tests unitaires avec ce framework :
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations = { "classpath:WEB-INF/applicationContext.xml" })
public class TestMockedService {
@Autowired
private MyJaxrsApplication jaxrsApplication;
@Test
public void testGet1() throws URISyntaxException, IOException {
Request request = new Request(Method.GET, "/someResource/1");
request.getResourceRef().setBaseRef("/");
request.setOriginalRef(request.getResourceRef());
request.setRootRef(request.getResourceRef().getBaseRef());
Response response = new Response(request);
jaxrsApplication.getJaxRsRestlet().handle(request, response);
assertEquals(HttpServletResponse.SC_OK, response.getStatus().getCode());
assertEquals("{\"someBean\":[{\"someAttribute\":\"someValue\"}]}", response.getEntity().getText());
}
}
Performances
Un dernier élément intéressant est celui des performances. J'utilise pour ma part le plugin Infinitest pour Eclipse, il est donc essentiel que mes tests s'exécutent le plus rapidement possible. Le tableau suivant inclut les résultats obtenus pour le même test avec, puis sans serveur embarqué. Chaque implémentation utilisant un serveur embarqué différent j'ai indiqué le nom de ce dernier à chaque fois :
Implémentation | Serveur | Durée du test avec serveur embarqué | Avec serveur mocké |
CXF | Jetty | 1.6s | 1.4s |
Jersey | Grizzly | 3s | 1.4s |
Embedded Glassfish | 4.4s | ||
RESTEasy | TJWS | 0.8s | 0.7s |
Restlet | Jetty | 1.2s | 0.9s |
Conclusion
Si la communauté derrière Jersey est semble-t-il plus active que celle derrière RESTEasy (impression personnelle basée uniquement sur la fréquence de sortie des milestones), ce dernier me paraît plus mature et surtout mieux pensé, surtout lorsque l'on aborde la question des tests unitaires.
CXF qui est pour sa part très puissant nécessite davantage de travail de configuration que ses deux concurrents. Il lui "suffirait" pourtant d'un mécanisme de détection automatique des services JAX-RS (et pourquoi pas JAX-WS ?) et des providers disponibles, ainsi que d'un vrai framework de test pour venir sérieusement les titiller.
Restlet, enfin, n'étant pas de base une implémentation de JAX-RS, fait plus figure de curiosité dans ce comparatif, même si côté performances dans les tests unitaires il s'en sort en seconde position derrière RESTEasy...
Au final, si à mon sens RESTEasy est aujourd'hui en tête, les quatre frameworks évoluent rapidement et mon classement pourrait être complètement chamboulé d'ici quelques mois. Il faut donc les surveiller de près... En outre, je ne me suis intéressé dans ces deux articles qu'au standard JAX-RS et il serait sans doute intéressant de voir ce qu'auraient à offrir d'autres framework REST tels que Restlet bien sûr, mais aussi Spring.
Pour finir voici un tableau récapitulatif :
Implémentation | Configuration | Code | Tests |
CXF | * | *** | ** |
Jersey | *** | *** | * |
RESTEasy | *** | *** | *** |
Restlet | ** | * | *** |
A noter que la note configuration tient compte de la qualité de la documentation et que dans le cas de Restlet les notes ne tiennent compte que de son utilisation dans le cadre bien particulier défini dans ces deux articles.