cerrar-sesion editar-perfil marker video calendario monitor periodico fax rss twitter facebook google-plus linkedin alarma circulo-derecha abajo derecha izquierda mover-vertical candado usuario email lupa exito mapa email2 telefono etiqueta

400510302. Test unitarios, el futuro de la programación

Escrito por Redacción en Secciones
no hay comentarios Haz tu comentario
Imagen de logotipo de facebook Imagen de logotipo de Twitter Imagen de Logotipo de Google+ Imagen de logotipo de Linkedin

En el año 2001 Kent Beck y un grupo de expertos desarrolladores y consultores, conocidos como la ‘Alianza Ágil’ firmaron el manifiesto para el desarrollo ágil de software. En dicho manifiesto nos contaban como habían descubiertos mejores formas de desarrollar software y como podían ayudar a otros a hacer lo mismo. Esta nueva metodología es conocida como el ‘Desarrollo Ágil’ y básicamente afirma que cualquier proyecto se debe adaptar fácilmente a los siguientes factores:
– La dificultad de predecir cuales son los requisitos de software que persistirán y cuales cambiaran.
– El análisis, el diseño y la construcción no son todo lo predecibles que desearíamos
– Resulta difícil saber cuánto diseño se necesita antes de la construcción, de manera que el diseño y la construcción deben estar intercalados.

Por todo lo anterior se necesitan procesos que sean adaptables, y que vean los cambios como una oportunidad.

((Los primeros trabajos sobre esta metodología fueron publicados a principios de los 80, si bien no fue hasta 1999 cuando Kent Beck publico un trabajo sobre la materia))

A partir del modelo Ágil surgen numerosas metodologías (Programación Extrema, Desarrollo Adaptativo de software, Desarrollo de sistemas dinámicos, etc.), si bien la que ha dejado una huella más profunda es la primera.

Programación Extrema

Los primeros trabajos sobre esta metodología fueron publicados a principios de los 80, si bien no fue hasta 1999 cuando Kent Beck publico un trabajo fundamental sobre la materia. Posteriores libros de Martin Fowler y R. Jeffries expusieron los detalles técnicos del método
El proceso de la programación extrema se basa en cuatro fases que son:
-# Planificación
-# Diseño
-# Codificación
-# Prueba (de unidad y de aceptación)
Las fases 3 y 4 van en pareja, de tal forma que hay una integración continua entre la codificación y las pruebas, que dan lugar a una re-fabricación constante del código. Es muy importante que dichas pruebas estén automatizadas, de manera que puedan ejecutarse de manera fácil y repetida. Es en las pruebas de test unitarios donde nos vamos a centrar.

Test Unitarios

Los test unitarios no son ni mucho menos las únicas pruebas que existen en programación. En la Figura 1 podemos ver una típica estrategia de prueba de software.


Pero sí son las que atañen más directamente al programador. Una prueba de test unitario es una porción de código que prueba un área de funcionalidad, generalmente pequeña, del código que se desea testear. Por ejemplo podemos pensar en un test unitario que se encargue de validar si un método obtiene el número mayor de una lista de números.

Argumentos a favor y en contra

El principal argumento a favor es que hará la vida del programador más fácil, mejorando notablemente el código resultante, y reduciendo drásticamente los tiempos de depuración. Otro argumento es que dará un enfoque más divertido a la programación, sacándola de la rutina tradicional.

Los enfoques en contra tienen múltiples caras, si bien la mayoría son no son ciertos. Exponemos algunos:
-# Excesivo tiempo para codificar los test, además requiere escribir dos veces código: el código del programa y el código de los test: Este es el argumento principal en contra de los test, y es absolutamente falso. La codificación de los test debe ir en pareja a la codificación del software, de manera que sea el test el que pida el código a desarrollar, y no al revés. Si escribimos los test demasiado tarde, cuando hay lógica de negocio ya escrita, perderemos el tiempo y nuestro trabajo se volverá mucho más difícil. Si vamos desarrollando la lógica de negocio a través de los test nuestro código será mejor, y estará escrito para ser testeado.
-# Hay que perder tiempo en testear: Falso, los test se ejecutan bastante rápido, y si alguno tarda demasiado, hay que dividirlo en segmentos más pequeños.
-# Si funciona, para que testear!: Poco hay que decir a esto. Los test de caja negra nos dicen que algo funciona, pero todo programador sabe que en cualquier momento algo puede ir mal, por lo que si algo funciona ahora, no podemos garantizar que siga haciéndolo ante cualquier mínimo cambio, es ahí donde los test nos ayudaran a garantizar el buen funcionamiento de nuestro software ante los cambios.
-# No es mi trabajo testear: En el caso de los test unitarios, como ya se ha explicado antes, sí que lo es porque los test unitarios y la codificación van unidos.
-# En mi empresa no me dejan realizarlos: Es lógico que no permitan hacerlo en entornos de producción, pero si pueden hacerse a nivel local, reproduciendo esos mismos entornos de producción.

