Sei sulla pagina 1di 40

UNIVERSIDAD MAYOR DE SAN SIMON

FACULTAD DE CIENCIAS Y TECNOLOGÍA


DIRECCIÓN DE POSGRADO

“ Las mejores prácticas para los programadores


aplicando
unit test y code coverage”

TRABAJO FINAL PRESENTADO PARA OBTENER EL CERTIFICADO


DE DIPLOMADO EXPERTO EN DESARROLLO DE APLICACIONES
EMPRESARIALES VERSIÓN I
.

POSTULANTE : Guido Rolando Llanos Zela


TUTOR : Lic. Valentin Laime Zapata

Cochabamba – Bolivia
2018

1
Índice de contenido
Resumen ________________________________________________________________________________________________ 4
INTRODUCCIÓN ________________________________________________________________________________________ 5
1.1. Antecedentes _________________________________________________________________________________________ 5
2. TEST DRIVEN DEVELOPMENT __________________________________________________________________ 6
2.1. Ciclo de Test Driven Development _______________________________________________________________ 6
2.1.1. Red __________________________________________________________________________________________________________ 6
2.1.2. Green ________________________________________________________________________________________________________ 7
2.1.3. Refactor _____________________________________________________________________________________________________ 7

2.2. Las tres leyes de TDD _______________________________________________________________________________ 8


3. PRINCIPIOS S.O.L.I.D. ____________________________________________________________________________ 9
3.1. Single Responsability Principle (SRP) __________________________________________________________ 9
3.2. Open-Closed Principle (OCP) ___________________________________________________________________ 11
3.3. Liskov Substitution Principle (LSP) ___________________________________________________________ 12
3.4. The Interface Segregation Principle (ISP) ___________________________________________________ 16
3.5. Dependency-Inversion Principle (DIP) _______________________________________________________ 19
4. PRUEBAS UNITARIAS ___________________________________________________________________________ 21
4.1. Porqué escribir pruebas unitarias_____________________________________________________________ 21
4.1.1. Reducir el tiempo invertido para ejecutar las pruebas de funcionalidad _________________________ 21
4.1.2. Reducir los bugs de regresión __________________________________________________________________________ 22
4.1.3. Mejorar la calidad de código ____________________________________________________________________________ 22
4.1.4. Ayuda a comprender el requerimiento ________________________________________________________________ 22

4.2. Características de una buena prueba unitaria ______________________________________________ 22


4.3. Organizar nuestro código para realizar pruebas unitarias. ______________________________ 23
4.4. Las cuatro fases de una prueba unitaria _____________________________________________________ 24
4.5. Buenas prácticas para pruebas unitarias ____________________________________________________ 26
4.5.1. Nombrar correctamente nuestras pruebas ___________________________________________________________ 26
4.5.2. Evitar el uso de hard code en las pruebas unitarias _________________________________________________ 26
4.5.3. Evitar el uso de lógica dentro de las pruebas unitarias ______________________________________________ 27
4.5.4. Usar métodos auxiliares para configurar y liberar recursos ________________________________________ 28
4.5.5. Validar métodos privados a través de métodos públicos ___________________________________________ 29
4.5.6. Evitar referencias estáticas o factores no determinanticos _________________________________________ 29

4.6. Señales para identificas código complicado de probar ___________________________________ 30


4.6.1. Clases del tipo Singleton _________________________________________________________________________________ 30
4.6.2. Métodos estáticos ________________________________________________________________________________________ 32

5. CODE COVERAGE ________________________________________________________________________________ 34


5.1. Medidas de cobertura ____________________________________________________________________________ 34
5.1.1. Cobertura de clase________________________________________________________________________________________ 34
5.1.2. Cobertura de método ____________________________________________________________________________________ 34
5.1.3. Cobertura de sentencia __________________________________________________________________________________ 35
5.1.4. Cobertura de bloque _____________________________________________________________________________________ 35
2
5.2. Consideraciones sobre Code Coverage _______________________________________________________ 37
5.2.1. Interpretar correctamente los reportes _______________________________________________________________ 37
5.2.2. Cubrir nuestro código no es fácil _______________________________________________________________________ 37
5.2.3. Las herramientas de Cobertura de Código verifica código existente ______________________________ 38

6. CONCLUSIONES __________________________________________________________________________________ 39
7. BIBLIOGRAFÍA ___________________________________________________________________________________ 40

Índice de Figuras

Figura 1: Red, Green, Refactor cicle [1] ................................................................................... 6


Figura 2: Clase con varias responsabilidades .......................................................................... 9
Figura 3: Interfaces con una solo responsabilidad .................................................................. 10
Figura 4: Diagrama de clases fuertemente acoplado .............................................................. 11
Figura 5: Diagrama de clases con bajo acoplamiento ............................................................ 12
Figura 6: Diagrama de clases que no cumple con LSP .......................................................... 13
Figura 7: Diagrama de clases que cumple con LSP ............................................................... 15
Figura 8: Diagrama de clase que no cumple con ISP ............................................................. 17
Figura 9: Diagrama de clases que cumple con ISP ................................................................ 18
Figura 10: Diagrama de clases que con cumple con DIP ....................................................... 19
Figura 11: Diagrama de clases que cumple con DIP .............................................................. 20
Figura 12: Tipos de pruebas .................................................................................................. 21
Figura 13: Código y pruebas en el mismo paquete. [3] ........................................................... 23
Figura 14: Pruebas dentro de un paquete especifico por ruta. [3] ........................................... 24
Figura 15: Paquetes independientes para código y pruebas. [3] ............................................. 24
Figura 16: Resultado de la cobertura de código incompleta ................................................... 36
Figura 17: Reporte de la cobertura de código incompleta ....................................................... 36
Figura 18: Resultado de la cobertura de código completo ...................................................... 37
Figura 19: Reporte de la cobertura de código completo ......................................................... 37

3
Resumen

El proceso de desarrollo de software durante los últimos años se ha inclinando por los procesos
de desarrollo ágiles porque estos procesos se adaptan al cambio; el cambio constante de los
requerimientos genera que el código este en constante cambio por lo que también las pruebas
sufren cambios, el test de regresión ejecutado por el equipo de control de calidad de software
puede tener como resultado algunos bugs. El test de regresión es un proceso crítico que
consume bastante tiempo dependiendo de los pasos que tienen que seguir para probar una
funcionalidad, existe mucha probabilidad de que alguna funcionalidad se vea afectada por el
cambio de algún requerimiento.

