miércoles, 12 de mayo de 2010

Patrones de test de unidad 2 : Humble Object

Este es el segundo post de nuestra serie sobre patrones de test de unidad. Hoy vamos a ver como hacer más poderosa nuestra suite de tests de unidad extendiendo su cobertura.

En todos los proyectos hay objetos para los cuales nos parece imposible escribir tests automáticos. Muchas veces el problema es que son objetos que no podemos instanciar en un test de unidad por que están muy acoplados a la infraestructura de la aplicación.

Un ejemplo típico de esta situación es la verificación del comportamiento de un objeto que debe correr dentro de un Application Server, como por ejemplo un EJB session bean. Este objeto no se puede instanciar en un test de unidad porque los tests de unidad no corren dentro de un Application Server y por lo tanto no vamos a poder verificar su comportamiento.

La solución a este problema es extraer a un objeto plano la lógica de negocio incluida en el objeto que no podemos instanciar y verificar el comportamiento de este nuevo objeto. El objeto original se limita a manejar su relación con el contexto y delega en el nuevo objeto todo el resto de sus viejas responsabilidades.

Este patrón se conoce como Humble Object, ya que el objeto acoplado a la infraestructura pierde la lógica de negocios y se transforma en un objeto mas "humilde".

Una variante se da cuando tenemos un método que no podemos probar porque tiene efectos colaterales (por ejemplo hacer commit en la base de datos). Entonces extraemos la lógica de negocio a otro método y dejamos solo el manejo de transacciones en el método original.

La ventaja más obvia de este pattern es que vamos a tener más código cubierto por tests de unidad y por lo tanto menos bugs. Sin embargo hay otra ventaja más sutil y quizás más importante que es que estamos bajando el acoplamiento de nuestro código. Por lo tanto tendremos más posibilidades de reuso, porque como el nuevo objeto no tiene dependencias puede ser utilizado en otras partes del código. Además mejoramos la mantenibilidad, porque podemos cambiar nuestra infraestructura sin necesidad de reescribir el código de negocios.

Esta situación donde trabajamos para hacer que nuestro código sea más testable y conseguimos como efecto colateral que también sea de mayor calidad es muy común. El motivo es que en general el código díficil de testear es código de mala calidad, de manera que la dificultad para escribir tests para un objeto funciona como un indicador de la existencia de problemas de diseño.

Veamos un ejemplo simple: un programa que recibe pedidos via HTTP. Los pedidos que recibe contienen dos parámetros y el programa devuelve la suma de los mismos.

package arrogant;
import java.io.IOException;
import java.io.OutputStream;
import java.net.InetSocketAddress;
import java.util.concurrent.Executors;
import....
 
public class HttpSum {
    public static void main(String[] args) throws Exception {
        InetSocketAddress addr = new InetSocketAddress(8080);
        HttpServer server = HttpServer.create(addr, 0);
 
        server.createContext("/", new HttpSumHandler());
        server.setExecutor(Executors.newCachedThreadPool());
        server.start();
    }
 
}
 
class HttpSumHandler implements HttpHandler {
 
 
    public void handle(HttpExchange exchange) throws IOException {
        String result = calculateSum(exchange);
        sendResponse(exchange,result);
    }
 
    private String calculateSum(HttpExchange exchange) {
        int a = Integer.parseInt(getHeader(exchange, "a"));
        int b = Integer.parseInt(getHeader(exchange, "b"));
        return new Integer(a + b).toString() ;
    }
 
    public String getHeader(HttpExchange exchange,String name) {
        Headers requestHeaders = exchange.getRequestHeaders();
        return requestHeaders.get(name).get(0).toString();
    }
 
 
    private void sendResponse(HttpExchange exchange, String result) throws IOException {
        Headers responseHeaders = exchange.getResponseHeaders();
        responseHeaders.set("Content-Type", "text/plain");
        exchange.sendResponseHeaders(200,result.getBytes().length );
        OutputStream responseBody = exchange.getResponseBody();
        responseBody.write(result.getBytes());
        responseBody.close();
    }
}

El código que podríamos llamar "de negocio" se encuentra en el método calculateSum, pero no podemos testearlo porque está totalmente mezclado con el código que se ocupa de la interfaz HTTP.

Un primer intento para extraer la funcionalidad a testear es el siguiente:

package humble1;
import java.io.IOException;
import java.io.OutputStream;
import java.net.InetSocketAddress;
import java.util.concurrent.Executors;
import...
 
 
public class HttpSum {
    public static void main(String[] args) throws Exception {
        InetSocketAddress addr = new InetSocketAddress(8081);
        HttpServer server = HttpServer.create(addr, 0);
 
        server.createContext("/", new HttpSumHandler());
        server.setExecutor(Executors.newCachedThreadPool());
        server.start();
    }
 
}
 
