Pruebas Unitarias, Parte 1: Introducción y utilización de objetos simulados (Mock)

...

La primera parte de esta serie de artículos realiza una introducción a las Pruebas Unitarias, repasa sus características y ventajas, para finalizar con la descripción de los diferentes tipos de objetos mock y la forma de utilizarlos.

Introducción

Todos los programadores saben que deben realizar pruebas a su código, pocos lo hacen de manera proactiva, la respuesta generalizada al “¿por qué no?” es “no tengo tiempo”.

Este apuro se convierte en un círculo vicioso. Cuanta más presión se siente, menos pruebas se realizan. Cuantas menos pruebas se realizan, menos productivo se es y el código se vuelve menos estable. Cuanto menos productivo y preciso se es, más presión se siente.

¿Qué es una prueba unitaria?

Una prueba unitaria, o “unit test”, es un método que prueba una unidad estructural de código.

Contrariamente a lo que piensan muchos desarrolladores –que el desarrollo de pruebas unitarias resta tiempo a tareas más importante– las pruebas unitarias por lo general son simples y rápidas de codificar, el desarrollo de una prueba unitaria no debería tomar más de cinco minutos.

Debido a la diversidad de definiciones, convendremos que una “buena” prueba unitaria tiene las siguientes características:

  • Unitaria, prueba solamente pequeñas cantidades de código.
  • Independiente, no debe depender ni afectar a otras pruebas unitarias.
  • Prueba métodos públicos, de otra forma la prueba sería frágil a cambios en la implementación y no se podría utilizar en pruebas de regresión.
  • Automatizable, la prueba no debería requerir intervención manual.
  • Repetible y predecible, no debe incidir el orden y las veces que se repita la prueba, el resultado siempre debe ser el mismo.
  • Profesionales, las pruebas deben ser consideradas igual que el código, con la misma profesionalidad, documentación, etc.

Respecto al último punto y contrariamente a lo que piensan muchos desarrolladores –que el desarrollo de pruebas unitarias resta tiempo a tareas más importante– las pruebas unitarias por lo general son simples y rápidas de codificar, el desarrollo de una prueba unitaria no debería tomar más de cinco minutos.

Ventajas

Las pruebas unitarias buscan aislar cada parte del programa y mostrar que las partes individuales son correctas, proporcionando cinco ventajas básicas:

  • Fomentan el cambio, las pruebas unitarias facilitan la reestructuración del código (refactorización), puesto que permiten hacer pruebas sobre los cambios y verificar que las modificaciones no han introducido errores (regresión).
  • Simplifican la integración, permiten llegar a la fase de integración asegurando que las partes individuales funcionan correctamente. De esta manera se facilitan las pruebas de integración.
  • Documentan el código, las propias pruebas pueden considerarse documentación, ya que las mismas son una implementación de referencia de como utilizar el código.
  • Separación de la interfaz y la implementación, la única interacción entre los casos de prueba y las unidades bajo prueba son las interfaces de estas últimas, se puede cambiar cualquiera de los dos sin afectar al otro (ver pruebas mock).
  • Menos errores y más fáciles de localizar, las pruebas unitarias reducen la cantidad de errores y el tiempo en localizarlos.
  • Pueden mejorar el diseño, la utilización de prácticas de diseño y desarrollo dirigida por las pruebas (Test Driven Development o TDD) permite definir el comportamiento esperado en un paso previo a la codificación.
  • Puede ser la forma más simple de verificar el funcionamiento, en situaciones como el desarrollo de una API o un componente que brinda servicios del cual no se cuenta aún con un cliente para consumirlos.

Adoptar el uso de pruebas unitarias como una disciplina generalizada puede ser un trabajo costoso, pero no debe ser considerado una desventaja, debe entenderse que esta actividad nos servirá para ahorrar tiempo en el futuro al disminuir la ocurrencia de errores.

