Créer et tester un client HTTP REST en JAX-RS JavaEE avec Jersey ?

java_ee_logo_vert_v2Voici comment créer un client HTTP REST en Java appelant un simple service CRUD. La solution se base sur le standard JavaEE JAX-RS avec comme implémentation Jersey-client pour le mapping et RESTEasy pour l’exécution, testé par JUnit et Jadler qui émule un server REST.

Environnement

Le client va appeler un service REST dont l’API est la suivante :

persons:  create, read, update, delete persons
POST /api/persons
GET  /api/persons/id/<person-id>
POST /api/persons/id/<person-id>
DEL  /api/persons/id/<person-id>

Source

ScreenShot003

pom.xml (config Maven)

<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
	<modelVersion>4.0.0</modelVersion>
	<groupId>com.damienfremont.blog</groupId>
	<artifactId>20150117-javaee-jersey_client</artifactId>
	<version>0.0.1-SNAPSHOT</version>
	<packaging>jar</packaging>
	<properties>
		<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
		<jdk.version>1.7</jdk.version>
		<jersey.version>1.18.3</jersey.version>
		<resteasy.version>2.3.1.GA</resteasy.version>
	</properties>
	<dependencies>

		<!-- JERSEY CLIENT -->
		<dependency>
			<groupId>com.sun.jersey</groupId>
			<artifactId>jersey-json</artifactId>
			<version>${jersey.version}</version>
		</dependency>
		<dependency>
			<groupId>com.sun.jersey</groupId>
			<artifactId>jersey-client</artifactId>
			<version>${jersey.version}</version>
		</dependency>
		<dependency>
			<groupId>org.jboss.resteasy</groupId>
			<artifactId>resteasy-jaxrs</artifactId>
			<version>${resteasy.version}</version>
		</dependency>
		<dependency>
			<groupId>org.jboss.resteasy</groupId>
			<artifactId>resteasy-jettison-provider</artifactId>
			<version>${resteasy.version}</version>
		</dependency>

		<!-- TEST -->
		<dependency>
			<groupId>junit</groupId>
			<artifactId>junit</artifactId>
			<version>4.9</version>
			<scope>test</scope>
		</dependency>
		<dependency>
			<groupId>org.easytesting</groupId>
			<artifactId>fest-assert-core</artifactId>
			<version>2.0M5</version>
			<scope>test</scope>
		</dependency>
		<dependency>
			<groupId>net.jadler</groupId>
			<artifactId>jadler-all</artifactId>
			<version>1.1.1</version>
			<scope>test</scope>
		</dependency>
		<dependency>
			<groupId>org.hamcrest</groupId>
			<artifactId>hamcrest-core</artifactId>
			<version>1.3</version>
		</dependency>

	</dependencies>
	<build>
		<plugins>
			<plugin>
				<groupId>org.apache.maven.plugins</groupId>
				<artifactId>maven-compiler-plugin</artifactId>
				<version>3.2</version>
				<configuration>
					<source>${jdk.version}</source>
					<target>${jdk.version}</target>
				</configuration>
			</plugin>
		</plugins>
	</build>
</project>

PersonModel.java (un simple POJO, annoté en JAX-RS )

@XmlRootElement(name="person")
@JsonIgnoreProperties(ignoreUnknown = true)
public class PersonModel implements Serializable {

	private static final long serialVersionUID = 6879685199191377814L;

	private Integer id;
	private String firstName;
	private String lastName;
	private String birthDate;

	public Integer getId() {
		return id;
	}

	public void setId(Integer id) {
		this.id = id;
	}

	public String getFirstName() {
		return firstName;
	}

	public void setFirstName(String firstName) {
		this.firstName = firstName;
	}

	public String getLastName() {
		return lastName;
	}

	public void setLastName(String lastName) {
		this.lastName = lastName;
	}

	public String getBirthDate() {
		return birthDate;
	}

	public void setBirthDate(String birthDate) {
		this.birthDate = birthDate;
	}

}

Personservice.java (une simple interface décrivant l’API, annotée en JAX-RS aussi)

@Path("persons")
public interface PersonService {

	@GET
	public void ping();

	@POST
	@Consumes(MediaType.APPLICATION_JSON)
	public String createPerson(PersonModel newPerson);

	@GET
	@Path("id/{id}")
	@Produces(MediaType.APPLICATION_JSON)
	public PersonModel readPerson(@PathParam("id") Integer id);

	@POST
	@Path("id/{id}")
	@Consumes(MediaType.APPLICATION_JSON)
	public String updatePerson(@PathParam("id") Integer id, PersonModel actualPerson);

	@DELETE
	@Path("id/{id}")
	public void deletePerson(@PathParam("id") Integer id);
}

Demo

Pour commencer, voici un test de connexion de notre client vers l’API en émulant un server HTTP REST avec Jadler.

PersonClientTest .java

public class PersonClientTest {

	PersonService client;

	@Before
	public void setUp() {
		// INIT MOCK
		initJadler();
		// INIT REST CLIENT
		client = ProxyFactory.create(//
				PersonService.class, //
				"http://localhost:" + Jadler.port() + "/api");
	}

	@After
	public void tearDown() {
		closeJadler();
	}