Los desarrolladores se dieron cuenta de que esto era un gran problema porque si el equipo de
calidad de software no detecta las fallas estos llegarían hasta el cliente. Extreme Programing
una de las metodologías ágiles adoptó el desarrollo de software guiado por pruebas Test Driven
Development (TDD), el cual indica que se tiene que crear una prueba que falle antes de
empezar a programar, esta prueba garantiza que el código funcione correctamente; ante
cualquier cambio de funcionalidad primero se tiene que cambiar o crear una prueba para luego
empezar a codificar el cambio, estas pruebas pueden ejecutarse las veces que uno desee
porque no depende del ejecutor, esto agrega un aspecto de calidad mas al software
desarrollado porque por cada cambio se correrán las pruebas y se detectará la o las
funcionalidades afectadas. Estas pruebas son independientes al test de regresión que el equipo
de calidad de software tiene que realizar porque incluso aplicando TDD puede que aun se
detecten bugs en la funcionalidad pero la posibilidad de entregar funcionalidades libres de bugs
aumenta.

Los cinco principios SOLID un principio por cada letra; Single Responsability Principle (SRP),
Open-Closed Principle (OCP), Liskov Substitution Principle (LSP), The Interface Segregation
Principle (ISP) y el último Dependency-Inversion Principle (DIP). Estos principios hacen que el
código tenga cualidades que facilitan la generación de pruebas unitarias.
Code Coverage es una métrica que ayuda a identificar clases, métodos, bloques y sentencias
de código a las que no se llegaron a cubrir con las pruebas unitarias.

4
INTRODUCCIÓN
El proceso de desarrollo de software ha avanzado bastante al adoptar los procesos agiles como
metodología de desarrollo, acelerando los tiempos de entrega y una adaptación razonable al
cambio. Las metodologías ágiles introdujeron cambios en varias etapas del desarrollo de
software orientados en el diseño, implementación y pruebas. Estas tres etapas mencionadas
están involucradas dentro de Test Driven Development (TDD).

TDD es una práctica de programación que consiste en escribir pruebas antes de empezar a
producir código para que pasen las pruebas (estas dos acciones involucran las etapas de
desarrollo y pruebas). Una vez que el código pase la prueba satisfactoriamente se ingresa en la
etapa de refactorización donde se elimina el código duplicado o se cambia el diseño del código
(esta acción se puede entender como diseño).

1.1. Antecedentes
El desarrollo de software tiene varios problemas sin importar la metodología que se aplique
para el desarrollo de software. El entender correctamente el requerimiento antes de empezar el
desarrollo es importante, las comprensiones parciales del requerimiento genera código con
funcionalidades defectuosas que se desencadenan como bugs en el proceso de testeo, por
tanto se invierte mucho más tiempo y dinero para la corrección de una funcionalidad mal
implementada.

Los cambios de requerimientos involucran cambios en código que ya estaba funcionando


correctamente, este tipo de implementación son las causantes de bugs que son catalogados
como bugs de regresión, algo que consume nuevamente recursos para resolverlos.
El diseño aplicado al proceso de desarrollo es mínimo porque el desarrollador solo trata de que
el código trabaje como el entendió el requerimiento y no como debería de serlo, nuevamente un
mal diseño hace que el código no sea reusable ni mantenible lo que lleva a realizar cambios
grandes en la arquitectura por el cambio del requerimiento aceptado.

5
2. TEST DRIVEN DEVELOPMENT

El libro de Kent Beck Extreme Programing donde se menciona el Test Driven Development TDD
expresados bajo dos reglas simples:
 Nunca escribir una línea de código sin primero escribir una prueba automática que falle.
 No escribir pruebas repetidas.

Lo que prácticamente resulta en ejecutar los siguientes pasos en el desarrollo:


 Escribir una prueba que falle.
 Escribir el código necesario para que pase la prueba.
 Refactorizar el código.

2.1. Ciclo de Test Driven Development


TDD es un proceso basado en un ciclo de desarrollo muy corto y repetitivo, este proceso
continuo se lo realiza hasta que se tenga pruebas unitarias para todas las funcionalidades. Los
tres pasos del ciclo de vida del TDD más conocido como red, green, refactor cycle.

Figura 1: Red, Green, Refactor cicle [1]


Fuente: Jasek, 2014

2.1.1. Red
En el primer paso, tenemos que escribir una prueba fallida basada en el requerimiento, esta
prueba fallara porque todavía no se desarrollo el código necesario para cumplir con el
requerimiento; este es el escenario ideal pero no siempre sucede, existen dos razones
principales por las cuales no se presenta este escenario, considerados como retroalimentación
positiva.
6
La primera razón más común sucede cuando se cometió un error al escribir la prueba o que el
código este mal implementado, en este caso se tiene que hacer una doble verificación del
proceso porque la prueba unitaria verifica que el código este correcto y por otro lado el código
verifica que la prueba este bien escrita. [1]

La segunda razón la prueba pasa porque la funcionalidad ya fue implementada anteriormente y


el cambio o la mejora en esa funcionalidad no tuvieron un efecto colateral contra la prueba.
También se puede obtener pruebas que no pasaron porque están mal escritas, nunca se debe
escribir más de una prueba unitaria fallida.

2.1.2. Green
En el segundo paso se comienza a implementar la funcionalidad para la que se escribió la
prueba unitaria, en este paso solo se debe escribir el código suficiente para que la prueba pase.
Esto es difícil de cumplir porque uno por experiencia ya conoce lo que su código necesita para
que este completo. Un ejemplo: se quiere calcular el factorial de un número, siguiendo el
algoritmo de TDD primero escribimos la prueba para el número 0 donde se itene que retornar 1,
el código necesario solo seria retornar 1; continuando con el algoritmo escribimos una prueba
unitaria para el número 1 donde tenemos que retornar 1, Kent Beck llama falsa iteración a este
tipo de práctica, lo recomendable para este caso es escribir el código completo para calcular el
factorial de cualquier número, por tanto escribir pruebas para calcular el factorial parece ser
muy innecesario. Pero se puede escribir pruebas para las excepciones que puedan pasar con
este algoritmo como el factorial de un número negativo o un número faccionario, donde el
factorial no existe. Estas son las pruebas que se podrían escribir. [1]