Objetivos de los test unitarios

Los beneficios principales de realizar test unitarios son tres:
-# Mejora notable del código obtenido (de más calidad, hecho para testear, muy refactorizado).
-# Reducción drástica de los tiempos de depuración, y por consiguiente del tiempo de entrega del proyecto.
-# Seguridad de que nuestro código seguirá funcionando ante cualquier cambio, y si no lo hace, sabremos muy rápidamente en donde se produce el fallo.

Parte Práctica. Comencemos con los test

Para realizar nuestro laboratorio de test utilizaremos tecnología .Net y contaremos con algunas herramientas como:
– Visual Studio 2003 ó 2005
– Nunit
– NcoverExplorer
Podemos encontrar las tres herramientas de forma gratuita, si bien Visual Studio presenta versiones de pago.

En Internet es posible encontrar numerosos frameworks para realizar tests, aunque no es necesario emplear ninguno de ellos para nuestros tests, es una manera más formal y correcta, hasta que tengamos la suficiente experiencia y motivación, como para crear nuestros propios frameworks.


El primer paso es crear un nuevo proyecto de visual Studio en nuestro equipo, lo que vamos a hacer es crear un nuevo proyecto (una biblioteca de clases) en nuestra solución de visual Studio (puede ser perfectamente una aplicación Windows, web, de consola o simplemente una biblioteca de clases). Dentro de este proyecto creamos una clase, que será cuyo funcionamiento vamos a comprobar. En la Figura 2 hay un ejemplo.

En este punto solo nos falta crear el proyecto de test (una biblioteca de clases) y hacer referencia a nuestra DLL que contiene el framework de test (lo tendremos en la carpeta donde hayamos instalado Nunit), y otra referencia al proyecto que queremos probar.

De tal forma que podríamos tener una solución estructurada de la forma que se ve en la Figura 3.

Funcionamiento del framework de tests. Explicación de los ‘Asserts’

En este punto se hace imprescindible una introducción al uso de nuestro framework de tests. Si bien comentábamos antes que no es necesario un framework de tests, si que es conveniente utilizar alguno. Nuestro framework nos permite ver si un método está funcionando correctamente a través unos métodos conocidos como ‘assertions’ (afirmaciones).

En efecto, las pruebas no son más que afirmaciones sobre el funcionamiento o comportamiento esperado del código. La implementación de estas afirmaciones se realiza mediante los mencionados “Asserts”, que lanzan una excepción cuando no se cumple la regla o condición afirmada. Hay asserts de diferentes tipos, todos ellos lanzaran una excepción si no se cumple una determinada condición (Tabla 1).

Preparación de la clase de test. Primeros Test

Para preparar nuestra clase de test supongamos que necesitamos que nuestra clase de negocio tenga un método que devuelva el valor máximo dados tres valores.

El organigrama de desarrollo que debemos seguir es el reflejado en la Figura 4.

Como hemos comentado anteriormente, primero viene el test y luego el código, y no al revés, como tradicionalmente se ha venido haciendo.

Paso 1

Nuestra clase TestBasico es una clase de test, y por tanto tiene que ser marcada como tal, para ello hay que insertar el atributo de test:

Imports NUnit.Framework

_

Public Class TestBasico

End Class

Esto hará que el programa la reconozca como una clase de test.

Codificamos también nuestra clase a testear (con el código mínimo para poder compilar el proyecto):

Public Class ClaseATestear