	@Test
	public void test_QUAND_create_person_ETANT_DONNE_new_ALORS_success() {

		// ETANT DONNE
		onRequest().havingMethodEqualTo("POST")
				.havingPathEqualTo("/api/persons").respond()
				.withBody("Location: /persons/id/6") //
				.withEncoding(Charset.forName("UTF-8")) //
				.withContentType("text/plain; charset=UTF-8") //
				.withStatus(204);

		PersonModel newPerson = new PersonModel();
		newPerson.setFirstName("Luke");
		newPerson.setLastName("Skylwalker");
		newPerson.setBirthDate("19 BBY");

		// QUAND
		client.createPerson(newPerson);

		// ALORS
		// nothing to assert
	}

	@Test
	public void test_QUAND_get_person_ETANT_DONNE_id_valid_ALORS_success() {

		// ETANT DONNE
		onRequest()
				.havingMethodEqualTo("GET")
				.havingPathEqualTo("/api/persons/id/1")
				.respond()
				.withBody(
						"{person:{" //
								+ "\"id\":\"1\","
								+ "\"firstName\":\"Anakin\","
								+ "\"lastName\":\"Skylwalker\","
								+ "\"birthDate\":\"41.9 BBY\"" //
								+ "}}") //
				.withEncoding(Charset.forName("UTF-8")) //
				.withContentType("application/json; charset=UTF-8") //
				.withStatus(200);

		Integer arg = 1;

		// QUAND
		PersonModel response = client.readPerson(arg);

		// ALORS
		assertThat(response).isNotNull();
		assertThat(response.getId()).isNotNull();
		assertThat(response.getFirstName()).isEqualTo("Anakin");
		assertThat(response.getLastName()).isEqualTo("Skylwalker");
		assertThat(response.getBirthDate()).isEqualTo("41.9 BBY");
	}

	@Test
	public void test_QUAND_post_person_ETANT_DONNE_person_exists_ALORS_success() {

		// ETANT DONNE
		onRequest()
				.havingMethodEqualTo("POST")
				.havingPathEqualTo("/api/persons/id/1")
				.respond()
				.withStatus(200);

		Integer arg = 1;
		PersonModel newPerson = new PersonModel();
		newPerson.setId(1);
		newPerson.setFirstName("Anakin");
		newPerson.setLastName("Skylwalker");
		newPerson.setBirthDate("41.9 BBY");

		// QUAND
		client.updatePerson(arg, newPerson);

		// ALORS
		// nothing to assert
	}

	@Test
	public void test_QUAND_delete_person_ETANT_DONNE_person_exists_ALORS_success() {

		// ETANT DONNE
		onRequest()
				.havingMethodEqualTo("DELETE")
				.havingPathEqualTo("/api/persons/id/1")
				.respond()
				.withStatus(200);

		Integer arg = 1;

		// QUAND
		client.deletePerson(arg);

		// ALORS
		// nothing to assert
	}
}

C’est donc un test d’intégration du client que l’on peut lancer comme un simple test JUnit.
ScreenShot001

Amélioration

Il est recommandé de configurer les TimeOuts sur votre client JAX-RS, pour éviter de bloquer votre application (ou un de vos threads si c’est un server).

Executor.java

public class Executor extends ApacheHttpClient4Executor
{

	private static final int	connectionTimeoutMillis	= 2000;
	private static final int	socketTimeoutMillis	= 3000;

	public Executor()
	{
		super( initHttpClient() );
	}

	private static DefaultHttpClient initHttpClient()
	{
		DefaultHttpClient httpClient = new DefaultHttpClient();
		HttpParams params = httpClient.getParams();
		HttpConnectionParams.setConnectionTimeout( params, connectionTimeoutMillis );
		HttpConnectionParams.setSoTimeout( params, socketTimeoutMillis );
		return httpClient;
	}
}

Qui peut aussi se tester avec Jadler.

ExecutorTest.java

public class ExecutorTest {

	PersonService client;

	@Before
	public void setUp() {
		// INIT MOCK
		initJadler();
		// INIT REST CLIENT
		client = ProxyFactory.create(//
				PersonService.class, //
				"http://localhost:" + Jadler.port() + "/api", new Executor());
	}

	@After
	public void tearDown() {
		closeJadler();
	}

	@Test
	public void test_QUAND_ping_ETANT_DONNE_system_ok_ALORS_success() {

		// ETANT DONNE
		onRequest().havingMethodEqualTo("GET")
				.havingPathEqualTo("/api/persons").respond()
				.withDelay(1, SECONDS);

		// QUAND
		client.ping();
	}

	@Test
	public void test_QUAND_ping_ETANT_DONNE_system_lag_ALORS_timeout() {

		// ETANT DONNE
		onRequest().havingMethodEqualTo("GET")
				.havingPathEqualTo("/api/persons").respond()
				.withDelay(50, SECONDS);

		// QUAND
		try {
			client.ping();
			fail("Expected Exception!");
		} catch (Exception e) {

			// ALORS
			assertThat(e.getMessage()).contains("Read timed out");
		}
	}

}

ScreenShot002

Conclusion

Pour une fois, pas de réelle contre-partie :

  • Le client est facile à coder car il n’y a même pas d’implémentation.
  • Les tests sont auto-documentés et automatisé (indépendants, répétables, non-régression, etc).

Le plus intéressant étant que la maintenance en est grandement simplifié …le source ne contenant que du code “métier” (pas de code boilerplate) et le comportement est testé.

Source code

https://github.com/damienfremont/blog/tree/master/20150120-javaee-jersey_client

References

http://www.mkyong.com/tutorials/jax-rs-tutorials/

http://www.mastertheboss.com/jboss-frameworks/resteasy/resteasy-tutorial

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s