class HttpSumHandler implements HttpHandler {
 
    public void handle(HttpExchange exchange) throws IOException {
 
        String result = calculateSum(exchange);
 
        sendResponse(exchange,result);
 
    }
 
    private String calculateSum(HttpExchange exchange) {
        int a = Integer.parseInt(getHeader(exchange, "a"));
        int b = Integer.parseInt(getHeader(exchange, "b"));
        Adder adder = new Adder(a,b);
        return new Integer(adder.sum()).toString();
    }
 
    public String getHeader(HttpExchange exchange,String name) {
        Headers requestHeaders = exchange.getRequestHeaders();
        return requestHeaders.get(name).get(0).toString();
    }
 
 
    private void sendResponse(HttpExchange exchange, String result) throws IOException {
        Headers responseHeaders = exchange.getResponseHeaders();
        responseHeaders.set("Content-Type", "text/plain");
        exchange.sendResponseHeaders(200,result.getBytes().length );
        OutputStream responseBody = exchange.getResponseBody();
        responseBody.write(result.getBytes());
        responseBody.close();
    }
}
 
class Adder {
    int a;
    int b;
 
    public Adder(int a, int b) {
        this.a = a;
        this.b = b;
    }
 
    public int sum() {
        return a+b;
    }
 
}

Aquí la lógica de negocio se extrae a una clase Nueva (Adder) que recibe todos los parámetros que necesita en su constructor. La clase Adder no tiene dependencias y puede ser fácilmente testeada.

Sin embargo este enfoque puede no ser adecuado cuando se reciben muchos parámetros del contexto, ya que en esos caso la construcción del objeto que se ocupa de la lógica de negocios es demasiado compleja. En esta situación puede ser mejor la siguiente idea:

 
package humble2;
 
 
 
import java.io.IOException; 
 
import java.io.OutputStream;
 
import java.net.InetSocketAddress; 
 
import java.util.concurrent.Executors; 
 
 
 
import ... 
 
 
 
public class HttpSum {
 
   public static void main(String[] args) throws Exception {
 
   InetSocketAddress addr = new InetSocketAddress(8082);
 
   HttpServer server = HttpServer.create(addr, 0); 
 
   server.createContext("/", new HttpSumHandler());
 
   server.setExecutor(Executors.newCachedThreadPool());
 
    server.start();
 
  }
 
}
 
 
 
  class HttpSumHandler implements HttpHandler {
 
        public void handle(HttpExchange exchange) throws IOException {
 
         String result = calculateSum(exchange);
 
         sendResponse(exchange,result);
 
    }
 
 
 
    private String calculateSum(HttpExchange exchange) {
 
         IAdderInput input = new HttpAdderInput(exchange);
 
         Adder adder = new Adder(input);
 
        return new Integer(adder.sum()).toString() ;
 
    }
 
 
 
    private void sendResponse(HttpExchange exchange, String result) throws IOException {
 
 
 
        Headers responseHeaders = exchange.getResponseHeaders();
 
        responseHeaders.set("Content-Type", "text/plain");
 
        exchange.sendResponseHeaders(200,result.getBytes().length );
 
        OutputStream responseBody = exchange.getResponseBody();
 
        responseBody.write(result.getBytes());
 
        responseBody.close();
 
    }
 
 }
 
 
 
   interface IAdderInput {
 
         int getA();
 
         int getB();
 
     }
 
 
 
    class HttpAdderInput implements IAdderInput {
 
        HttpExchange exchange;
 
        public HttpAdderInput(HttpExchange exchange) {
 
        this.exchange = exchange;
 
   }
 
 
 
   @Override public int getA() {
 
             return getIntegerParameter("a");
 
    }
 
 
 
   @Override public int getB() {
 
            return getIntegerParameter("b");
 
    }
 
 
 
   private int getIntegerParameter(String name) {
 
             return Integer.parseInt(getHeader(name));
 
 
 
   }
 
  private String getHeader(String name) {
 
             Headers requestHeaders = exchange.getRequestHeaders();
 
             return requestHeaders.get(name).get(0).toString();
 
         }
 
   }
 
 
 
 
 
  class Adder {
 
         IAdderInput input;
 
 
 
        public Adder(IAdderInput input) {
 
             this.input = input;
 
        } 
 
 
 
        public int sum() {
 
               return input.getA() + input.getB();
 
          }
 
  }

En este caso cuando se crea el objeto de negocios este recibe como parámetro una instancia de una interfaz que representa todos los parámetros que se toman del contexto. En el ambiente de ejecución, recibe una implementación que es un Wrapper sobre el código HTTP original y que se ocupa de la extracción de los parámetros necesarios. En ambiente de tests de unidad, seguramente recibirá un Mock de la interfaz que devolverá los valores que nos interesen para los tests.

No hay comentarios:

Publicar un comentario