Public Function DevuelveMaximoValor(ByVal Valor1 As Integer, ByVal Valor2 As Integer, ByVal Valor3 As Integer) As Integer

Dim ValorMaximo As Integer

Return ValorMaximo

End Function

End Class

Paso 2

Ahora volvemos a nuestra clase de test y añadimos algo de código para que pueda testear el funcionamiento:

Imports NUnit.Framework

Imports CapaDeNegocio

_

Public Class TestBasico

Dim ClaseATestear As ClaseATestear

Public Sub InicializarClase()

ClaseATestear = New ClaseATestear

End Sub

Public Sub ValorMaximoCorrecto()

Dim valor1, valor2, valor3 As Integer

variable1 = 5

variable2 = 9

variable3 = 3

Assert.AreEqual(9, ClaseATestear.DevuelveMaximoValor( valor1, valor2, valor3), “El valor recibido no es el valor máximo”)

End Sub

Public Sub LiberarRecursos()

ClaseATestear = Nothing

System.GC.Collect()

End Sub

End Class

Podemos ver que hemos introducido cuatro métodos. Los métodos marcados con los atributos (Setup) y (TearDown) se inician al inicio y al final de cada test respectivamente. Lo que hacemos es crear el objeto al inicio y liberar los recursos al final. Ambos métodos son declarados como públicos ya que necesitamos que puedan ser accesibles desde fuera de la clase, si no obtendríamos un error al iniciar los test.

((En Internet es posible encontrar numerosos frameworks para realizar tests, aunque no es necesario emplear ninguno de ellos para nuestros tests))

El método valor máximo correcto tiene un método assert que comprueba que el valor esperado coincide con el valor actual, y en caso de que no coincida lanzara una excepción con el mensaje que hemos parametrizado.

Llega la hora de probar nuestro código de test. Para ello, abrimos el programa Nunit que hemos instalado. Esto lo podemos hacer también configurando Visual Studio para que lo arranque al iniciar la compilación, pero como nuestro objetivo no es depurar el código no utilizaremos esta opción).

Pulsamos en File/New Project, introducimos un nombre para nuestro proyecto y guardamos. Luego pulsamos en Proyect/Add Assembly, y seleccionamos la DLL del proyecto de test (véase Figura 5)
Nuestra aplicación tendría un aspecto igual al de la Figura 6.

En el árbol de la izquierda podemos ver la clase de test con sus métodos. A la derecha tenemos el botón Run que dará comienzo a los test. Los test que pasen correctamente serán marcados en verde, los que fallen en rojo, y los que son ignorados en amarillo.

Pulsamos en Run y obtenemos el resultado de la Figura 7.


Efectivamente el test ha fallado, ya que nuestra clase de negocio no está haciendo ningún trabajo.

Paso 3

Completamos el código de la clase de negocio para nuestra clase haga su trabajo y el test pase. Nuestro método recibirá tres valores y nos devolverá el mayor de ellos, y en caso de empate el que este situado antes en la lista.

((Las pruebas no son más que afirmaciones sobre el funcionamiento o comportamiento esperado del código))


Public Class ClaseATestear

Public Function DevuelveMaximoValor(ByVal Valor1 As Integer, ByVal Valor2 As Integer, ByVal Valor3 As Integer) As Integer

Dim ValorMaximo As Integer = Valor1

CompararValorMaximo(ValorMaximo, Valor2)

CompararValorMaximo(ValorMaximo, Valor3)

Return ValorMaximo

End Function

Private Sub CompararValorMaximo(ByRef ValorMaximo As Integer, ByVal ValorAComparar As Integer)

If ValorAComparar > ValorMaximo Then ValorMaximo = ValorAComparar

If ValorAComparar > ValorMaximo Then ValorMaximo = ValorAComparar

End Sub

End Class

Y volvemos a ejecutar el test. El resultado es el de la Figura 8.


Ahora nuestro test ha pasado correctamente, podemos estar seguros de que nuestro código va por buen camino. Ya hemos realizado nuestro primer test, ahora introducimos un bug en nuestro método del proyecto de negocio, y veremos como el test nos avisa de ello.


Private Sub CompararValorMaximo(ByRef ValorMaximo As Integer, ByVal ValorAComparar As Integer)

If ValorAComparar > ValorMaximo Then ValorMaximo = ValorAComparar

