Testing nivel 2: ¿Qué hace que un test sea un Test?

Avatar
12 min lectura

En el post anterior sobre testing estuvimos hablando sobre el mundo del testing en general, de sus beneficios, de los tipos de test y de las ventajas que nos ofrece SOLID de cara al testing. Ahora que ya tenemos una visión global es hora de profundizar en los test unitarios.

En esta ocasión vamos a usar definiciones y conceptos teóricos, que son aplicables al código que hacemos. En un primer momento pueden ser duros de entender, sobre todo si es tu primera toma de contacto con el mundo de los test; por eso mismo, si al acabar te sientes algo confuso, te invito a releer el artículo una segunda vez para que veas que todo empieza a cobrar más sentido. Vamos “al turrón”.

Un poco de vocabulario, para empezar…

Sujeto bajo pruebas: el sujeto bajo pruebas es la clase testada. Es decir, el actor principal que va a ser el objeto de todos nuestros test. En inglés se dice “Subject Under Test” y sus iniciales son SUT, por lo que no te extrañe ver esas iniciales más de una vez a lo largo del post.

Cobertura de código: es la parte de código o caminos de ejecución que han sido testados.

Alcance del test: o también llamado en inglés “Scope” del test. Es lo que abarca en cobertura de código la ejecución de nuestro test. Es importante tener consciencia de la importancia de este concepto y su estrecha relación con la palabra test unitario. En otras palabras, un test unitario puro  tiene unas fronteras o límites que no salen más allá del propio SUT, por lo tanto podemos deducir que su Scope es reducido.

Un test unitario puro tiene unas fronteras o límites que no salen más allá del propio SUT. Clic para tuitear

Teniendo claros estos tres conceptos es hora de abordar los tipos de test. Vamos con ello…

Tipos de Test unitarios

Vamos a clasificar los tipos de test unitarios dentro de 2 grupos perfectamente compatibles y complementarios: según su interacción con el resto de test y según aquello que van a testar o su intención.

Interacción

  • Aislados, solitarios, no sociables: son la base de los test unitarios, también los podemos llamar test unitarios puros. Hemos de empezar por hacer test de este tipo ya que van a ser el punto de partida. El Scope de estos test es la unidad mínima testable, es decir sólo testarán el SUT sin cruzar sus fronteras. Por lo tanto, estos test se encargan de validar en un entorno aséptico y no contaminado comportamientos o estados de una pieza de código concreta. Si fallan es porque lo que estamos testando tiene algún error. No son susceptibles de verse afectados por efectos de lado producidos por algún colaborador del test, porque todos los colaboradores están bajo nuestro control en el test y no usan código real, sólo se encargan de generar un entorno idóneo para nuestro caso de test. Veremos cómo conseguir eso más tarde en la sección de “colaboradores de nuestros tests”.
  • Sociables o en colaboración: En estos test pueden interactuar varios de los SUT ya testados de forma que el scope del test es bastante más amplio. Estos test tardan más tiempo en pasar y son susceptibles de producir fallos en cascada, como podremos imaginar, además su mantenimiento suele ser más costoso. A cambio nos dan una cobertura de código más amplia.

No es suficiente con hacer sólo test no sociables. La experiencia nos demuestra que dos piezas probadas con test unitarios no sociables, no nos asegura que combinadas vayan a funcionar en nuestro sistema como es de esperar. Es por eso que partiendo de la base de los test unitarios debemos ir avanzando y conectando piezas en test sociables para ver si encajan como si de engranajes en un reloj suizo se tratasen: a la perfección, otorgando el resultado esperado por el sistema.

Dos piezas probadas con test unitarios no sociables, no asegura que combinadas funcionen. Clic para tuitear

Intención

  • De verificación de estado: comprueban que a una entrada el resultado tras la ejecución es la salida esperada o lo que es lo mismo, un estado (variable) del sistema tras una ejecución queda en otro estado. Las comprobaciones de este tipo son las que llamamos  “Asserts” o “asertar”.
  • De verificación de comportamiento: en estos test en lugar de testar en que estado ha quedado el sistema simplemente verificamos que se han hecho una o varias llamada, es decir, estamos verificando que el sistema se comporte como esperamos. Estos test suelen identificarse con la palabra clave “Verify” o verificación.

Estructura de un Test : “AAA”