La utilización de pruebas unitarias permitirá mejorar progresivamente la calidad del código en la medida que los desarrolladores aumenten la calidad de las pruebas y la cobertura del mismo, por ejemplo, "en MicroGestion durante el año 2011 se dedicaron 1160 horas a la resolución de errores en la fase de desarrollo y en el período de garantía, y en 2012 se registraron 710 horas".

Consideraciones

Como en la adopción de cualquier otra disciplina, la incoporación de pruebas unitarias no está exenta de problemas o limitaciones, a continuación se enumeran algunas consideraciones a tener en cuenta:

  • Por estar orientada a la prueba de fragmentos de código aislados, la pruebas unitarias no descubrirán errores de integración, problemas de rendimiento y otros problemas que afectan a todo el sistema en su conjunto.
  • En alguno casos será complicado anticipar inicialmente cuales son los valores de entradas adecuados para las pruebas, en esos casos las pruebas deberán evolucionar e ir incorporando valores de entrada representativos.
  • Si la utilización de pruebas unitarias no se incorpora como parte de la metodología de trabajo, probablemente, el código quedará fuera de sincronismo con los casos de prueba.
  • Otro desafío es el desarrollo de casos de prueba realistas y útiles. Es necesario crear condiciones iniciales para que la porción de aplicación que está siendo probada funcione como parte completa del sistema al que pertenece.
  • Escribir código para un caso de pruebas unitario tiene tantas probabilidades de estar libre de errores como el mismo código que se está probando.

Pruebas utilizando objetos simulados (mock)

Las pruebas Mock son pruebas unitarias que utilizan objetos simulados ("mock") que sustituyen a los objetos reales utilizados por la clase o fragmento de código a probar.

Por ejemplo, se tiene una clase Calculator que necesita de un DAO (Objeto de Acceso a Datos) para obtener información de la base de datos, este DAO es lo que llamamos el “objeto real”. Si quisiéramos realizar una prueba de la clase Calculator, deberíamos pasarle al objeto una conexión válida a la base de datos y asegurarnos de que los datos que necesita existan.

Este proceso de determinar los datos que el objeto necesita e insertarlos en la base requiere de mucho trabajo.

En lugar de esto, se puede proveer una instancia falsa del DAO que sólo devuelva lo que necesitamos para la prueba. Esta clase no va a tomar la información de la base de datos, sino que va a ser un mock.

Reemplazar el objeto real hace que probar la clase Calculator sea mucho más simple. Estrictamente hablando, dicho objeto que reemplaza al DAO real no sería un mock, sino un stub. Luego hablaremos de esta diferencia.

Este ejemplo puntual de la clase Calculator se puede generalizar diciendo que es una unidad (Calculator) utilizando un colaborador (DAO).

Unidad → Colaborador

Nota: la flecha significa ‘usa’.

Cuando reemplazamos al colaborador por un mock (o stub), esto se puede expresar de la siguiente manera:

Unidad → Mock

En el contexto de una prueba unitaria se vería de la siguiente manera:

Prueba Unitaria → Unidad → Colaborador

ó

Prueba Unitaria → Unidad → Mock

Utilización de stub, mock y proxy

Hay tres tipo de objetos falsos que se pueden usar para reemplazar a los objetos colaborador para realizar pruebas unitarias: stub, mock y proxy.

Objetos stub

Un stub es un objeto que implementa una interface de un componente, pero en lugar de retornar lo que el componente devolvería, el stub puede ser configurado para retornar un valor que se ajuste a lo que la prueba unitaria intenta probar.

Utilizando este tipo de objetos se puede probar si una unidad es capaz de manejar diferentes valores devueltos por el colaborador. Esto se puede expresar de la siguiente manera:

1. Prueba Unitaria → Stub
2. Prueba Unitaria → Unidad → Stub
3. La prueba unitaria verifica (assert) sobre el resultado y el estado de la unidad

El primer paso de la prueba unitaria es crear el objeto stub y configurar los valores de retorno (1). Luego se crea la unidad y se le setea el stub en lugar del colaborador “real” (2). Por último se verifica el resultado de la llamada a la unidad (3).