If ValorAComparar > ValorMaximo Then ValorMaximo = ValorAComparar

ValorMaximo = 0

End Sub

Hemos introducido ValorMaximo=0, lo cual provocar que nuestro método devuelva cero en lugar del valor máximo que espera el test. Volvemos a ejecutar el test con el resultado de la Figura 9.

Perfecto, nuestro método falla y nos avisa con el tipo error que ha encontrado. Eliminamos la línea que habíamos introducido y nuestro test vuelve a pasar correctamente.

((Cuando desarrollamos código es normal que el cliente, ó alguien que este testeando nuestra aplicación, nos informen de algún bug))

¿Cómo seguir desde aquí? Ahora deberíamos plantearnos que test son los necesarios para podamos asegurar que nuestro código funciona bien, esto no está escrito en ningún sitio, pero os puedo dar algunas ideas:
– ¿Se reciben los valores en un determinado formato (enteros, fechas, símbolos, etc.)?
– ¿Funciona con lógica inversa, (2+ 5= 5 +2, raíz cuadrada de 16= 2 al cuadrado)?
– ¿Se reciben los valores en el orden adecuado?
– ¿Están dentro de un máximo y un mínimo?
– ¿Hacen referencia a elementos externos que escapan de nuestro control?
– ¿Existen esos valores (son nulos, etc.)?
– ¿Hay valores suficientes para que nuestro código trabaje?
– ¿Ocurre todo en el orden que esperamos?
– ¿Si ocurre un error ajeno al código, funcionara como esperamos?
– ¿Y por ultimo son los resultados correctos
Por supuesto habrá muchos más test, tantos como nuestra lógica de negocio pida. Pero al final nuestro código será mucho más robusto, y podrá ser revisado de manera automatizada.

Corrección de Errores

Evidentemente nuestros tests no abarcaran toda la lógica posible en las primeras fases de desarrollo y en este punto hay que hacer una aclaración muy importante. Cuando desarrollamos código es normal que el cliente, ó alguien que este testeando nuestra aplicación, nos informen de algún bug o fallo en el comportamiento de nuestro programa cuando sucede algo determinado. Normalmente cuando pensamos que algo ha ido mal, o que algo podría ir mal, depuramos el programa y hacemos un seguimiento para ver donde se comporta de manera inesperada. En el caso de los test cuando algo falla, hay arreglar el bug de la siguiente forma:
-# Identificar el fallo
-# Escribir un test que demuestre ese fallo,
-# Corregir el código y volver a pasar el test hasta que vaya todo correcto.

No es aconsejable cuando un test falla, escribir nuevo código y nuevos test sin haber solucionado el anterior.

Excepciones esperadas

Otros tipos de test son los que se van a encargar de detectar que nuestro programa lanza un determinado tipo de excepción cuando algo va mal. Por ejemplo cuando nuestro método espera un número y en su lugar recibe una letra. Ampliemos el método Obtener valor máximo para que examine si los tres valores son numéricos, y en caso contrario lance una excepción determinada:


Public Class ClaseATestear

Public Function DevuelveMaximoValor(ByVal Valor1 As Integer, ByVal Valor2 As Integer, ByVal Valor3 As Integer) As Integer

Dim ValorMaximo As Integer = Valor1

CompararValorMaximo(ValorMaximo, Valor2)

CompararValorMaximo(ValorMaximo, Valor3)

Return ValorMaximo

End Function

Private Sub CompararValorMaximo(ByRef ValorMaximo As Integer, ByVal ValorAComparar As Integer)

If ValorAComparar > ValorMaximo Then ValorMaximo = ValorAComparar

If ValorAComparar > ValorMaximo Then ValorMaximo = ValorAComparar

ValorMaximo = 0

End Sub

Public Function ValoresSonNumeros(ByVal Valor1 As Object, ByVal Valor2 As Object, ByVal Valor3 As Object) As Boolean

EsDigito(Valor1)

EsDigito(Valor2)

EsDigito(Valor3)

End Function

Private Function EsDigito(ByVal valor As String) As Boolean

If Not Char.IsNumber(valor) Then Throw New ValoresNoNumericosException

End Function

Public Class ValoresNoNumericosException

Inherits ApplicationException