2.1.3. Refactor
El último paso del ciclo TDD es la refactorización, debido a que en la fase anterior se escribió
código necesario para pasar la prueba puede que se haya creado un algoritmo redundante o
inadecuado; en esta fase se debe limpiar el código creado sin agregar nuevas funcionalidades,
este proceso es una forma de validar el código. Uno siente la necesidad de agregar nueva
funcionalidad en este paso, pero se tiene que evitar porque esta fase es para eliminar el código
duplicado, aumentar la legibilidad y expresividad del código, como también reducir el
acoplamiento y aumentar la cohesión del código. En resumen se debe mejorar el diseño del
código. [1]
Después de terminar de refactorizar el código se debe asegurar de que no se haya afectado la
funcionalidad para la cual se escribió el código, corriendo nuevamente la prueba unitaria para
verificar que la funcionalidad no esté afectada, caso contrario revertir el código modificado en la
7
refactorización y así empezar el ciclo nuevamente porque la prueba unitaria fallo llevándonos al
paso uno conocido como Red.

2.2. Las tres leyes de TDD


Los tres ciclos de TDD pueden estar resumidos o envueltos en las tres leyes de TDD, las
personas que apliquen TDD tienen que seguir estas leyes que aseguran de que se cumpla con
los tres ciclos red, green y refactor:
1. You may not write production code unless you’ve first written a failing unit test.
2. You may not write more of a unit test than is sufficient to fail.
3. You may not write more production code than is sufficient to make the failing unit test
pass.[1]

8
3. PRINCIPIOS S.O.L.I.D.

Estos principios hablan sobre el diseño orientado a objetos, gestión de dependencias,


capacidades de una clase, código reutilizable y de fácil mantenimiento.
Son 5 los principios fundamentales, uno por cada letra, que hablan del diseño orientado a
objetos en términos de gestión de dependencias. Las dependencias entre clases hacen que las
clases sean más frágiles, robustas o reutilizables.
El problema con el modelado tradicional es que no se ocupa en profundidad de la gestión de
dependencias entre clases sino de la conceptualización. Quien decidió resaltar estos principios
y darles nombre a algunos de ellos fue Robert C. Martin, allá por el año 1995. [7]

3.1. Single Responsability Principle (SRP)


El principio Single Responsability Principle habla de que una clase solo debería tener una tarea
de la cual ocuparse. Una clase solo debería tener un único motivo para ser modificada, esto
hace que el código sea fácilmente adaptable y mantenible al cambio. Si una clase tiene más de
una responsabilidad, estas responsabilidades están acopladas. Cambiar una de esas
responsabilidades puede perjudicar la capacidad de la clase para cumplir con las otras.
Incumplir con el principio SRC conduce a un diseño frágil que se rompe de manera inesperada
cuando se cambia una responsabilidad. [2]

Como se puede observar en el siguiente ejemplo no se cumple con este principio.

Figura 2: Clase con varias responsabilidades


Fuente: Elaboración propia

public interface Modem {


public void dial(String no);
public void hangup();
public void send(String message);
public String rev();
}

9
Como se puede ver en esta clase existen dos responsabilidades la primera es la conexión y la
segunda es la comunicación. Los métodos dial y hangup son de conexión; send y recv son
métodos de comunicación datos.
Para cumplir con el principio SRP se tiene que separar las responsabilidades de la siguiente
manera.

Figura 3: Interfaces con una solo responsabilidad


Fuente: Elaboración propia

public interface Connection {


public void dial(String no);
public void hangup();
}

public interface DataChannel {


public void send(String message);
public String rev();
}

public class Modem implements DataChannel, Connection {


public void dial(String no) {
}

public void hangup() {


}

public void send(String message) {


}

public String rev() {


return null;
}
}
10
El principio de simple responsabilidad es uno de los principios más simples de entender pero el
más complicado de cumplir porque uno está acostumbrado a crear clases con muchas
responsabilidades.

3.2. Open-Closed Principle (OCP)

Este principio menciona que clases, módulos, funciones, etc deben estar abiertos para
extensiones y cerrados para las modificaciones. Cuando un cambio simple crea un efecto en
cascada o bola de nieve es porque nuestro código no está cumpliendo con este principio. Para
evitar este efecto el principio OCP dice que el comportamiento de una entidad debe poder ser
modificado sin tener que modificar su código fuente. [7]
Estar abiertos para extensión significa que el comportamiento del módulo puede ser extendido,
como los requerimientos de una aplicación pueden cambian nosotros podemos extender el
módulo para soportar nuevos comportamientos.
Estar cerrados a las modificaciones significa que no se tiene que modificar el comportamiento
existente sino extender y crear una nueva implementación que soporte el nuevo requerimiento.
Como se puede observar en el siguiente ejemplo donde no se cumple este principio:
La clase Client usa la clase Server, si nosotros queremos que el cliente use otro servidor
tenemos que modificar la clase Client.

Figura 4: Diagrama de clases fuertemente acoplado


Fuente: Elaboración propia

Este principio es el que produce los mayores beneficios para la programación orientada a
objetos como ser flexibilidad, reutilización y mantenibilidad.

11
Figura 5: Diagrama de clases con bajo acoplamiento
Fuente: Elaboración propia

Una forma de que este ejemplo cumpla con el principio OCP es hacer que la clase Server
implemente una Interface donde este el cliente. Se puede decir que es abierto a modificaciones
porque podemos extender la clase Client o Server sin afectar el modelo, es cerrado porque no
se afecta a comportamientos anteriormente definidos.

3.3. Liskov Substitution Principle (LSP)


Este principio nos dice que un subtipo puede ser sustituido por su tipo base. Este principio
resalta el polimorfismo, como ejemplo un método que recibe como parámetro el tipo X también
puede recibir un tipo Y si el tipo Y es subtipo de X, dicho método no debería de ser afectado en
su funcionamiento. Este principio está muy ligado al OCP porque si una función no cumple con
el LSP tampoco cumple con el OCP. [2]
Como se puede observar en el ejemplo no se cumple con este principio:

12
Figura 6: Diagrama de clases que no cumple con LSP
Fuente: Elaboración propia

public class Rectangle {


private Integer width;
private Integer height;

public Rectangle(Integer with, Integer height) {


this.width = with;
this.height = height;
}

public Integer getWidth() {


return width;
}

public void setWidth(Integer width) {


this.width = width;
}

public Integer getHeight() {


return height;
}

public void setHeight(Integer height) {


this.height = height;
}

public Integer calculateAre() {


return width * height;
}
}

13
public class Square extends Rectangle {
public Square(Integer with, Integer height) {
super(with, height);
}

public void setWidth(Integer with){


this.setHeight(with);
this.setWidth(with);
}

public void setHeight(Integer height) {


this.setWidth(height);
this.setHeight(height);
}
}

