Ir al contenido principal

Unit-Testing SOAP: ¿Qué hacemos con los Holder?

En los últimos meses he tenido que integrarme con algunos Servicios Web (SOAP). Suerte que frameworks como CXF consiguen que la tarea sea algo más sencilla de lo que era hace unos años...

El problema vino cuando intenté probar uno de mis métodos: las clases que necesitaba llamar (auto-generadas con CXF) tenían como parámetros instancias de Holder (parámetro de entrada/salida). Y yo necesitaba utilizar los resultados de los Holder para continuar...

Veamos un ejemplo. Mi código tiene que llamar a esta interfaz:

package external.soap;

import javax.xml.ws.Holder;

// Omito todas las anotaciones y demás meta-información...
public interface IWebService {
    public String execute(int param, Holder<String> innerResult);
}

E imaginemos que tiene que hacer algo tan sencillo como concatenar la salida del método, con el resultado contenido en innerResult. El test que lo comprobara podría ser tan sencillo como:

[...]
import org.jmock.*;
import org.junit.*;
import static org.junit.Assert.*;

import javax.xml.ws.Holder;
import external.soap.IWebService;
    
public class MyServiceTest {
    
    private MyService myService = null;

    private Mockery mockery = new Mockery();
    private IWebService webServiceMock = null;

    @Before
    public void setUp() {
        myService = new MyService();

        webServiceMock = mockery.mock(IWebService.class);
        myService.setWebService(webServiceMock);
    }

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

    @Test
    public void testBothResultsAreConcatenated() throws Exception {
        mockery.checking(new Expectations() {
          {
            exactly(1).of(webServiceMock).execute(with(any(Integer.class)),
                    with(any(Holder.class)));
            will(returnValue("outer"));
          }
        });

        String result = myService.executeOperation(1);
        assertEquals("outer-inner", result);
    }
}

Por muchos "inventos" que hagamos en el método executeOperation(), no habrá manera de hacer funcionar ningún test que tenga Holders, ya que del mismo modo que especificamos que el método IWebService.execute() tiene que retornar la respuesta "outer", no estamos declarando en ningún sitio que además debería rellenar el Holder con la respuesta "inner".

¿Se puede hacer algo al respecto? Aunque el capítulo 15 del JMock Cookbook explique la manera de realizar acciones concretas dentro de los objetos mock utilizando BeanShell, creo que es una solución demasiado compleja. Sin embargo, sí que es posible encontrar una solución sencilla y fácil de implementar para estas situaciones: extender las Action. Utilizando la propia jerarquía de clases de JMock, se puede redefinir el comportamiento de un objeto, implementando la clase Action.

A la hora de la verdad, yo prefiero utilizar la subclase VoidAction, creando una clase anónima y redefiniendo su método invoke:

[...]
import org.jmock.*;
import org.junit.*;
import static org.junit.Assert.*;

import javax.xml.ws.Holder;
import external.soap.IWebService;
    
public class MyServiceTest {
    
    private MyService myService = null;

    private Mockery mockery = new Mockery();
    private IWebService webServiceMock = null;

    @Before
    public void setUp() {
        myService = new MyService();

        webServiceMock = mockery.mock(IWebService.class);
        myService.setWebService(webServiceMock);
    }

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

    @Test
    public void testBothResultsAreConcatenated() throws Exception {
        mockery.checking(new Expectations() {
          {
            exactly(1).of(webServiceMock).execute(with(any(Integer.class)),
                    with(any(Holder.class)));
            will(new VoidAction() {
                @Override
                public Object invoke(Invocation invocation) throws Throwable {
                    Holder<String> holder = (Holder) invocation.getParameter(1);
                    holder.value = "inner";
                    return "outer";
                }
            });
          }
        });

        String result = myService.executeOperation(1);
        assertEquals("outer-inner", result);
    }
}

Como se puede ver, aunque la sintaxis es algo extraña, se consigue definir el comportamiento exacto de cada uno de los parámetros de entrada/salida. Sólo hay que tener en cuenta que el primer parámetro es invocation.getParameter(0), el segundo es invocation.getParameter(1), y así sucesivamente.

Por último, si vemos que tenemos que probar muchas veces el mismo comportamiento, y se nos repite en varios tests, siempre podemos optar por crear una inner class dentro de nuestra clase de pruebas, creando una nueva instancia desde cada uno de los tests:

    [...] 

    @Test
    public void testBothResultsAreConcatenated() throws Exception {
        mockery.checking(new Expectations() {
          {
            exactly(1).of(webServiceMock).execute(with(any(Integer.class)),
                    with(any(Holder.class)));
            will(new OuterInnerAction("outer", "inner"));
          }
        });

        String result = myService.executeOperation(1);
        assertEquals("outer-inner", result);
    }

    // ... other tests.

    private class OuterInnerAction extends VoidAction {
        private String returnValue = null;
        private String holderValue = null;

        public OuterInnerAction(String returnValue, String holderValue) {
            this.returnValue = returnValue;
            this.holderValue = holderValue;
        }

        @Override
        public Object invoke(Invocation invocation) throws Throwable {
            HolderHolder<String> holder = (Holder) invocation.getParameter(1);
            holder.value = holderValue;
            return returnValue;
        }
    }

    [...]

Aunque la solución no sea tan elegante como utilizar BeanShell, creo que es una manera rápida y sencilla de dotar a cada test de un comportamiento "a medida".

¡Suerte!

Comentarios

Entradas populares de este blog

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 HttpClie

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