Ir al contenido principal

Dependency Injection de HttpClient 3.x

Para un fanático de la Dependency Injection como yo, siempre me ha costado trabajar con Apache HttpClient 3.x y la multitud de maneras que tiene la gente de inicializar los clientes. Además, como usuario asiduo de Spring Framework, es imprescindible disponer de una manera de cargar clientes HTTP desde el contexto XML y que sea sencilla de probar.

Una de las soluciones más comunes para crear y configurar los clientes es la declaración de un método init() donde se inicializan todos los parámetros, obligando que nuestra clase conozca todos los detalles y parámetros de este componente externo. Pese a que la solución funciona, no deja de ser complicada de testear, y esto es justamente lo que se quiere evitar desde el Test Driven Development (TDD). Para ello, hay que buscar una fórmula que permita "inyectar" los clientes HTTP, ya configurados en el exterior, permitiendo a la clase de destino centrarse en su objetivo.

En primer lugar, hay que conseguir la biblioteca HttpClient 3.1, usando nuestro gestor de dependencias favorito: http://mvnrepository.com/artifact/commons-httpclient/commons-httpclient/3.1. Al finalizar el proceso, deberíamos tener descargadas las siguientes dependencias:

El siguiente paso es declarar una instancia de HttpClient, sin inicializar, en la clase que necesite realizar conexiones web. Además tendremos que decidir cómo se configurará la instancia desde el exterior: por inyección en el constructor, por inyección usando un método setter o utilizando algún sistema por anotaciones, como el @Autowired de Spring Framework:

import org.apache.commons.httpclient.HttpClient;

public class HttpClient3Injected {

  @Autowired // Dependency injection by annotation.
  private HttpClient client = null;

  /**
   * Dependency injection by constructor.
   */
  public HttpClient3Injected(HttpClient client) {
    this.client = client;
  }

  [...]

  /**
   * Dependency injection by setter.
   */
  public void setClient(HttpClient client) {
    this.client = client;
  }

}

Sólo con esta declaración, ya podemos realizar tests unitarios con mocks, que nos permitan simular cualquier entrada y salida del HttpClient sin tener que realizar la petición real. Por ejemplo, usando JMock 2.5.x:

import static org.junit.Assert.assertNull;

import org.apache.commons.httpclient.*;
import org.jmock.*;
import org.junit.*;
import org.jmock.lib.legacy.ClassImposteriser;

public class TestHttpClient3Injected {

  private HttpClient3Injected example = null;

  private Mockery mockery = new Mockery() {
    {
      setImposteriser(ClassImposteriser.INSTANCE);
    }
  };

  private HttpClient clientMock = null;

  @Before
  public void setUp() throws Exception {
    clientMock = mockery.mock(HttpClient.class);

    example = new HttpClient3Injected(clientMock);
  }

  @After
  public void tearDown() throws Exception {
    mockery.assertIsSatisfied();
  }

  @Test
  public void testConnectShouldReturnNullIfHttpConnectionFails() throws Exception {
    mockery.checking(new Expectations() {
      {
        exactly(1).of(clientMock).executeMethod(with(any(HttpMethod.class)));
        will(throwException(new HttpException("Oops!")));
      }
    });

    assertNull(example.connect());
  }

}

Ya podemos definir todos los casos de test que necesitemos e implementar el método connect usando TDD.

Pero ahora queda ver cómo utilizar esta clase desde el exterior. Es tan sencillo como crear una instancia de HttpClient, con la configuración que necesitemos, y facilitarla a nuestra clase:

// Simple default HttpClient
HttpClient client = new HttpClient();
HttpClient3Injected myInstance = new HttpClient3Injected(client);

// HttpClient with a multithreaded customized connection pool
HttpConnectionManagerParams mgrParams = new HttpConnectionManagerParams();
mgrParams.setDefaultMaxConnectionsPerHost(20);
mgrParams.setMaxTotalConnections(100);
mgrParams.setSoTimeout(300000);
MultiThreadedHttpConnectionManager manager = new MultiThreadedHttpConnectionManager();
manager.setParams(mgrParams);
HttpClient client = new HttpClient(manager);
HttpClient3Injected myInstance = new HttpClient3Injected(client);

Y lo mejor de todo, gracias a este método podemos inyectar un HttpClient totalmente configurado desde XML a nuestra clase, usando un framework de inyección de dependencias, como Spring Framework:

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans" 
 xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
 xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">

  <bean id="instance" class="cat.edra.web.HttpClient3Injected">
    <constructor-arg ref="client" />
  </bean>

  <bean id="client" class="org.apache.commons.httpclient.HttpClient">
    <constructor-arg ref="manager" />
  </bean>

  <bean name="manager" class="org.apache.commons.httpclient.MultiThreadedHttpConnectionManager">
    <property name="params">
      <bean class="org.apache.commons.httpclient.params.HttpConnectionManagerParams">
        <property name="defaultMaxConnectionsPerHost" value="20" />
        <property name="maxTotalConnections" value="100" />
        <property name="soTimeout" value="300000" />
      </bean>
    </property>
  </bean>

</beans>

Como véis, este método permite centrarse en la implementación de nuestra clase, y siempre precedida de una batería completa de tests. La inyección de dependencias nos ofrece un HttpClient completamente configurado por la clase que nos invoque.

Comentarios

Entradas populares de este blog

Unit Tests (I): "mocking" la interacción

Indispensables. De entre todos los tipos de tests, son los más sencillos de escribir, aunque son los que permiten encontrar (a tiempo) la mayor cantidad de bugs. ¿Qué es un Unit Test? Mejor contar qué hacen y no lo que son: Un Unit Test debe probar un ÚNICO método de una ÚNICA clase . Simple. Sin excepciones (*) . El método a probar SIEMPRE debe tener un estado inicial conocido y un resultado esperado. ¡Siempre! Si cumple ambas premisas, el test podrá ser ejecutado de forma automática y sin depender de nadie (**) . Considero que probar la interacción de una clase con el resto de componentes es uno de los sistemas que mejores resultados ofrece. Estas interacciones se "simulan" en el mismo test, utilizando mock-objects. Antes de empezar con algún ejemplo de Unit Test con Mock Objects, hay algunos consejos que nos pueden facilitar el trabajo: Cada clase debe tener su Interfaz (***) , exceptuando las java.lang.Exception y poco más. Conocer el patrón de Inyección de Depen

Unit Tests (II): clases sin interfaz

Sigamos con los tests unitarios: ¿qué ocurre si alguna de mis dependencias no tiene interfaz? O simplemente, ¿qué ocurre si no me gusta añadir una interfaz a cada una de mis clases? Aunque recomiendo encarecidamente el uso de interfaces (ayudan a definir y delimitar el alcance de una clase), no es del todo extraño que algunas de nuestras dependencias no las tengan. Como ejemplo, veamos una que seguro que a muchos nos ha tocado implementar: un cliente para leer los mensajes de Twitter . Si decidimos usar el proyecto Twitter4J , nos encontramos con que su cliente twitter4j.Twitter no tiene interfaz. Si intentáramos hacer un test usando la guía del post anterior : [...] private TwitterService twitterService = null; private Mockery mockery = new Mockery(); private Twitter twitterMock = null; @Before public void setUp() { twitterService = new TwitterService(); twitterMock = mockery.mock(Twitter.class); twitterService.setTwitter(twitte