End Class

End Class

Vemos como ahora el método ‘ValoresSonNumeros’ comprobara que los valores son numéricos y lanzara una excepción en caso contrario. Como queremos ser específicos en nuestros test con las excepciones que lanzamos, hemos creado una clase anidada que hereda de ‘Application Exception’.

((El test ha ido bien, eso quiere decir que nuestro código lanzara una excepción cuando detecte que se le pasan valores que no son número))

Ahora volvemos a nuestro proyecto de test, e introducimos un método que efectivamente compruebe que se lanza una excepción cuando alguno de los valores no es un número. Para ellos cambiamos el tipo aceptando valores de cadena. Este error es típico cuando se lee un valor equivocadamente:


Public Sub ValoresConFormatoNoValidos()

Dim valor1, valor2, valor3 As String

CargarDatosMinimos(valor1, 55, valor2, 9, valor3, 3)

valor3 = “email@email.com”

ObjectTest.ValoresSonNumeros(valor1, valor2, valor3)

End Sub

Vemos como el valor3 es erróneo. Lanzamos el test para ver si nuestro código dispara la excepción como esperamos que haga (véase la Figura 10).

Efectivamente el test ha ido bien, eso quiere decir que nuestro código lanzara una excepción cuando detecte que se le pasan valores que no son números. Estos tipos de test de excepciones esperadas son muy útiles sobre todo en el desarrollo de la lógica de negocio.

Implementación de un objeto de test. Mock Objects

Llegados a este punto, es necesario ser un poco más formales en nuestros test. Uno de los problemas fundamentales que nos podemos encontrar es que nuestro código depende de otras partes del sistema, por ejemplo:
– Alguien cambia una interface o una tabla de una base de datos, y de repente nuestro test deja de funcionar.
– El objeto real que queremos testear aun no está configurado ó no existe.
– El objeto real es lento o tiene una interfaz de usuario.

Para resolver este problema existen los Mock Objects. Estos por poner un símil con el mundo real son como los dobles de las películas de televisión. En detalle los Mock Objects son patrones de test que nos ayudan en la labor de reemplazar un objeto del mundo real. Lo hacen de la siguiente forma:
-# Usamos una interfaz que describe el objeto
-# Implementamos la interfaz para el código de producción
-# Implementamos la interfaz en el MockObject del test
Siguiendo con nuestro proyecto, vamos a implementar un MockObject. Supongamos que necesitamos recuperar la fecha de la reserva para procesarla, y para ello empezaremos implementando la interfaz en una clase llamada interfaces:


Public Class Interfaces

Public Interface IFechaReserva

Function FechaLimiteReserva() As Date

End Interface

End Class

Vamos a añadir una clase con los datos de configuración del entorno (Fecha), que implemente dicha interfaz:


Public Class Configuracion

Implements Interfaces.IFechaReserva

Public Function FechaLimiteReserva() As Date Implements Interfaces.IFechaReserva.FechaLimiteReserva

Return Now.Date

End Function

End Class

Y crearemos otra clase que servirá de MockObject, y que también implementara la interface:


Public Class MockObjectConfiguracion

Implements CapaDeNegocio.Interfaces.IFechaReserva

Dim FechaLimite As Date

Sub New(ByVal Fecha As Date)

FechaLimite = Fecha

End Sub

Public Function FechaLimiteReserva() As Date Implements CapaDeNegocio.Interfaces.IFechaReserva.FechaLimiteReserva

Return FechaLimite

End Function

End Class

Podemos ver como ambas clases han codificado de manera distinta el método de la interfaz, ya que el objeto real necesita trabajar con la fecha actual, y el MockObject trabajara con una fecha que vendrá determinada por la necesidad del test, ya que este trabajara con datos estáticos. Ahora solo nos queda implementar el método que valide la fecha en nuestra ‘ClaseATestear’ de la capa de negocio:


Public Function FechaReservaCorrecta(ByVal Entorno As Interfaces.IFechaReserva, ByVal FechaSolicitada As Date)

Return FechaSolicitada <= Entorno.FechaLimiteReserva End Function

((Podemos ver como ambas clases han codificado de manera distinta el método de la interfaz))