public class Client {


public Boolean areaVerifier(Rectangle rectangle) throws Exception {
rectangle.setHeight(4);
rectangle.setWidth(5);
if (rectangle.calculateAre() != 20) {
throw new Exception("Bad area");
}
return true;
}
}

La clase Square que extiende en primer lugar no cumple con el principio Open Close Principle
porque tiene un método que no necesita aunque un poco difícil de entender un Cuadrado solo
tiene un solo valor para alto y ancho, la clase Square sobrescribe y trata de solo mantener un
solo valor de sus cuatro lados por su definición.

La clase Client con su método areaVerifier que es el método que no cumple con el principio
Liskov Substitution Principle porque este método recibe como parámetro una clase Rectangle
clase de la que extiende Square según la definición de LSP este método tiene que funcionar de
la misma forma para la clase base como para la extendida, pero no lo hace si se manda un
Square como parámetro este método cambia sus lados haciendo que el ultimo valor en su alto
o ancho sea el valor de sus cuatro lados, en el ejemplo el resultado seria 25. Ahora si
mandamos un rectángulo funciona como es debido dando como resultado 20.

Como se puede observar en el siguiente ejemplo donde se cumple con el principio LSP:

14
Figura 7: Diagrama de clases que cumple con LSP
Fuente: Elaboración propia

public class Vehicle {

private Integer velocity;


private Boolean isOn;

public Vehicle() {
this.velocity = 0;
this.isOn = false;
}

public void startEngine() {


}

public void accelerate() {


}

public Integer getVelocity() {


return velocity;
}

public void setVelocity(Integer velocity) {


this.velocity = velocity;
}

public Boolean getOn() {


return isOn;
}

public void setOn(Boolean on) {


15
isOn = on;
}
}

public class Car extends Vehicle {


public void startEngine() {
this.engageIgnition();
super.startEngine();
}

public void engageIgnition() {


}
}

