apuntes:pruebas
Diferencias
Muestra las diferencias entre dos versiones de la página.
apuntes:pruebas [24/06/2019 10:55] – [Pruebas de Usabilidad] Fernando Valdeón | apuntes:pruebas [16/09/2024 19:32] (actual) – editor externo 127.0.0.1 | ||
---|---|---|---|
Línea 1: | Línea 1: | ||
+ | ====== Diseño de Pruebas ====== | ||
+ | La etapa de pruebas de software (software testing) es un proceso más dentro del __desarrollo de software__. Sirve para mantener un control de calidad sobre el producto que creamos, y aunque requiera tiempo al igual que programar, a la larga el software mal programado acaba saliendo demasiado caro. | ||
+ | |||
+ | ===== ¿Qué se prueba? (Caso de Prueba) ===== | ||
+ | Un caso de prueba (//test case//) es una situación, contexto o escenario bajo el que se comprueba una funcionalidad de un programa para ver si se comporta de la forma en qué se espera. | ||
+ | |||
+ | Por ejemplo, //un videojuego de plataformas; | ||
+ | |||
+ | * **Objeto de la prueba**: asegurarnos que cuando el personaje cae al agua, ocurre lo esperado | ||
+ | * **Caso prueba**: llevas al personaje a un punto de la pantalla donde pueda caer al agua | ||
+ | |||
+ | En programación, | ||
+ | |||
+ | Otro ejemplo, //int Math.abs(int num) devuelve un int con el valor absoluto del parámetro '' | ||
+ | |||
+ | * **Objeto de la prueba**: asegurarnos que Math.abs() devuelve el valor absoluto | ||
+ | * **Caso de prueba**: si calculo el valor absoluto de -7 debe dar 7 | ||
+ | |||
+ | |||
+ | En otras palabras, un caso de prueba //es una pregunta que se le hace al programa para saber si el programa contesta (reacciona) correctamente// | ||
+ | |||
+ | |||
+ | |||
+ | ===== Tipos de pruebas según su enfoque ===== | ||
+ | |||
+ | ==== Caja Negra ==== | ||
+ | {{ : | ||
+ | Son las pruebas que se centran en evaluar el valor de las salidas de un sistemas a partir de unas entradas concretas, sin tener en cuenta el funcionamiento interno del sistema. Se centran en el **qué** hace un sistema y no en el **cómo** lo hace. **El programador no necesita saber cómo funciona el código, sino evaluar solamente la salidas**. | ||
+ | |||
+ | Este tipo de test se puede aplicar a cualquier nivel de testeo de software: pruebas '' | ||
+ | ==== Caja Blanca ==== | ||
+ | {{ : | ||
+ | Estos tipos de pruebas se centran en analizar cada uno de los posibles caminos en el flujo de ejecución de un programa antes unos valores de entrada concretos. **Es decir, si ante unos valores de entrada (parámetros) de un método, el flujo del programa ejecuta los //if//, o los //else//, o entra en un //bucle//, o sale de él. ** | ||
+ | Este tipo de funcionamiento se puede comprobar también con el depurador. | ||
+ | |||
+ | Las pruebas de Caja Blanca también se pueden aplicar a las pruebas '' | ||
+ | |||
+ | ===== Tipos de pruebas según su alcance===== | ||
+ | ==== Pruebas Unitarias ==== | ||
+ | Una prueba unitaria es un tipo de prueba enfocada en verificar una sección específica dentro del código de un programa. En programación orientada a objetos se realiza en cada clase y se centra en los métodos de cada clase. | ||
+ | | ||
+ | Las pruebas unitarias son código (Java, C#, etc) escrito normalmente al mismo tiempo que se va desarrollando el programa (enfoque Caja Blanca). Un método creado en el programa, puede tener distintas pruebas unitarias. Por si misma no asegura el funcionamiento completo del programa, pero sin embargo no indican que los bloques de software que vamos creando funcionan independientemente de los otros. | ||
+ | |||
+ | Son los tests más importantes para el desarrollo de software y por supuesto para el __desarrollo guiado por tests__ (TDD). | ||
+ | |||
+ | Para realizar pruebas unitarias tenemos herramientas parecidas para cada lenguaje o plataforma: | ||
+ | * JUnit para Java | ||
+ | * PHPUnit para PHP | ||
+ | * CPPUnit para C++ | ||
+ | * NUnit para .NET | ||
+ | * CUnit para C | ||
+ | * PyUnit para Python | ||
+ | |||
+ | ==== Pruebas de Integración ==== | ||
+ | Las pruebas de integración es una fase en el proceso de pruebas de software, en la que se combinan los distintos módulo de software de un programa y se comprueba que trabajan de forma conjunta. Se realizan después de la fase de pruebas unitarias y se centran principalmente en probar la comunicación entre los componentes y sus comunicaciones, | ||
+ | |||
+ | Los tests de integración prueban que el sistema __completo__ funciona como se espera. Por ejemplo, una interfaz gráfica y la aplicación con la que trabaja, una aplicación de acceso a datos y el módulo que trabaja un gestor de bases de datos, etc. | ||
+ | |||
+ | ==== Pruebas de Usabilidad ==== | ||
+ | Las pruebas de usabilidad consisten en seleccionar a un grupo de usuarios de una aplicación y solicitarles que lleven a cabo las tareas para las cuales fue diseñada. | ||
+ | |||
+ | Posteriormente los desarrolladores involucrados toman nota de la interacción, | ||
+ | |||
+ | No es necesario que se trate de una aplicación completamente terminada, pudiendo tratarse de un prototipo o de las distintas etapas de avance de un proyecto. | ||
+ | |||
+ | |||
+ | |||
+ | |||
+ | |||
+ | ===== JUnit ===== | ||
+ | {{ : | ||
+ | Es un framework para Java enfocado en la realización de __pruebas unitarias__ (//unit testing//). Consiste en unas librerías JAR que debemos añadir a un proyecto en Java. http:// | ||
+ | |||
+ | Eclipse viene con JUnit integrado, por lo que no es necesario descargar ningún software adicional. | ||
+ | |||
+ | JUnit5 ofrece herramientas para poder ejecutar tests de las versiones anteriores (JUnit4 y JUnit3), por lo que JUnit5 puede ejecutar tests escritos en las versiones anteriores. | ||
+ | |||
+ | Necesitamos al menos la versión Oxygen del Eclipse IDE para tener integración con la última versión del framework, //JUnit 5//. | ||
+ | |||
+ | La API de JUnit5, en la que encontraremos los métodos para implementar nuestros casos de prueba: | ||
+ | |||
+ | https:// | ||
+ | |||
+ | Ejemplo de clase de pruebas: | ||
+ | <code java> | ||
+ | public class ClaseTest { | ||
+ | |||
+ | @BeforeAll | ||
+ | public static void setUpBeforeClass() throws Exception { | ||
+ | //Codigo que se ejecuta antes de cualquier prueba | ||
+ | } | ||
+ | |||
+ | @BeforeEach | ||
+ | public void setUp() throws Exception { | ||
+ | //Codigo que se ejecuta antes de cada prueba | ||
+ | } | ||
+ | |||
+ | @Test | ||
+ | public void test1() { | ||
+ | //Caso de prueba | ||
+ | } | ||
+ | |||
+ | @Test | ||
+ | public void test2() { | ||
+ | //Caso de prueba | ||
+ | } | ||
+ | |||
+ | @Test | ||
+ | public void test3() { | ||
+ | //Caso de prueba | ||
+ | } | ||
+ | |||
+ | @AfterEach | ||
+ | public void tearDown() throws Exception { | ||
+ | //Metodo que se ejecuta después de cada prueba | ||
+ | } | ||
+ | |||
+ | |||
+ | @AfterAll | ||
+ | public static void tearDownAfterClass() throws Exception { | ||
+ | //Codigo que se ejecuta después de todas las pruebas | ||
+ | } | ||
+ | } | ||
+ | </ | ||
+ | |||
+ | ==== Qué cosas se deben probar ==== | ||
+ | Los tests unitarios se usan para evitar el miedo a que algo se //rompa//. Cualquier método que no tengamos claro si funciona de forma correcta o no, es un método susceptible de ser probado de forma unitaria. | ||
+ | |||
+ | Los métodos getters y setters normalmente son tán sencillos que no suelen albergar dudas, pero si hay alguna razón para que no funcionen, entonces también se deben probar. | ||
+ | |||
+ | En resumen, se debe probar todas las cosas hasta que todas nos inspiren confianza, basándonos en nuestro criterio. | ||
+ | |||
+ | //Una buena aproximación es realizar una prueba afirmativa y otra negativa para cada requisito. __Una prueba para evaluar que un requisito funciona adecuadamente, | ||
+ | |||
+ | En caso de querer comprobar todos los requisitos de un programa se deben al menos plantear **dos** casos de prueba para cada requisito o funcionalidad: | ||
+ | ==== Métodos de prueba vs Casos de prueba==== | ||
+ | |||
+ | Un método de prueba es simplemente un fragmento de código que realiza una comprobación sobre una funcionalidad de un programa. Para que un método sea un método de prueba debe estar precedido por la anotación '' | ||
+ | |||
+ | Un caso de prueba (test case), como hemos dicho es una situación o caso (un estado concreto del programa) bajo el que se prueba una funcionalidad del programa. | ||
+ | |||
+ | Nosotros vamos a intentar que cada método de prueba plantee únicamente un caso de prueba distinto, ya que un programa puede funcionar de formas distintas dependiendo del contexto. | ||
+ | |||
+ | Los métodos de prueba se agrupan atendiendo a su finalidad en '' | ||
+ | |||
+ | Si tenemos una clase '' | ||
+ | <code java> | ||
+ | public class Matematicas{ | ||
+ | | ||
+ | return num1 + num2; | ||
+ | } | ||
+ | } | ||
+ | </ | ||
+ | |||
+ | Y creamos una clase '' | ||
+ | |||
+ | <code java> | ||
+ | class MatematicasTest { | ||
+ | @Test | ||
+ | void testSuma() { | ||
+ | int actual= Metodos.suma(7, | ||
+ | int esperado = 7; | ||
+ | assertEquals(esperado, | ||
+ | } | ||
+ | } | ||
+ | </ | ||
+ | |||
+ | |||
+ | Por convenio a cada método de prueba se le suele llamar con un nombre que identifique lo que se está probando, seguido | ||
+ | |||
+ | {{ vimeo> | ||
+ | |||
+ | ==== Características de un caso de prueba ==== | ||
+ | A la hora de implementar pruebas con JUnit debemos seguir los siguientes principios. **Cada caso de prueba debe:** | ||
+ | - Probar una sola cosa | ||
+ | - Tener un propósito claro | ||
+ | - Estar escrito de la forma más clara posible | ||
+ | - Ser lo más pequeño posible | ||
+ | - Ser independiente: | ||
+ | - Poder ser repetido las veces necesarias | ||
+ | ==== Anotaciones==== | ||
+ | Las siguientes anotaciones de JUnit 5 se emplean en la cabecera de cada método de test: | ||
+ | |||
+ | ^Anotación^Descripción^ | ||
+ | |@Test| Indica que el método es un test| | ||
+ | |@DisplayName | Indica un nombre para el //test class// o el //test method//| | ||
+ | |@Tag | Define etiquetas para filtrar por tests| | ||
+ | |@BeforeEach | Se aplica a un método para indicar que se ejecute antes de cada método de prueba. (JUnit4 @Before)| | ||
+ | |@AfterEach | Se aplica a un método para indicar que se ejecuta después de cada método de prueba. (JUnit4 @After)| | ||
+ | |@BeforeAll | Se aplica a un método '' | ||
+ | |@AfterAll | Se aplica a un método '' | ||
+ | |@Disable | Ese aplica a un método de prueba para evitar esa prueba (JUnit4 @Ignore)| | ||
+ | |||
+ | ==== Clase Assertions: Afirmaciones ==== | ||
+ | La clase '' | ||
+ | |||
+ | Se usan para afirmar una condición como resultado del funcionamiento esperado de una parte del código. Si no se cumple lo que se afirma, la prueba falla y JUnit nos lo indica. | ||
+ | |||
+ | **Se recomienda realizar una sola // | ||
+ | |||
+ | ^Metodo^ Descripción ^ | ||
+ | |**assertTrue**(boolean valor)| Falla si valor no es '' | ||
+ | |**assertFalse**(boolean valor)|Falla si valor no es '' | ||
+ | |**assertFalse**(boolean valor, String mensaje)|Falla si valor no es '' | ||
+ | |**assertEquals**(int esperado, int actual)| Falla si '' | ||
+ | |**assertEquals**(int esperado, int actual, String mensaje)| Falla si '' | ||
+ | |**assertEquals**(double esperado, double actual, double delta)| Falla si la diferencia entre '' | ||
+ | |**assertNull**(Object obj) | Falla si '' | ||
+ | |**assertNotNull**(Object obj) |Falla si '' | ||
+ | |**assertEquals**(Object esperado, Object actual) | Falla si los objetos son distintos, evaluando su método '' | ||
+ | |**assertNotEquals**(Object esperado, Object actual) | Falla si los objetos no son distintos, evaluando su método '' | ||
+ | |**assertSame**(Object esperado, Object actual) | Falla si las referencias de los objetos sin distintas| | ||
+ | |||
+ | * [[https:// | ||
+ | * [[https:// | ||
+ | |||
+ | |||
+ | |||
+ | ==== Contexto de una clase de prueba: Fixtures ==== | ||
+ | |||
+ | Preparar el contexto de un test o grupo de tests se conoce en la jerga de JUnit como '' | ||
+ | |||
+ | Se usan para ejecutar código antes o después, de todos o cada uno, de los tests de una clase de pruebas. De este modo, evitan duplicar código, sobre todo a la hora de inicializar objetos en cada método de test. | ||
+ | |||
+ | No es necesario utilizar todas y muchas veces no necesitaremos ninguna, pero nos pueden servir para construir objetos, vaciar listas. | ||
+ | |||
+ | |||
+ | === Una vez por cada test: @BeforeEach y @AfterEach === | ||
+ | Actúan sobre un método para indicar que se ejecutará antes o después de ejecutar cada caso de prueba. Se usa para establecer el contexto del test. | ||
+ | |||
+ | <code java> | ||
+ | public class ClaseTest{ | ||
+ | |||
+ | //Una vez antes de cada test | ||
+ | @BeforeEach | ||
+ | void setUp(){ | ||
+ | //Me aseguro que antes de cada test solo haya un vehiculo en la lista | ||
+ | | ||
+ | | ||
+ | } | ||
+ | |||
+ | //Una vez después de cada test | ||
+ | @AfterEach | ||
+ | void tearDown(){ | ||
+ | | ||
+ | } | ||
+ | |||
+ | ... | ||
+ | //Resto de métodos de prueba | ||
+ | </ | ||
+ | |||
+ | '' | ||
+ | |||
+ | === Una vez por cada clase de pruebas: @BeforeAll y @AfterAll === | ||
+ | Se realiza una sola vez para toda la clase: al iniciar la clase o al terminarla. Se suele usar para abrir recursos: creación de instancias, conexiones, apertura de ficheros, etc. | ||
+ | |||
+ | La diferencia con las anotaciones anteriores, __es que @BeforeAll y @AfterAll actúan sobre métodos '' | ||
+ | |||
+ | <code java> | ||
+ | public class ClaseTest | ||
+ | |||
+ | | ||
+ | |||
+ | | ||
+ | |||
+ | //Se ejecuta al inicio de la clase | ||
+ | | ||
+ | | ||
+ | gestor = new GestorVehiculo() | ||
+ | } | ||
+ | |||
+ | //Se ejecuta al final de la clase | ||
+ | | ||
+ | | ||
+ | escritor.close(); | ||
+ | } | ||
+ | |||
+ | ... | ||
+ | // | ||
+ | </ | ||
+ | |||
+ | '' | ||
+ | |||
+ | {{ vimeo> | ||
+ | ==== Ejecutar varios tests: Test Suite ==== | ||
+ | Una '' | ||
+ | |||
+ | En Junit5 existen distintas formas de hacer esto en base a las anotaciones | ||
+ | |||
+ | === Test runners === | ||
+ | Un '' | ||
+ | <code java> | ||
+ | @RunWith(JUnitPlatform.class) | ||
+ | public class JUnit5Runner{ | ||
+ | } | ||
+ | |||
+ | </ | ||
+ | |||
+ | === Ejecutar varias clases de tests === | ||
+ | Si tengo varias clases de tests y quiero ejecutarlas de forma conjunta puedo seleccionar un paquete concreto. | ||
+ | Esto se hace con la etiqueta '' | ||
+ | |||
+ | <code java> | ||
+ | @RunWith(JUnitPlatform.class) | ||
+ | @SelectPackages(" | ||
+ | public class JUnit5Runner{ | ||
+ | |||
+ | } | ||
+ | |||
+ | </ | ||
+ | === Ejecutar un paquete de tests === | ||
+ | También puedo seleccionar las clases de pruebas, concretando cuáles quiero ejecutar. Para ello uso la etiqueta '' | ||
+ | |||
+ | <code java> | ||
+ | @RunWith(JUnitPlatform.class) | ||
+ | @SelectClasses({MetodosTest.class, | ||
+ | public class JUnit5Runner{ | ||
+ | |||
+ | } | ||
+ | </ | ||
+ | |||
+ | === Etiquetar los tests === | ||
+ | Con la etiqueta '' | ||
+ | |||
+ | <code java> | ||
+ | @Test | ||
+ | @Tag(" | ||
+ | void testCaseA(TestInfo testInfo) { | ||
+ | ... | ||
+ | } | ||
+ | |||
+ | @Test | ||
+ | @Tag(" | ||
+ | @Tag(" | ||
+ | void testCaseB(TestInfo testInfo) { | ||
+ | ... | ||
+ | } | ||
+ | |||
+ | </ | ||
+ | |||
+ | Ahora desde una clase runner puedo ejecutar los tests con etiquetas concretas: | ||
+ | <code java> | ||
+ | @RunWith(JUnitPlatform.class) | ||
+ | @SelectPackages(" | ||
+ | @IncludeTags(" | ||
+ | public class JUnit5Runner{ | ||
+ | } | ||
+ | |||
+ | // o excluirlos | ||
+ | @RunWith(JUnitPlatform.class) | ||
+ | @SelectPackages(" | ||
+ | @ExcludeTags(" | ||
+ | public class JUnit5Runner{ | ||
+ | } | ||
+ | |||
+ | // o ejecutar tests de varias categorias | ||
+ | @RunWith(JUnitPlatform.class) | ||
+ | @SelectPackages(" | ||
+ | @IncludeTags({" | ||
+ | public class JUnit5Runner{ | ||
+ | } | ||
+ | </ | ||
+ | |||
+ | ===== Desarrollo dirigido por pruebas ===== | ||
+ | El desarrollo dirigido por pruebas (Test Driven Development, | ||
+ | //TDD// se centra en tres pilares fundamentales: | ||
+ | * La implementación de las funciones justas que el cliente necesita, __y no más__. Se debe evitar desarrollar funcionalidades que nunca serán usadas. | ||
+ | * Reducción al mínimo del número de defectos que llegan al software en fase de producción. | ||
+ | * La producción del software modular, altamente reutilizable y preparado para el cambio. | ||
+ | |||
+ | El // | ||
+ | A partir de unos requisitos para un proyecto se diseñan los casos de prueba. Los requisitos son funcionalidades que debe realizar mi programa, por lo que se puede de antemano diseñar los tests que comprueban dicha funcionalidad y posteriormente el código que superará cada prueba. | ||
+ | |||
+ | ==== El algoritmo TDD ==== | ||
+ | {{ : | ||
+ | |||
+ | La esencia de TDD es sencilla pero ponerla en práctica correctamente es cuestión de entrenamiento, | ||
+ | |||
+ | - Escribir la prueba para el requisito (el ejemplo, el test). | ||
+ | - Implementar el código según dicho ejemplo. | ||
+ | - Refactorizar para eliminar duplicidad y hacer mejoras. | ||
+ | |||
+ | === Escribir la especificación primero === | ||
+ | Una vez que tenemos claro cuál es el requisito o funcionalidad, | ||
+ | |||
+ | Para poder escribir los tests, tenemos que pensar primero en cómo queremos que sea la API del programa, es decir, qué métodos queremos que tenga el programa y cómo funcionarán. Pero sólo una parte pequeña, un comportamiento del programa bien definido y sólo uno (método). Tenemos que hacer el esfuerzo de imaginar cómo seria el código del programa si ya estuviera implementado y cómo comprobaríamos que, efectivamente, | ||
+ | |||
+ | No tenemos que diseñar todas las especificaciones antes de implementar cada una, sino que vamos una a una siguiendo los tres pasos del algoritmo TDD. El hecho de tener que usar una funcionalidad antes de haberla escrito le da un giro de 180 grados al código resultante. No vamos a empezar por fastidiarnos a nosotros mismos sino que nos cuidaremos de diseñar lo que nos sea más cómodo, más claro, siempre que cumpla con el requisito objetivo. | ||
+ | |||
+ | === Implementar el código que hace funcionar el test === | ||
+ | Teniendo el ejemplo escrito, codificamos lo mínimo necesario para que se cumpla, para que el test pase. Típicamente, | ||
+ | menos tiempo nos llevó escribirlo. No importa que el código parezca feo o chapucero, eso lo vamos a enmendar en el siguiente paso y en las siguientes iteraciones. | ||
+ | |||
+ | === Refactorizar === | ||
+ | Refactorizar es modificar el diseño sin alterar su comportamiento, | ||
+ | API pública (los métodos). | ||
+ | |||
+ | ===== Objetos Mock (Objetos Simulados) ===== | ||
+ | Los objetos Mock son objetos que imitan el comportamiento de un tipo real de objetos. Esto permite especificar qué valores pueden devolver sus métodos en un caso concreto. Se usan para probar el comportamiento de otras operaciones que requieran el uso de ese tipo de objetos, //de la misma forma que se usan muñecos con aspecto humano (test dummies) para simular el comportamiento de un vehiculo ante un choque//. | ||
+ | |||
+ | Existen diversos frameworks para crear objetos simulados en Java: | ||
+ | * [[http:// | ||
+ | * [[http:// | ||
+ | * [[http:// | ||
+ | |||
+ | Nosotros nos centraremos en '' | ||
+ | |||
+ | https:// | ||
+ | |||
+ | ===== Herramientas de Depuración ===== | ||
+ | Una vez que advertimos que nuestro código no pasa las pruebas, podemos utilizar el depurador (Modo Debug) del IDE que empleemos para revisar el estado de nuestro programa mientas lo vamos ejecutando paso a paso y corregir los posibles errores que pueda contener. | ||
+ | Es recomendable establecer // | ||
+ | |||
+ | ---- | ||
+ | ===== Prácticas ===== | ||
+ | - Desarrollo guiado por pruebas | ||
+ | |||
+ | ---- | ||
+ | (c) {{date> %Y}} Fernando Valdeón |