Ejemplo utilizando objetos stub (pseudocódigo)
public void testStub() {
	Calculadora calculadora = new Calculadora();
	GenericDAO stubGenericDao = stub(GenericDAO.class);
		
	when call (stubGenericDao.getPrimerValor()) then return(4);
	when call (stubGenericDao.getSegundoValor()) then return(8);
	calculadora.setDao(stubGenericDao);
		
	assertEquals(new Integer(12), calculadora.sumar());
}

Objetos mock

Un mock es como un stub, que además permite determinar qué métodos fueron llamados durante la prueba. Utilizando un mock se puede probar tanto si los valores devueltos por la unidad son los correctos, como así también si la unidad está utilizando de forma correcta al colaborador.

Por ejemplo, no se puede ver a partir del valor devuelto desde el objeto DAO si la información que fue leída desde la base de datos se realizó a través de un Statement o de un PreparedStatement. Tampoco se puede determinar si el método .close() fue invocado antes de devolver el valor. Esto es posible con mock, estos permiten probar la integración completa de la unidad con el colaborador.

1. Prueba Unitaria → Mock
2. Prueba Unitaria → Unidad → Mock
3. La prueba unitaria verifica (assert) el resultado y el estado de la unidad
4. La prueba unitaria verifica (assert) las llamadas a los métodos en el mock

El primer paso de la prueba unitaria es crear el objeto mock y configurar los valores de retorno (1). Luego la prueba unitaria llama a la unidad que llama al mock (2). Por último se verifica el resultado de la llamada a la unidad (3). La prueba unitaria también realiza verificaciones con respecto a los métodos que fueron llamados en el mock (4).

Ejemplo utilizando mock (pseudocódigo)
public void testMock() {
  Calculadora calculadora = new Calculadora();
  GenericDAO mockGenericDao= mock(GenericDAO.class);
		
  When call (mockGenericDao.getPrimerValor()) then return(10);
  When call (mockGenericDao.getSegundoValor()) then return(11);
  
  calculadora.setDao(mockGenericDao);
		
  assertEquals(new Integer(21), calculadora.sumar());
  
  verify at least one call of mockGenericDao.getPrimerValor();
}

Objetos proxy

Los proxies son objetos mock que delegan las llamadas a los métodos en el objeto colaborador real, pero continúa registrando internamente qué métodos fueron llamados en el proxy. Los proxies permiten hacer pruebas mock con los colaboradores reales:

1. Prueba Unitaria → Colaborador
2. Prueba Unitaria → Proxy
3. Prueba Unitaria → Unidad → Proxy → Colaborador
4. La prueba unitaria verifica (assert) el resultado y el estado de la unidad
5. La prueba unitaria verifica (assert) las llamadas a los métodos en el proxy

El primer paso de la prueba unitaria es crear el objeto colaborador (1). Luego la prueba unitaria crea el proxy para el colaborador (2). La prueba unitaria crea la unidad y le setea el proxy. Ahora la prueba unitaria llama a la unidad que llama al proxy, que a su vez llama al colaborador (3). Finalmente la prueba unitaria hace las verificaciones (assert) sobre los resultados de los métodos que llama la unidad (4). La prueba unitaria también hace verificaciones sobre los métodos invocados en el proxy (5).

Conclusiones

En este primer artículo de la serie dedicada a las Pruebas Unitarias hemos realizado una introducción al tema, realizamos una descripción de las ventajas que supone su adopción, e intentamos incorporar información de utilidad verificada durante la implementación de la disciplina dentro de MicroGestion.

La introducción nos sirvió como base para describir de manera teórica la utilización de objetos mock en los casos que es necesario simular los objetos utilizados por las clases cuyo funcionamiento necesitamos verificar.

En el próximo artículo describiremos las estrategias para realizar pruebas en diferentes escenarios.

Referencias


Modificado por última vez en Lunes, 24 Febrero 2014 12:28

Acerca del autor

Andrés Sommariva

Andrés cuenta con más de diez años de experiencia liderando proyectos en diversas tecnologías, actualmente se desempeña como Gerente de Operaciones de MicroGestion Software.