Cómo completar un plan de pruebas para la suma de dos números utilizando JUnit.

Por descargarpseint / hace 5 meses / 0 Comentarios ».

Hoy quiero seguir hablándote del plan de pruebas haciendo uso de JUnit y los test unitarios. Para ello vamos a  resolver juntos algunos de los ejercicios que te propuse en el artículo anterior y desde este punto seguir evolucionando la calculadora para que sea capaz de sumar números de 64 bits en java. Si continuas leyendo podrás comprobar que no solo intento ofrecerte una solución, sino también una posible metodología de programación que pueda ayudarte a resolver los problemas por ti mismo.

En el último artículo del curso Tutorial en Java Programar una calculadora paso a paso te expliqué como probar la calculadora a partir de pruebas unitarias. En el te dejé un par de ejercicios para que intentaras resolverlos y hoy te voy a presentar una posible solución a los mismos. En el mundo de la programación suelen existir varias soluciones para resolver un mismo problema, yo intento ofrecerte soluciones profesionales, sin embargo, el proceso de esfuerzo y razonamiento que requiere llegar a una solución no tiene precio y es lo primero que exigen las empresas que se dedican al desarrollo del software.

Ejercicio 1: Programar el Test que prueba que el resultado de  sumar un número y el cero es ese número.

        @Test

        public void testQuinientosMasCeroIgualAQuinientos(){

               Number sumar;

               boolean sumarIgualAQuinientos;

               sumar = calculadora.sumar(500, 0);

               sumarIgualAQuinientos = (sumar.intValue() == 500);

               Assert.assertTrue(sumarIgualAQuinientos);

        }

Como puedes ver la solución es muy parecida a la que vimos para el caso de prueba 0+0=0. A pesar de ello he programado el método línea a línea lo que me ha permitido darme cuenta de las similitudes entre ambos códigos.

Esta solución es correcta porque funciona y cumple su cometido, sin embargo, desde el punto de vista de calidad de software es posible mejorar la implementación utilizando la refactorización de código. Esto lo veremos en el próximo artículo utilizando este mismo ejemplo para que puedas tener una visión mas completa y real de lo que comprende el ciclo de programación de cualquier pedazo de código.

Ejercicio 2: Programar el Test que prueba que el resultado de sumar un número y su inverso dará como resultado cero.

        @Test

        public void testQuinientosMasSuInversoIgualACero(){

               Number sumar;

               boolean sumarIgualACero;

               sumar = calculadora.sumar(500, -500);

               sumarIgualACero = (sumar.intValue() == 0);

               Assert.assertTrue(sumarIgualACero);

        }

Lo dicho en el apartado anterior. Uno de los objetivos de un programador eficiente y productivo es la de encontrar patrones de código que se repitan en las soluciones que va desarrollando. Aquí hemos encontrado uno y a partir de aquí nuestro trabajo es encontrar la mejor forma de explotarlo incorporándolo en la resolución del mayor número de problemas.

Ejercicio 3: Hacer cambios en la calculadora para que el Test que suma un número muy grande con quinientos de como resultado un número positivo.

Este problema que te proponía en el artículo anterior como ejemplo JUnit no tiene nada que ver con la construcción del test unitario sino mas bien con las limitaciones en el rango de valores de la clase Integer (entero de 32 bits complemento a dos con rango [-2,147,483,648 a  2,147,483,647]). Por ello te ponía como requisito no modificar el test ya que este si estaba cumpliendo con su responsabilidad de comprobar que la suma de dos números positivos es otro número positivo.

        @Test

        public void testQuinientosMasNumeroGrandeEsMayorQueCero(){

                Number sumar;

               boolean sumaMayorQueCero;

               sumar = calculadora.sumar(500, 2147483647);

               sumaMayorQueCero = (sumar.intValue()>0);

               Assert.assertTrue(sumaMayorQueCero);

        }

La solución a este problema consiste en dotar a nuestra calculadora de la capacidad de sumar números grandes. El tipo de dato encargado de realizar esta función en java es long y su clase envoltorio Long. Existe una relación de herencia entre Number y Long y gracias a ello nuestra interfaz Calculadora no va a tener que sufrir ningún cambio.

public interface Calculadora {

        Number sumar(Number operando1, Number operando2);

}

En cambio, la implementación que estamos haciendo de ella si. Aquí surgen diferentes posibilidades para resolver el problema. Primero vamos a ver el código de la clase CalculadoraDeIntegers.

public class CalculadoraDeIntegers implements Calculadora {

        public Number sumar(Number operando1, Number operando2) {

               return Integer.valueOf(operando1.intValue()+operando2.intValue());

        }

}

La primera consistiría en modificar el código del método sumar para que sea capaz de devolver un Long, en lugar de un Integer, cuando el resultado de la suma sea un número muy grande.

        public Number sumar(Number operando1, Number operando2) {

               Number suma = Integer.valueOf(operando1.intValue()+operando2.intValue());

               boolean sumaEsNegativa = suma.intValue()<0;

               boolean operando1EsPositivo = operando1.intValue()>0;

               boolean operando2EsPositivo = operando2.intValue()>0;




               if(sumaEsNegativa){

                       if(operando1EsPositivo && operando2EsPositivo){

                               //Se ha producido un desbordamiento. Uno de los numeros

                               //es muy grande. Vamos a utilizar la clase Long.

                               suma = Long.valueOf(operando1.longValue()+operando2.longValue());

                       }

               }

               return suma;

        }