public class ElectricBus extends Vehicle {


public void accelerate() {
this.increaseVoltage();
this.connectIndividualEngines();
}

private void increaseVoltage() {

public void connectIndividualEngines() {


}
}

public class Driver {


public void go(Vehicle vehicle) {
vehicle.startEngine();
vehicle.accelerate();
}
}

La clase Vehicle define los métodos como startEgine() y acceletare() que son implementadas a
su manera por las clases ElectricBus y Car; la clase Driver tiene un método que recibe como
parámetro una clase Vehicle este método ejecuta los métodos startEngine() y accelerate() que
funciona correctamente para cualquier clase que extienda de Vehicle cumpliendo así con este
principio.

3.4. The Interface Segregation Principle (ISP)


Este principio nos muestra la desventaja de las interfaces con muchos métodos, con esto se
obliga a que una clase que implemente esta interfaz defina métodos que no usara. Una clase
puede implementar un grupo de métodos y otra clase implementar otro grupo. Este tipo de
interfaz hace sospechar que alguna clase tiene varias responsabilidades rompiendo con el
principio Single Responsability Principle (SRP). [2]
16
Figura 8: Diagrama de clase que no cumple con ISP
Fuente: Elaboración propia

public class Door implements TimerClient {


public void timerOut() {
}
}

public class Timer {


}

public interface TimerClient {


Timer timer = null;

public void timerOut();


}

Ahora hacemos una modificación se necesita una puerta temporizada.

public class TimedDoor extends Door {


}

Esta solución no cumple con el principio de Interface Segregation Principle (ISP) porque ahora
la clase Door implementa métodos que no usara como el método timeOut. Para cumplir con el
principio (ISP) se puede separar las clases de la siguiente forma.

17
Figura 9: Diagrama de clases que cumple con ISP
Fuente: Elaboración propia

public class Door {


}

public class DoorTimerAdapter implements TimerClient {


public void timeOut() {
}
}

public class TimedDoor extends Door {


private DoorTimerAdapter adapter;
private Timer timer;
public void doorTimeOut() {
}
}

public class Timer {


}

public interface TimerClient {


Timer timer = null;
public void timeOut();
}

18
3.5. Dependency-Inversion Principle (DIP)

La inyección de dependencias es una de las mejores formas de trabajar con colaboraciones de


otras clases produciendo un código reutilizable y preparado para el cambio, este principio nos
dice que una clase no debe depender directamente de otra sino de una abstracción. Los
módulos de alto nivel no deben depender de módulos de bajo nivel, ambos deben depender de
abstracciones. [2]
Las abstracciones no deben depender de los detalles. Los detalles deben depender de las
abstracciones.

Figura 10: Diagrama de clases que con cumple con DIP


Fuente: Elaboración propia

public class Button {


private Lamp lamp;

public void poll() {


lamp.turnOn();
}
}

public class Lamp {


public void turnOn() {
}

public void turnOff() {


}
}

Se puede apreciar que la clase Button depende directamente de la clase Lamp, esta
dependencia implica que cualquier cambio en la clase Lamp afecta a la clase Button; ad emás

19
que el Button no puede ser usado para accionar otro dispositivo. La solución es crear una
abstracción entre las clases Button y Lamp.

Figura 11: Diagrama de clases que cumple con DIP


Fuente: Elaboración propia

public interface ButtonServer {


public void turnOn();
public void turnOff();
}

public class Button {


private ButtonServer buttonServer;
public void poll(){
buttonServer.turnOn();
}
}

public class Lamp implements ButtonServer {


public void turnOn() {
}

public void turnOff() {


}
}

20
4. PRUEBAS UNITARIAS
Las pruebas unitarias son un nivel de prueba de software donde se prueban unidades/
componentes individualmente de un software. El propósito es validar que cada unidad del
software funcione según lo diseñado. Una prueba unitaria es la parte comprobable más
pequeña de cualquier software, por lo general tiene pocos parámetros de entrada y casi
siempre uno de salida.
En programación orienta a objetos, la unidad más pequeña es un método, que puede
pertenecer a una clase base o una clase abstracta.

Figura 12: Tipos de pruebas


Fuente: http://softwaretestingfundamentals.com/unit-testing/, 2018

4.1. Porqué escribir pruebas unitarias

4.1.1. Reducir el tiempo invertido para ejecutar las pruebas de funcionalidad


Las pruebas funcionales toman bastante tiempo y generalmente se tiene que abrir la aplicación
y realizar varios pasos que alguien tiene que seguir para validar el comportamiento esperado;
es posible que la persona que está verificando el comportamiento no conozca todos los pasos,
lo que significa que necesita comunicarse con alguien que conozca los pasos. La prueba puede
tomar segundos o minutos dependiendo del cambio. Este proceso debe repetirse por cada
cambio que se realice en el sistema.
Por otro lado las pruebas unitarias toman milisegundos y se pueden ejecutar con solo presionar
un botón y no necesariamente requiere que se conozca el sistema. Si la prueba pasa o no pasa
depende de la prueba no del individuo.

21
4.1.2. Reducir los bugs de regresión
Una de las razones para escribir pruebas unitarias es para reducir los bugs de regresión. Todo
cambio en el código puede afectar funcionalidades de manera no intencional, al trabajar con
metodologías ágiles es inevitable no realizar cambios en las funcionalidades y como
consecuencia cambio de código. Los bugs generados por el cambio pueden ser encontrados
por el equipo de control de calidad, pero si no ocurre esto el bug puede llegar hasta el cliente.
Las pruebas unitarias pueden reducir las posibilidades de que estos bugs ocurran porque el
desarrollador tiene que cambiar las pruebas unitarias por cada cambio de código asegurándose
de resolver los fallos que generaron sus cambios.

4.1.3. Mejorar la calidad de código


Las fases que describe TDD red, green y refactor, en especial la fase de refactor es donde se
elimina el código duplicado, se aumenta la legibilidad y expresividad del código, como también
la reducción del acoplamiento e incrementar la cohesión de nuestro código. Además de generar
código ya verificado por las pruebas unitarias ayudan a que el código funcione según el
requerimiento.

4.1.4. Ayuda a comprender el requerimiento


Otra de las razones para escribir pruebas unitarias es que exige tener en claro el requerimiento
antes de empezar a escribir código, para escribir las pruebas unitarias es necesario tener en
claro lo que se quiere obtener entradas y salidas de la funcionalidad a programar. Un
desarrollador que tiene claro el requerimiento podrá escribir las pruebas unitarias y por tanto lo
que desarrollará será lo esperado. Actualmente es un problema tener desarrolladores
programando sin que ellos tengan en claro las entradas y salidas de la funcionalidad, está mala
interpretación de funcionalidades generan pérdida de tiempo porque el código desarrollado no
es lo esperado y por tanto se tiene que refactorizar.

4.2. Características de una buena prueba unitaria


Fácil de escribir, los desarrolladores no deberían de tener problema al escribir las pruebas
unitarias empleando poco esfuerzo.

Legible, una prueba unitaria debe ser clara y fácil de comprender para que nos ayude a corregir
la funcionalidad si el resultado de la prueba unitaria es fallida.

22
Confiable, las pruebas unitarias solo deben de fallar solo si hay un error de sistema, una prueba
unitaria que falla durante la ejecución de un conjunto de pruebas unitarias o durante las pruebas
de integración hacen pensar que las pruebas unitarias no están bien definidas porque una
prueba unitaria no debería de ser afectado por el tipo de ejecución.

Rápido, los desarrolladores escriben pruebas unitarias para que se puedan ejecutarse
repetidamente para verificar si otras pruebas unitarias fallan y se pueda corregir generando
código confiable y verificado.

Atómico, las pruebas unitarias son independientes no debe confundirse con pruebas de
integración porque esas pruebas si interactúan con otros módulos, las pruebas unitarias no
debería de depender de la red, base de datos, sistema de archivos etc.

4.3. Organizar nuestro código para realizar pruebas unitarias.

Generalmente no se quiere romper la encapsulación de las clases sólo para crear pruebas
unitarias, una de las opciones es crear las pruebas unitarias en el mismo paquete, pero no es
recomendable porque ese código no tiene que ser parte de la puesta en producción. Otra
posibilidad es crear una subcarpeta test para crear dentro las pruebas unitarias para tener los
accesos del paquete, esta solución es más fácil de separar del código que se pondrá en
producción. La última opción es tener paquetes paralelos uno donde este el código fuente y otro
donde estén las pruebas unitarias. Maven genera por defecto los paquetes paralelos. [3]

Figura 13: Código y pruebas en el mismo paquete. [3]


Fuente: Appel, 2015

23
Figura 14: Pruebas dentro de un paquete especifico por ruta. [3]
Fuente: Appel, 2015

Figura 15: Paquetes independientes para código y pruebas. [3]


Fuente: Appel, 2015

4.4. Las cuatro fases de una prueba unitaria


La primera sección es donde se crean los objetos necesarios para testear. En general esta
sección establece los SUT (system under test) se encarga de preparar las entradas y
establecer las precondiciones del Test. Existen varias formas de implementar esta sección en
código uno es sobre el mismo test si las condiciones son mínimas, en un método estático si esa
configuración es reusable y la última es usar las anotaciones que nos provee JUnit (@before).
[3]

@Test
public void addItemOnEmptyList() {
ArrayList<String> names = new ArrayList();
Names.add(“test”);
assertEquals(1, names.size ());
}

@Test
public void addItemOnEmptyList() {
ArrayList<String> names = getArray();
24
Names.add(“test”);
assertEquals(1, names.size());
}

public static void List<String> getArray () {

return new ArrayList<String>();


}

public ArrayList<String>names ();

@Before
public void create Array() {
names = new ArrayList<String>();
assertEquals(2147483647, 2147483647);
}

@Test
public void addItemOnEmptyList() {
names.add(“test”);
assertEquals(1, names.size());
}

La segunda sección después de inicializar todos los objetos y precondiciones necesarias para
ejecutar la prueba, este es el punto en el que se ejecuta la funcionalidad al cual se está
aplicando la prueba unitaria esta sección por lo general es solo una llamada a un método.

@Test
public void addItemOnEmptyList() {
names.add(“test”);
}

La tercera sección se puede considerar última porque es el punto en el que se verifica si el


resultado esperado es el esperado o no.

@Test
public void addItemOnEmptyList () {
assertEquals(1,names.size());
}

La cuarta sección es liberar el entorno dejar en la misma condición antes de que se haya
empezado la ejecución del test, en esta sección se puede usar la anotación @After para limpiar
los objetos creados usados por la prueba unitaria.

@Test
public void addItemOnEmptyList () {
names = null;
}

25
4.5. Buenas prácticas para crear pruebas unitarias

4.5.1. Nombrar correctamente nuestras pruebas

Encontrar el nombre correcto para las clases, métodos y variables para que sean descriptivos,
legibles y entendibles. Usar el prefijo Test antes de que existiera el uso de las anotaciones
@Test eran de mucha ayuda porque ayudaba a diferenciar las clases reales de las clases que
se crean para realizar las pruebas unitarias, usar este prefijo ayudaba a separar las clases si en
el proyecto no usa paquetes paralelos, donde las clases reales están en la misma ruta que las
pruebas unitarias. Si se usa paquetes en paralelo para las pruebas unitarias es opcional el uso
del prefijo Test. Usar el prefijo Test es recomendable porque es de mucha ayuda en proyectos
grandes, con este prefijo es más fácil realizar la búsqueda de archivos.

El nombre de una prueba unitaria puede estar separado de la siguiente forma:


(funcionalidad_entrada_resultadoEsperado)
Por ejemplo:
addItem_theSameItem_addUniqueItem

Como se puede observar el nombre está compuesto por tres partes separadas por una barra
baja aparte de usar notación camel Case entre barras. El propósito de nombrar de esta forma
es hacer legible y de fácil comprensión el propósito de la prueba unitaria.

Otra forma es usar el prefijo should when on todo esto en camel Case

shouldAddUniqueItemWhenAddItemOnItemIsTheSame

4.5.2. Evitar el uso de hard code en las pruebas unitarias


Nombrar constantes de forma correcta dentro de la prueba unitaria es importante, esto porque
si alguien más ve el código de la prueba no podrá interpretar lo que representa el valor, pero si
este valor esta declarado como constante y con el nombre adecuado la lectura de ese código
es más simple.

@Test
public void nameBigNumberBad() {
assertEquals(2147483647, 2147483647);
}

26
@Test
public void nameBigNumberCorrect() {
final int MAX_VALUE = 2147483647;
assertEquals(2147483647, MAX_VALUE);
}

4.5.3. Evitar el uso de lógica dentro de las pruebas unitarias


Cuando se escribe pruebas unitarias se debe evitar el uso de las siguientes instrucciones if,
while, for, switch, etc. Porque si se usa lógica dentro de las pruebas la probabilidad de generar
bugs se incrementa. El último lugar donde uno quiere encontrar un bug es dentro de una prueba
unitaria.

@Test
public void addNumbersArray() {
Integer[][] arrays = {
{2, 2, 4},
{6, 2, 8},
{19, 2, 21},
{22, 2, 24},
{23, 2, 25}
};
for (int i = 0; i< arrays.length; i++) {
Integer[] numbers = arrays [i];
Integer resultAux = numbers[0] + numbers[1];
Assert.assertEquals(numbers[2], resultAux);
}
}

@RunWith(Parameterized.class)
public class Param {
private int numberOne;
private int numberTwo;
private int result;

public Param(int numberOne,int numberTwo, int result) {


this.numberOne = numberOne;
this.numberTwo = numberTwo;
this.result = result;
}

@Parameterized.Parameters
public static Collection primeNumbers() {
return Arrays.asList(new Object[][] {
{ 2, 2, 4 },
{ 6, 2, 8 },
{ 19, 2, 21 },
{ 22, 2, 24 },
{ 23, 2, 25 }
27
});
}

@Test
public void testAddArray() {
int resultAux = numberOne + numberTwo;
assertEquals(result, resultAux);
}
}

4.5.4. Usar métodos auxiliares para configurar y liberar recursos


Si se requiere usar un objeto en varias pruebas unitarias es preferible crear un método auxiliar
porque este método se puede reusar en otras pruebas unitarias dentro de la misma clase, así
se evita tener código duplicado. La configuración de las precondiciones pueden ser varias
líneas de código. Los métodos setup y teardown son métodos por defecto para configurar y
liberar recursos; las anotaciones respectivas son @Before y @After.

public class AddTest {


private final Calculator calculator;

public AddTest() {
calculator = new Calculator();
}

@Test
public void AddTwoNumbers() {
Integer numberOne = 12;
Integer numberTwo = 1;
Integer result = calculator.add(numberOne, numberTwo);
assertEquals(new Integer(13), result);
}
}

public class AddCalculatorTest {


private Calculator createCalculator () {
return new Calculator();
}

@Test
public void addTwoNumbers(){
Integer numberOne = 12;
Integer numberTwo = 1;
Calculator calculator = createCalculator();
Integer result = calculator.add(numberOne, numberTwo);
assertEquals(new Integer(13), result);
}
}

28
4.5.5. Validar métodos privados a través de métodos públicos
En muchos casos se necesita aplicar una prueba unitaria de un método privado; por lo general
un método privado es un detalle de implementación se puede asumir que existe un método
publico que usa al método privado como parte de su implementación, este método público es al
que se tiene que aplicar la prueba unitaria.

public class Parser {


public String cleanSpace(String input) {
return trim(input);
}

private String trim(String input) {


return input.trim();
}
}
public class ParserTest {
@Test
public void cleanSpace() {
String longString = " text ";
Parser parser = new Parser();
String stringClean = parser.cleanSpace(longString);
assertEquals("text", stringClean);
}
}

4.5.6. Evitar referencias estáticas o factores no determinista


Uno de los principios de las pruebas unitarias es que se tienen que tener control de todo el
sistema a la que se aplicará las pruebas. Si se usa referencias estáticas como fechas del
sistema existirá problemas para realizar pruebas unitarias porque se tiene dependencias con el
sistema, la prueba unitaria no es independiente ni tampoco se lo puede ejecutar en cualquier
momento, veamos un ejemplo.

public double getDiscountedPrice(int price) {


Date date = new Date();
SimpleDateFormat simpleDateFormat = new SimpleDateFormat("EEEE");

if ("SATURDAY".equals(simpleDateFormat.format(date).toUpperCase())) {
return price / 2;
} else {
return price;
}
}

29
@Test
public void getPriceOnSaturday() {
Promo promo = new Promo();
double price = promo.getDiscountedPrice(2);
assertEquals(1,price);
}
@Test
public void getPriceOnOtherDaysNotSaturday() {
Promo promo = new Promo();
double price = promo.getDiscountedPrice(2);
assertEquals(2,price);
}

4.6. Señales para identificas código complicado de probar

4.6.1. Clases del tipo Singleton

El patrón singleton es solo otra forma de manejar variables globales. Los singletons promueven
API oscuras que se basan en dependencias reales e introducen un acoplamiento innecesario
entre componentes, tampoco cumplen con el principio de Single Responsability Principle porque
además de sus tareas principales controlan su propio inicialización y ciclo de vida. Los
singletons pueden hacer que las pruebas unitarias dependan del orden de ejecución ya que
mantiene su estado durante toda su vida útil de la aplicación o el conjunto de pruebas unitarias.
Veamos el siguiente ejemplo:

Usar singletons es una mala práctica que puede y debe evitarse en la mayoría de los casos, se
debe distinguir entre singleton como patrón de diseño y única instancia.
Como única instancia la responsabilidad de crear y mantener una solo instancia es el trabajo de
la aplicación. Normalmente para este caso se usa una fábrica o un contenedor de inyección de
dependencias que crea una sola instancia en algún lugar para que la aplicación pueda usarla y
pasarla a cada objeto que lo necesite, este punto de enfoque de singleton es correcto y
verificable.
El siguiente ejemplo, la clase SessionCache es la clase singleton:

public class SessionCache {


private static Map<String, Object> sessions;

public SessionCache() {
sessions = new HashMap<>();
}

30
public static Map<String, Object> getInstance() {
if (sessions == null) {
sessions = new HashMap<>();
}
return sessions;
}
}

public class Login {

public boolean isUserOnline(String login) {


return SessionCache.getInstance().get(login) != null ? true : false;
}
}

@Test
public void userIsNotOnline() {
boolean isOnlineUser1 = SessionCache.getInstance().containsKey("user1");
assertEquals(false, isOnlineUser1);
}

Una alternativa para no usar clase de tipo singleton es usar la inversión de dependencia como
veremos en el siguiente ejemplo.

@Component
public class SessionCacheComponent {
private Map<String, String> sessions;

public SessionCacheComponent() {
sessions = new HashMap();
}

public void add(String login) {


if (sessions.get(login) != null) {
sessions.put(login, login);
}
}

public String get(String login) {


return sessions.get(login);
}
}

public class Login {

public boolean isUserOnline(String login) {


return SessionCache.getInstance().get(login) != null ? true : false;
}
}

31
@Test
public void userIsNotOnline() {
SessionCacheComponent sessionCacheComponent = new SessionCacheComponent();
String isOnlineUser1 = sessionCacheComponent.get("user1");
assertEquals(null, isOnlineUser1);
}

4.6.2. Métodos estáticos


Los métodos estáticos son otra forma de código no determinista porque pueden introducir
fácilmente un acoplamiento y hacer que nuestro código sea difícil de ser probado.
Como se ve en el siguiente ejemplo:

public class MethodStatics {

public static String returnRandomDayOfWeek(String[] names) {


int rndIndex = (int) (Math.random() * names.length);
return names[rndIndex];
}
}

public class MethodStaticTest {


private String[] daysOfWeek = {"Monday", "Tuesday", "Wednesday", "Thursday",
"Friday", "Saturday", "Sunday"};

@Test
public void shouldReturnSaturday() {
String day = MethodStatics.returnRandomDayOfWeek(daysOfWeek);
assertEquals("Saturday", day);
}
}

Este ejemplo es un metodo no determinístico porque cada que se ejecuta la prueba unitaria el
resultado es diferente por lo tanto nuestro test fallará y no dará el mismo resultado. Lo que hace
que la prueba unitaria no pase todas las veces que se ejecute.

public interface IRandomGenerator {


double getNextNumber();
}

public class MethodStatics {


public static String returnRandomDayOfWeek(String[] names, IRandomGenerator
randomGenerator) {
int rndIndex = (int) (randomGenerator.getNextNumber() * names.length);
return names[rndIndex];
}
32
}

public class MethodStaticTest {


private String[] daysOfWeek = {"Monday", "Tuesday", "Wednesday", "Thursday",
"Friday", "Saturday", "Sunday"};

@Test
public void shouldReturnSaturdayWithRandomParameter() {
IRandomGenerator randomGenerator = ()-> 0.8d;
String day = MethodStatics.returnRandomDayOfWeek(daysOfWeek, randomGenerator);
assertEquals("Saturday", day);
}
}

La corrección sobre este código es para que la prueba unitaria sea determinista manteniendo el
método estático fue mandar el número aleatorio como parámetro cosa que podemos controlar
en la prueba unitaria para que sea un solo resultado. Además se está desacoplando la
implementación del generador de números aleatorios, con lo cual para el desarrollador será fácil
cambiarlo por otro tipo de generador de números aleatorios.

33
5. CODE COVERAGE

Code coverage es una medida usada para el testeo de software para determinar cuánto del
código fuente ha sido cubierto por las pruebas unitarias implementadas.
Usando code coverage, el equipo de desarrollo puede ver si su código ha sido probado y si
tiene errores. También ayuda a identificar partes del código que no ha sido probado por tanto se
desconoce si el comportamiento de ese sector de código no abarcado es el correcto.
Los expertos describen code coverage como parte de las pruebas de caja blanca, un método
que examina el código del programa. En algunos casos code coverage se usa para encontrar el
sector de código no cubierto para crear estrategias y pruebas para llegar a cubrir el código y
verificar su correcto funcionamiento.

Resultados altos del code coverage usualmente está asociado con la calidad de código, esta
medida esta expresada entre 0% y 100%; donde cero significa que no se cubrió nada del
código y el 100% que todo el código está cubierto por las pruebas unitarias. Comúnmente el
valor mínimo aceptable para el code coverage es del 80%. Si el código está por debajo del 80%
es un indicador de que se desconoce un 20% de los requerimientos, para incrementar este
indicador es necesario escribir más pruebas unitarias.

5.1. Medidas de cobertura

Las herramientas de cobertura de código generan reportes de forma diferente dependiendo del
tipo de cobertura. Veremos cuáles son estas medidas y explicar su alcance.

5.1.1. Cobertura de clase


La cobertura de clase describe cuantas clases del proyecto han sido cubiertos por las pruebas
unitarias. Esta es una medida útil para identificar las clases y dentro de que paquetes están las
clases que no tienen pruebas unitarias. [6]

5.1.2. Cobertura de método


La cobertura de método es el porcentaje de métodos que están cubiertos por pruebas unitarias.
Esta medida no toma en cuenta el tamaño ni la complejidad del método, pero nos indica que
métodos han sido llamados desde el conjunto de pruebas unitarias. [6]

34
5.1.3. Cobertura de sentencia
La cobertura de sentencia rastrea la llamada de declaraciones. Este es un reporte muy
importante que permite localizar de que archivo fue hecho la llamada y que líneas de código no
se han ejecutado desde las pruebas unitarias. [6]

5.1.4. Cobertura de bloque


La cobertura de bloque ve los bloques de código que han sido cubiertos por las pruebas
unitarias, los bloques son la unidad básica para la cobertura de código, porque muestra los
bloques o condiciones que las variables tienen que cumplir para ejecutar las sentencias dentro
del bloque. [6]

Analizaremos el siguiente código y ver el porcentaje que estamos cubriendo con las pruebas
unitarias.

Public class CodeCoverage {

public static boolean isNumber(String s) {


try {
Integer.parseInt(s);
return true;
} catch (NumberFormatException e) {
return false;
}
}
}

public class CodeCoverageTest {


@Test
public void testNumberWithNumber(){
String number = “1”;
boolean isNumber = CodeCoverage.isNumber(number);
assertEquals(true, isNumber);
}
}

35
El resultado de le ejecución de la prueba con la cobertura de código

Figura 16: Resultado de la cobertura de código incompleta


Fuente: Elaboración propia

El sector de color verde indica la parte de código que fue cubierto por la prueba unitaria y el
sector marcado de color rojo indica que la parte de código que no fue cubierto por ninguna
prueba unitaria.

Figura 17: Reporte de la cobertura de código incompleta


Fuente: Elaboración propia

Modificando las pruebas unitarias para cubrir la parte de código que no verificado.

Public class CodeCoverageTest {


@Test
public void testNumberWithNumber(){
String number = “1”;
boolean isNumber = CodeCoverage.isNumber(number);
assertEquals(true, isNumber);
}
@Test
public void testNumberWithString(){
String number = “a”;
boolean isNumber = CodeCoverage.isNumber(number);
assertEquals(false, isNumber);
}
}

36
Figura 18: Resultado de la cobertura de código completo
Fuente: Elaboración propia

Figura 19: Reporte de la cobertura de código completo


Fuente: Elaboración propia

5.2. Consideraciones sobre Code Coverage

5.2.1. Interpretar correctamente los reportes


Cuando se ejecuta un conjunto de pruebas unitarias se obtiene un reporte con el porcentaje de
cobertura de código en ese momento, interpretarlos y realizar estrategias es importante. Los
reportes muestran las clases, métodos, bloques y sentencias que no han sido cubiertos, como
mejorar? La respuesta es simple se tiene que escribir más pruebas unitarias para cubrir el resto
de código, es lo primero que se nos viene a la mente pero esto no es lo correcto; lo que se debe
hacer es analizar porque esos sectores de código no han sido abarcados para encontrar el
verdadero problema, porque escribir más pruebas unitarias en ese momento no solucionara el
problema solo si se encuentra la verdadera causa del porque no se cubrió ese código
lograremos avanzar. [6]

5.2.2. Cubrir nuestro código no es fácil


Instalar o añadir una herramienta para la cobertura de código puede fácil, pero se tiene que
entender que esta herramienta solo nos indica los sectores del código que no están cubiertos
no nos dice como tiene que crear las pruebas unitarias para cubrirlo.

37
5.2.3. Las herramientas de Cobertura de Código verifica código existente
Se puede estar perdiendo tiempo tratando de llegar a cubrir el 100% del código dejando de lado
el correcto funcionamiento de las funcionalidades. No se tiene que dejar de lado los nuevos
requerimientos, mientras no se termine de implementar las funcionalidades no se llegara al
100%. Las pruebas unitarias y los reportes de cobertura de código no toman en cuenta el
código que no está escrito, por eso no representa una información del avance del producto de
software. Esto porque podemos tener el 100% de cobertura de código pero el 10% de las
funcionalidades completadas.

38
6. CONCLUSIONES
Aplicar TDD tiene muchas ventajas como se menciono, pero seguir los pasos que TDD implica
no es tarea fácil hay que tener disciplina para empezar a escribir pruebas antes de empezar a
producir codigo; esto suena fácil pero tenemos que aplicarlo para ver que tanto esfuerzo aplica
seguir esas simples reglas.

El diseño adecuado de nuestro código es importante para que nuestro código sea escalable y
robusto, para esto nos ayudamos de los 5 principios SOLID, estos principios simples pero a
veces complicados de cumplir nos aseguran de que nuestro código tenga la capacidad de
adaptarse al cambio como también la facilidad de que se puedan crear pruebas unitarias para
ese código.

Las pruebas unitarias nos ayudan a producir código de mejor calidad, porque garantizamos que
el código generado pasó por varias pruebas que respaldan el correcto funcionamiento del
mismo. Aplicar pruebas unitarias hace que nuestro código también cumpla con diseño porque
nos damos cuenta que nuestro código no tiene un buen diseño si es complicado crear una
prueba unitaria para probar su funcionalidad.

Code coverage es una métrica que los desarrolladores podemos usar para encontrar código al
que no se ha aplicado una prueba, esto nos indica que no estamos escribiendo las suficientes
pruebas unitarias dejando incertidumbre en el correcto funcionamiento del código y tal vez los
sectores no cubiertos por pruebas pueden ser sectores a los que es complicado realizar
pruebas por un mal diseño de software.

39
7. BIBLIOGRAFÍA

[1] Test–Driven Development: 15 years later Documento de referencia (Octubre 2014). PDF
Reference. Recuperado de
https://projekter.aau.dk/projekter/files/204129305/Report_swd903e13_.pdf

[2] Martin C. Robert, Martin Micah. (2007). Agile Principles, Patterns, and Practices in C#.
Primera Edicion. Pearson Education.

[3] Frank Appel. (2015). Testing with JUnit. Master high-quality software development driven
by unit tests. Primera Edicion. Packt Publishing.

[4] Patkos Csaba. (2014). SOLID: Part 3 - Liskov Substitution & Interface Segregation
Principles. Recuperado de https://code.tutsplus.com/tutorials/solid-part-3-liskov-substitution-
interface-segregation-principles--net-36710

[5] Sergey Kolodiy. Unit Tests, How to Write Testable Code and Why it Matters.
https://www.toptal.com/qa/how-to-write-testable-code-and-why-it-matters

[6]Cedric Beust, Hani Suleiman. (). Next Generation Java Testing: TestNG and Advanced
Concepts. (Octubre 15, 2007).Primera Edición. Addison-Wesley Professional.

[7] Carlos B. Jurado. (2010) Diseño Ágil con TDD. Primera Edición. Sefe Creative

40

Potrebbero piacerti anche