La estructura de un test, o lo que es lo mismo, el cuerpo del test, las partes claramente diferenciadas se dividen principalmente en tres, las vamos a definir ahora y siempre han de respetar el orden que aquí exponemos:

  • “Arrange” o preparación: hacemos los preparativos para que lo que vayamos a testar esté todo lo aislado que queramos y no cruce fronteras que no nos interesen. Marcamos el límite de nuestro sujeto bajo pruebas y preparamos la entrada que le vamos a dar. En concreto preparamos el “Escenario de test”
  • “Act” o Llamada al método: ejecutamos la acción que queremos testar en el sujeto bajo pruebas.
  • “Assert” o comprobación: tras la ejecución vemos que el sistema ha quedado en el estado esperado a las entradas o que se ha comportado de la manera que esperamos porque ha disparado una o varias acciones. Véase varias como las mínimas necesarias. Ej: un test que hace 10 verificaciones de comportamiento o comprobaciones de estado es un smell de código, porque está haciendo demasiado trabajo. Esto quiere decir que probablemente el método que verifica haga demasiado también.
Estructura de un test AAA: Arrange, Act y Assert Clic para tuitear

Como podrás imaginar, se suelen preparar muchos escenarios de prueba diferentes, pero es muy posible que parte del preparativo sea común para estos diferentes escenarios. Es importante reutilizar nuestro código también en nuestros test.

Buenas Prácticas 

La importancia de los nombres

El nombre que damos a nuestros test importa, y mucho. Porque de lo anterior podemos deducir que vamos a tener que preparar varios test contra un mismo sujeto de prueba. Esto nos lleva a pensar que los nombres son más que importantes: han de describir el escenario en relación con lo que esperamos obtener y la acción ejecutada. Por ejemplo: shouldObtainEmployeeListWhenManagerIsNotEmpty() es un buen nombre para un método de test.

Como hemos dicho el sujeto bajo pruebas tendrá varias pruebas que configuran diferentes escenarios y salidas esperadas. De ahí que las clases de test tengan que tener como nombre el sujeto bajo pruebas y como nombres de métodos lo anteriormente descrito. Además es aconsejable que la ruta en el directorio de test sea lo más similar posible a la ruta del código real, para poder ubicar los test de una manera rápida y sencilla.

Siguiendo esto conseguiremos que los test sean documentación para nuestro código también. Algo que puede parecernos poco interesante a priori, pero que es muy de agradecer cuando volvemos sobre nuestro código al tiempo o cuando entra un nuevo compañero al proyecto.

Reutilizar nuestro código de test

Es la clave para hacer test fácilmente mantenibles e ir desarrollando cada vez con menos esfuerzo. Pero ten paciencia, el escribir test es algo diferente a la programación a la cuál estamos acostumbrados. Al principio costará y habrá que hacer mucho refactor sobre la forma de nuestro test, pero es una técnica que se va puliendo y como resultado puede dar lugar a sorprendentes resultados.

Crear una suite de herramientas para test es una buena práctica que podemos adoptar al coger expertise en el desarrollo de testing. En esta librería o librerías estaría bien colocar operaciones que solemos hacer con frecuencia: tratar fechas, validar ciertos estados por ejemplo cadena vacía o null… Esto puede evolucionar tanto como imaginemos. Hasta una pipeline de desarrollo para testing en la que podamos intercambiar piezas en tiempo de ejecución. Cuando usamos herramientas del estilo de “Harmcrest” es indispensable diseñar nuestros propios “matchers” y pueden ser carne de esa suite de herramientas. Puedes encontrar un par de enlaces al final del artículo interesantes relacionados con esto.

Dentro del mismo sujeto bajo test hay varios escenarios que comparten configuraciones similares aquí seguro que tiene sentido encapsular lo que varía de unos test a otros y usar las piezas encapsuladas según convenga para preparar los escenarios. También se pueden usar métodos de tipo setup y tearDown, que nos ayudan a crear condiciones iniciales y a “limpiar” el escenario una vez ejecutado el test. Sin embargo no debemos olvidar que el ámbito de esto es global al sujeto bajo pruebas y que se ejecutan siempre antes de cada test, por lo que podemos estar ejecutando siempre código que sólo los vale para un test.

Practica el “TOD”

“Tod” Es un concepto que me acabo de inventar y no viene en los libros, no intentes buscarlo, son las iniciales de “Test Oriented Development” y viene a decir algo tan simple como:

“Cuando programamos tests también programamos, y debemos seguir haciéndolo de la manera ordenada y limpia que requiere aquello que estamos haciendo: tests”

Por lo tanto con frecuencia habrá que construir entidades que sean necesarias para aislar nuestro SUT, o para emular ciertas entradas que le queremos dar. Esto desemboca en nuevos objetos,”builders”, “Test doubles”, estructuras de datos y utilidades que al fin y al cabo colaboran en la construcción y ejecución de nuestro test.

Colaboradores de nuestros tests

“Podemos llamar colaboradores de un test a aquellos objetos que participan de manera pasiva en el desarrollo del mismo. Es decir, nuestro SUT hace uso de ellos, pero no entran en el SCOPE de nuestro test”. También los podemos llamar “Test doubles”. Como podrás imaginar los test sociables usan menos “Test doubles” que los unitarios puros.

Colaboradores de un test son aquellos objetos que participan de manera pasiva en su desarrollo Clic para tuitear

En la ejecución de nuestro test, intercambiamos todo lo que no sea nuestro SUT por piezas como las que vamos a ver ahora, sobre las cuales tenemos control y además nos aportan información relevante sobre la ejecución del test.

  • Dummy: No hace nada, esta vacío y devuelve vacío.
public class DummyAuthorizer implements Authorizer {

    @Override
    public Boolean authorize(String username, String password) {
        return null;
    }
}
  • Stub: No hace nada más que devolver un valor por dfecto.
public class AcceptingAuthorizerStub implements Authorizer {

    @Override
    public Boolean authorize(String username, String password) {
        return true;
    }
}
  • Spy: Introducimos un parámetro al que tenemos acceso para que nos informe de algo que ocurre dentro del test.
public class AcceptingAuthorizerSpy implements Authorizer {
    public boolean authorizeWasCalled = false;

    @Override
    public Boolean authorize(String username, String password) {
        authorizeWasCalled = true;
        return true;
    }
}
  • Mock: Añadimos funcionalidad a un objeto que cumple la interfaz de SUT sólo para el propósito del test.
public class AcceptingAuthorizerVerificationMock implements Authorizer {
    public boolean authorizeWasCalled = false;

    @Override
    public Boolean authorize(String username, String password) {
        authorizeWasCalled = true;
        return true;
    }

    public boolean verify() {
        return authorizeWasCalled;
    }
}
  • Fake: Implementa una funcionalidad que puede emular a la realidad porque funciona según los datos de entrada se reciban, pero no es real.
public class AcceptingAuthorizerFake implements Authorizer {

    @Override
    public Boolean authorize(String username, String password) {
        return username.equals(“Bob”);
    }
}

Construcción de los colaboradores

En la construcción de los “Test Doubles” podemos ver diferentes estrategias para la creación de los mismos, entre ellas podemos observar con bastante frecuencia.

  • Mothers: Es un conjunto de “Factory Methods” que nos permiten crear diferentes objetos para nuestros tests.
  • Builders: Es el uso del patrón de diseño “builder*” para configurar el diferentes objetos a nuestro antojo que sean lo que necesitamos para nuestro test.

Definición original del patrón: El patrón de diseño “Builder” es un patrón de diseño de software de tipo creacional. La intención del patrón “Builder” es encontrar una solución al problema del anti-patrón de diseño del constructor telescópico.

No quiere decir que estos sean los únicos patrones de diseño o estrategias para la construcción de colaboradores que podemos usar. Al fin y al cabo estás programando como hemos dicho y tienes total libertad para diseñar los elementos que quieras y como quieras.

Pues muy bien, hasta aquí llegamos hoy. Hemos visto la forma que tiene un test y los diferentes elementos que interactúan en un test para ayudarnos a aislar nuestro código testado o SUT y ofrecernos información acerca de la ejecución del test. Es importante profundizar en estos conceptos y leer bastantes test en proyectos que podamos encontrar en la comunidad. De esta forma puedes asentar esta base teórica y darle forma cuando empieces a escribir las primeras líneas de test. ¿Te atreves a implementar tus primeros test ya?.

Si no te atreves, en la siguiente entrega, vamos a poner algún ejemplo interesante. Además vamos a ver cuáles son las partes más interesantes de nuestro código para testar, veremos también los errores comunes y algún concepto interesante como es el TDD, que no es una herramienta de diseño de test, como algunos piensan. Si quieres descubrir lo que es, ¡únete a la lista de correo para no perderte el próximo artículo!