Diseño de Pruebas Unitarias

Estructura

El código de una prueba debe realizar las siguientes tareas:

  1. Preparación. Donde se prepara el contexto necesario para ejecutar la prueba (test fixture).
  2. Ejecución. Donde se ejecuta la funcionalidad bajo prueba
  3. Verificación. Donde se verifica que el resultado de la ejecución sea el esperado
  4. Limpieza. Cada prueba debe dejar el estado del sistema tal cual estaba antes de iniciar la prueba, por ejemplo si la fase de preparación creó un archivo, este debe ser eliminado en la sección de limpieza. Esta fase es opcional en el sentido de que no es necesaria si es que la fase de preparación no ha creado cierto estado  en el fixture que pueda persistir más allá de la ejecución de la prueba.

Principios de diseño aplicados a las pruebas unitarias

  • Responsabilidad Única. Cada clase de prueba debe centrase únicamente en una responsabilidad de una clase y cada método debe probar únicamente un aspecto de dicha responsabilidad; es decir, un método en una clase de prueba deberá tener una sola razón para fallar (principio de única verificación).
  • Inversión de Dependencias. El código a probar no debe tener dependencias de código fuente con la prueba; la prueba depende del código a probar.
  • Abierto/Cerrado. El código de la prueba deberá estar cerrado a los cambios (siempre que la especificación no se modifique) y el código a probar deberá estar abierto a la modificación. Es decir, una vez definida la especificación como prueba de software esta no deberá cambiar; los cambios deben realizarse en el código bajo prueba para satisfacer la especificación.

Buenas prácticas para el desarrollo de pruebas de software

A continuación se describen algunas de las buenas prácticas recomendadas para el desarrollo de prueba. Mayores detalles sobre estas y otras prácticas se encuentran en las referencias el final del artículo.

Aislamiento e independencia

Las pruebas deben ser desarrolladas de manera tal que puedan ejecutarse de manera aislada (una prueba no debe depender de otra o del entorno en el que se ejecuta) y en cualquier orden (incluyendo la ejecución concurrente) [3]

Michael Feathers establece que una prueba NO es unitaria si:

  • Accede a una BD
  • Establece conexiones de red
  • Modifica archivos y directorios
  • No puede ejecutarse al mismo tiempo otras pruebas unitarias
  • Es necesario preparar el entorno antes de ejecutarla (Ej, modificar archivos de configuración).

Mantener limpio el código de las pruebas

De la misma manera que el código bajo prueba, el código de los test deberá mantenerse limpio: libre de redundancias, legible, etc. Para mejorar la legibilidad del código de una prueba se recomienda comenzar escribiendo la verificación y derivar a partir de ella el código de la prueba. La sección de verificación de una prueba debería expresar claramente una especificación de software; para facilitar esta tarea se puede emplear librerías como hamcrest.  De manera similar, el nombre de un método deberá expresar la intención de la prueba para lo cual se recomienda usar un estilo “condición_resultado”, por ejemplo:

public void concatenarXconCadenaVacia_resultaEnX()
{
//Preparacion
//Ejecucion
//Verificacion
//Limpieza
}

Desarrollo incremental del código bajo prueba

Para ayudar a derivar la funcionalidad esperada de manera incremental es recomendable comenzar con las pruebas para los escenarios excepcionales (como entradas nulas, cadenas vacías, etc.) para luego concentrase en los test enfocados en los aspectos centrales de la funcionalidad. Aplicando esta práctica, usualmente se hace evidente que a medida que se agregan más pruebas el código bajo prueba se hace gradualmente más genérico/completo.

Pruebas escalonadas

En muchas ocasiones la creación de una prueba puede causar que alguna prueba escrita previamente ya no sea necesaria porque la prueba recién creada resulta ser más genérica. En estos casos es perfectamente aceptable eliminar las pruebas redundantes.

Uno a Muchos

Cuando trabaje con colecciones, se recomienda implementar primero la lógica para trabajar con una instancia y luego generalizar para trabajar con la colección.

Referencias

[1] Test Driven Development: By Example

[2] Growing Object-Oriented Software, Guided by Tests

[3] Is Your Unit Test Isolated?

[4] 7 Popular Unit Test Naming Conventions

[5] JUnit: Test Execution Order

[6] Escribir código que sea fácil de probar (Write testable code)

Leave a Reply