Como podrás ver el código ha cambiado significativamente. La solución funciona y cuando estás trabajando muchas veces tienes que conformarte con esto, es lo que se espera de ti, y no hay ni tiempo ni dinero para buscar mejores alternativas.

¿Qué cosas no me gustan de esta solución?.

Lo primero es que hemos extendido la responsabilidad de CalculadoraDeIntegers. Como muy bien describe su nombre, esta clase nació con la misión de realizar las operaciones con enteros de 32 bits. Yo al pretender añadirle funcionalidad para satisfacer una nueva necesidad he violado este principio y el resultado es obvio y te lo describo a continuación.

Lo segundo que  no me gusta es que la complejidad del método sumar ha crecido considerablemente. Existen grandes defensores en la profesión de los métodos largos que encapsulan código que cumple con diferentes responsabilidades, sin embargo, un servidor no es uno de ellos y la experiencia me lleva diciendo los últimos 8 años que algo de razón llevo.

¿Cómo puedo valorar que el nuevo método es mas complejo?.

Lo primero es que a la vista habrás notado la gran diferencia. Esto que parece una manía estética, por mi parte, mas que otra cosa supone un desgaste psicológico a la larga. Ver un código sucio, una y otra vez, desconcentra y dificulta encontrar potenciales errores que puedan aparecer en un futuro.

Puedes seguir contando el número de líneas de una solución y la otra. La original era una línea de código mientras que la segunda son ocho.

Y lo mas importante son las operaciones (asignaciones, conversiones y sobre todo comparaciones) que hay en una y la otra solución. Estos tres elementos son básicos en cualquier lenguaje de programación, pero a la vez, son el origen de la gran mayoría de errores que se producen.

Existen nuevamente programadores expertos que argumentan que con conocimiento y prestando atención es suficiente, sin embargo, el código que hoy desarrollas tu no sabes quien lo tendrá que modificar mañana. ¿Estará el tan concentrado y tendrá tanto conocimiento como tú?.

Aplicando el paradigma de la programación orientada a objetos.

Ahora voy a presentarte otra solución mucho mas simple, que funciona igualmente y que está estrechamente relacionada con los principios de programar con objetos y que cada clase cumpla con una única responsabilidad.

Para ello voy a definir otra clase que implemente la calculadora limitando su responsabilidad a operar con enteros de 64 bits, es decir, con números de tipo Long.

package com.programarenjava.tutoriales.calculadora.implementaciones;

import com.programarenjava.tutoriales.calculadora.interfaces.Calculadora;

public class CalculadoraDeLongs implements Calculadora {

        public Number sumar(Number operando1, Number operando2) {

               Number sumar = Long.valueOf(operando1.longValue()+operando2.longValue());

               return sumar;

        }

}

Puedes ver que la clase implementa el interfaz Calculadora y por lo tanto se responsabiliza de implementar el contrato definido por la misma.

Si te fijas en el código del método sumar puedes ver que es mucho mas sencillo, simple y claro. En realidad es muy parecido al código original de la clase CalculadoraDeIntegers con dos cambios fundamentales.

  • Uso del método estático valueOfde la clase Long en lugar de la clase Integer.
  • Uso del método longValuede la clase Number en lugar de intValue. De este modo sumamos longs en lugar de integers y así evitamos el desbordamiento de rango.

Ahora vamos a probar el nuevo código en el test que nos estaba fallando. Antes hay que realizar dos pequeños cambios que te muestro a continuación:

private Calculadora calculadora = new CalculadoraDeLongs();

//En lugar de

private Calculadora calculadora = new CalculadoraDeIntegers();

//Y también la condición de éxito

sumaMayorQueCero = (sumar.longValue()>0);

//en lugar de

sumaMayorQueCero = (sumar.intValue()>0);


Si realizas ambos cambios en el Test y vuelves a ejecutarlo te debería decir que todas las pruebas fueron bien. Ahora, antes de terminar, quiero explicarte en que consisten dichos cambios.

El primero es evidente, sustituimos una clase por la otra y el impacto en la clase de test es mínimo. Estas son algunas de las ventajas de realizar un buen diseño antes de ponerse a programar en java. Coste del cambio mínimo.

El segundo tiene que ver con las limitaciones del los enteros de 32 bits ya mencionadas en el transcurso del artículo. Aunque nuestra calculadora devuelve un long, en realidad, lo está devolviendo envuelto en una variable de tipo Number. Cuando realizamos la llamada a sumar, java está transformando el long en un Number y si queremos volver a tener el long tendremos que hacer uso del método encargado de ello (longValue).

Hasta aquí el capitulo de hoy. En el has visto la utilidad de los test unitarios en el sentido que te permiten validad la coherencia de código que podría sufrir cambios por la aparición de nuevas necesidades. También has experimentado como la calculadora está evolucionando para ir satisfaciendo nuevos requisitos. A partir de ahora seguirá siendo así para que puedas sentir como crece el desarrollo paso a paso. Nos vemos en la próxima entrega y mientras tanto puedes ponerte en contacto conmigo si tienes alguna duda o sugerencia.

Deja un comentario

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