Destaca el hecho de que tenemos el parámetro entorno que viene determinado por la interface, lo que hará que pueda funcionar tanto con el objeto real como con el MockObject. Ahora escribimos el test que probara el método anterior (véase Figura 11).

¡Error! ¿Qué ha ocurrido? Si observamos el mensaje vemos que nuestro código no procesa una fecha que si esperamos que lo haga. Observando el código vemos que al crear el MockObject pusimos como fecha el ‘01/05/2007’ (New MockObjectConfiguracion (“01/05/2007”)), y posteriormente solicitamos como fecha de reserva el “20/05/2007”, por lo que nuestro código de test está mal. Corregimos la primera fecha de esta forma:


Public Sub FechaSolicitadaCorrecta()

Dim Entorno As New MockObjectConfiguracion(“01/06/2007”)

Dim FechaSolicitada As Date = “20/05/2007”

Dim FechaCorrecta As Boolean = ObjectTest.FechaReservaCorrecta(Entorno, FechaSolicitada)

Assert.IsTrue(FechaCorrecta, “No procesa fecha inferior al limite de reserva”)

End Sub

Y lanzamos otra vez el test como en la Figura 12.

(Multitarea en el Framework .NET

Algunas cualidades que deben tener los buenos test son:
- Automáticos.
- Fáciles de repetir: Deben ejecutarse rápido.
- Profesionales: Con la misma calidad en su código que el código de producción.
- Minuciosos.
- Cortos.
- Independientes: Podemos valernos de ficheros XML para recuperar datos, o de SnapShots (copia de solo lectura de la base de datos original) para trabajar.
- Refactorizador: Dara lugar a que nuestro código este muy refactorizado, y por lo tanto sea mas legible por cualquier otro programador.

)]

Y ahora vemos que el código funciona como esperamos
Hay que tener cuidado con el uso de los MockObjects, ya que estos representan un coste para nuestros proyectos, y en algunos casos se elimina la necesidad de utilizarlos simplemente refactorizando el código. Por lo tanto antes de utilizarlos hay que preguntarse si realmente es necesario.

Cobertura del test. NCoverExplorer

Otra herramienta útil para los test es NcoverExplorer, este es una interfaz gráfica para ncover y nos permite ver que parte del código esta testeado y que parte no lo está, así como la cobertura general del proyecto. Su uso sólo requiere una pequeña configuración. Lanzamos el programa y pulsamos en File/Run Ncover (Figura 13).

Pulsamos en Run y vemos el resultado del análisis de cobertura (véase la Figura 14).


Vemos que hay métodos que tienen una cobertura del 100%. En el caso de la clase de negocio llega a una cobertura del 92%. Cuando la cobertura sea inferior nos marca el código que no ha sido visitado. En este caso vemos que ValorMaximo=ValorAComparar no es visitado en la segunda ocasión, (lo que es lógico debido a que nuestro orden de parámetros es (5, 9, 3) y por lo tanto 3 al no ser mayor que 9 evita que se entre en esa línea de código. Al mismo tiempo vemos que nuestra función ValoresSonNumeros no retorna un valor, por lo que podríamos haberla codificado como una rutina.

[(
-Visual Studio 2003 ó 2005 (Podemos encontrar una versión de la 2005 gratuita en [www.Microsoft.es
).
- Nunit (www.nunit.org). Framework para test, disponible para el framework 1 ó 2 de .Net.
- NcoverExplorer. Utilidad para cobertura de test. Disponible en: ( http://www.kiwidude.com/dotnet/DownloadPage.html )

)]

En definitiva toda esta información nos puede ayudar a ver que parte del código deberíamos testear o mejorar, y que grado de confianza podemos tener en nuestro desarrollo.

El futuro

En el futuro cabe esperar que los test unitarios se acaben imponiendo como metodología de trabajo, si bien su uso ya está extendido en las grandes empresas y consultoras de software, cabe esperar que las de nivel medio también lo adopten. Los test unitarios permitirán abordar tareas muy complejas en otras triviales, y crear un código mucho más saludable, robusto y eficaz.

Noticias relacionadas

Comentarios

No hay comentarios.

Deja un comentario

Tu dirección de correo electrónico no será publicada. Los campos necesarios están marcados *

Debes haber iniciado sesión para comentar una noticia.