Tipos de prueba
¿Qué es una prueba? Según Wikipedia: "Las pruebas de software implican la ejecución de un componente de software o un componente del sistema para evaluar una o más propiedades de interés". En otras palabras, es una comprobación de la corrección de nuestro sistema en determinadas situaciones. Bueno, veamos qué tipos de pruebas hay en general:- Pruebas unitarias — Pruebas cuyo propósito es verificar cada módulo del sistema por separado. Estas pruebas deben aplicarse a las partes atómicas más pequeñas del sistema, por ejemplo, módulos.
- Pruebas del sistema: pruebas de alto nivel para comprobar el funcionamiento de una parte más grande de la aplicación o del sistema en su conjunto.
- Prueba de regresión: prueba que se utiliza para verificar si las nuevas funciones o las correcciones de errores afectan la funcionalidad existente de la aplicación o introducen errores antiguos.
- Pruebas funcionales: comprobar si una parte de la aplicación cumple los requisitos establecidos en las especificaciones, historias de usuarios, etc.
Tipos de pruebas funcionales:
- Pruebas de caja blanca: verificar si una parte de la aplicación cumple con los requisitos mientras se conoce la implementación interna del sistema;
- Pruebas de caja negra: verificar si una parte de la aplicación cumple con los requisitos sin conocer la implementación interna del sistema.
- Pruebas de rendimiento: pruebas escritas para determinar cómo funciona el sistema o parte del sistema bajo una determinada carga.
- Pruebas de carga: pruebas diseñadas para verificar la estabilidad del sistema bajo cargas estándar y encontrar la carga máxima en la que la aplicación aún funciona correctamente.
- Prueba de estrés: prueba diseñada para verificar el rendimiento de la aplicación bajo cargas no estándar y para determinar la carga máxima antes de que falle el sistema.
- Pruebas de seguridad: pruebas utilizadas para verificar la seguridad del sistema (contra piratas informáticos, virus, acceso no autorizado a datos confidenciales y otros ataques agradables).
- Pruebas de localización: pruebas de la localización de la aplicación.
- Pruebas de usabilidad: pruebas destinadas a verificar la usabilidad, la comprensibilidad, el atractivo y la capacidad de aprendizaje.
- Unidad: esta sección hace referencia a las pruebas unitarias, que se aplican en diferentes capas de la aplicación. Ponen a prueba la unidad divisible más pequeña de lógica de aplicación. Por ejemplo, clases, pero más a menudo métodos. Estas pruebas suelen intentar en la medida de lo posible aislar lo que se prueba de cualquier lógica externa. Es decir, intentan crear la ilusión de que el resto de la aplicación funciona como se esperaba.
Siempre debe haber muchas de estas pruebas (más que cualquier otro tipo), ya que prueban piezas pequeñas y son muy livianas, no consumen muchos recursos (es decir, RAM y tiempo).
- Integración: esta sección se refiere a las pruebas de integración. Esta prueba verifica piezas más grandes del sistema. Es decir, combina varias piezas de lógica (varios métodos o clases) o comprueba la corrección de la interacción con un componente externo. Estas pruebas suelen ser más pequeñas que las pruebas unitarias porque son más pesadas.
Un ejemplo de una prueba de integración podría ser conectarse a una base de datos y verificar la corrección de la operación de los métodos para trabajar con ella.
- Interfaz de usuario: esta sección hace referencia a las pruebas que verifican el funcionamiento de la interfaz de usuario. Involucran la lógica en todos los niveles de la aplicación, por lo que también se denominan pruebas de extremo a extremo. Por regla general, hay muchos menos, porque son los más engorrosos y deben verificar las rutas más necesarias (usadas).
En la imagen de arriba, vemos que las diferentes partes del triángulo varían en tamaño: existen aproximadamente las mismas proporciones en el número de diferentes tipos de pruebas en el trabajo real.
Hoy vamos a echar un vistazo más de cerca a las pruebas más comunes, las pruebas unitarias, ya que todos los desarrolladores de Java que se precien deberían poder usarlas a un nivel básico.
Conceptos clave en pruebas unitarias
La cobertura de prueba (cobertura de código) es una de las principales medidas de qué tan bien se prueba una aplicación. Este es el porcentaje del código que está cubierto por las pruebas (0-100%). En la práctica, muchos persiguen este porcentaje como objetivo. Eso es algo con lo que no estoy de acuerdo, ya que significa que las pruebas comienzan a aplicarse donde no se necesitan. Por ejemplo, supongamos que tenemos operaciones CRUD (crear/obtener/actualizar/eliminar) estándar en nuestro servicio sin lógica adicional. Estos métodos son puramente intermediarios que delegan trabajo a la capa que trabaja con el repositorio. En esta situación, no tenemos nada que probar, excepto quizás si el método dado llama a un método DAO, pero eso es una broma. Se suelen utilizar herramientas adicionales para evaluar la cobertura de las pruebas: JaCoCo, Cobertura, Clover, Emma, etc. Para un estudio más detallado de este tema, TDD significa desarrollo basado en pruebas. En este enfoque, antes de hacer cualquier otra cosa, escribe una prueba que verificará el código específico. Esto resulta ser una prueba de caja negra: sabemos cuál es la entrada y cuál debería ser la salida. Esto permite evitar la duplicación de código. El desarrollo basado en pruebas comienza con el diseño y desarrollo de pruebas para cada bit de funcionalidad en su aplicación. En el enfoque TDD, primero creamos una prueba que define y prueba el comportamiento del código. El objetivo principal de TDD es hacer que su código sea más comprensible, más simple y libre de errores. El enfoque consiste en lo siguiente:- Escribimos nuestra prueba.
- Realizamos la prueba. Como era de esperar, falla, ya que aún no hemos implementado la lógica requerida.
- Agregue el código que hace que la prueba pase (ejecutamos la prueba nuevamente).
- Refactorizamos el código.
Etapas de prueba
Una prueba consta de tres etapas:- Especifique los datos de prueba (accesorios).
- Ejercitar el código bajo prueba (llamar al método probado).
- Verifique los resultados y compare con los resultados esperados.
Entornos de prueba
Entonces, ahora al grano. Hay varios entornos de prueba (marcos) disponibles para Java. Los más populares son JUnit y TestNG. Para nuestra revisión aquí, usamos: Una prueba JUnit es un método en una clase que se usa solo para probar. La clase suele tener el mismo nombre que la clase que prueba, con "Prueba" añadido al final. Por ejemplo, CarService -> CarServiceTest. El sistema de compilación de Maven incluye automáticamente dichas clases en el alcance de la prueba. De hecho, esta clase se llama clase de prueba. Repasemos brevemente las anotaciones básicas:- @Test indica que el método es una prueba (básicamente, un método marcado con esta anotación es una prueba unitaria).
- @Before significa un método que se ejecutará antes de cada prueba. Por ejemplo, para llenar una clase con datos de prueba, leer datos de entrada, etc.
- @After se utiliza para marcar un método que se llamará después de cada prueba (por ejemplo, para borrar datos o restaurar valores predeterminados).
- @BeforeClass se coloca sobre un método, de forma análoga a @Before. Pero dicho método se llama solo una vez antes de todas las pruebas para la clase dada y, por lo tanto, debe ser estático. Se utiliza para realizar operaciones que consumen más recursos, como activar una base de datos de prueba.
- @AfterClass es lo opuesto a @BeforeClass: se ejecuta una vez para la clase dada, pero solo después de todas las pruebas. Se utiliza, por ejemplo, para borrar recursos persistentes o desconectarse de una base de datos.
- @Ignore indica que un método está deshabilitado y se ignorará durante la ejecución de la prueba general. Esto se usa en varias situaciones, por ejemplo, si se ha cambiado el método base y la prueba aún no se ha vuelto a trabajar para adaptarse a los cambios. En tales casos, también es deseable agregar una descripción, es decir, @Ignore("Alguna descripción").
- @Test(expected = Exception.class) se usa para pruebas negativas. Estas son pruebas que verifican cómo se comporta el método en caso de error, es decir, la prueba espera que el método arroje algún tipo de excepción. Dicho método se indica mediante la anotación @Test, pero con una indicación de qué error detectar.
- @Test(timeout = 100) comprueba que el método se ejecuta en no más de 100 milisegundos.
- @Mock se usa encima de un campo para asignar un objeto simulado (esta no es una anotación JUnit, sino que proviene de Mockito). Según sea necesario, establecemos el comportamiento del simulacro para una situación específica directamente en el método de prueba.
- @RunWith(MockitoJUnitRunner.class) se coloca encima de una clase. Esta anotación le dice a JUnit que invoque las pruebas en la clase. Hay varios corredores, incluidos estos: MockitoJUnitRunner, JUnitPlatform y SpringRunner. En JUnit 5, la anotación @RunWith se reemplazó con la anotación @ExtendWith más potente.
- afirmarEquals(Objeto esperado, Objeto actual) : comprueba si los objetos pasados son iguales.
- assertTrue(bandera booleana) — comprueba si el valor pasado es verdadero.
- afirmarFalse (bandera booleana) : comprueba si el valor pasado es falso.
- afirmarNull(Objeto objeto) — comprueba si el objeto pasado es nulo.
- assertSame(Object firstObject, Object secondObject) — comprueba si los valores pasados se refieren al mismo objeto.
- afirmar que (T t, Matcher
emparejador) — Comprueba si t cumple la condición especificada en el comparador.
Pruebas en la práctica
Ahora veamos el material anterior en un ejemplo específico. Probaremos el método de actualización de un servicio. No consideraremos la capa DAO, ya que estamos usando la predeterminada. Agreguemos un iniciador para las pruebas:<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<version>2.2.2.RELEASE</version>
<scope>test</scope>
</dependency>
Y aquí tenemos la clase de servicio:
@Service
@RequiredArgsConstructor
public class RobotServiceImpl implements RobotService {
private final RobotDAO robotDAO;
@Override
public Robot update(Long id, Robot robot) {
Robot found = robotDAO.findById(id);
return robotDAO.update(Robot.builder()
.id(id)
.name(robot.getName() != null ? robot.getName() : found.getName())
.cpu(robot.getCpu() != null ? robot.getCpu() : found.getCpu())
.producer(robot.getProducer() != null ? robot.getProducer() : found.getProducer())
.build());
}
}
Línea 8: extraiga el objeto actualizado de la base de datos. Líneas 9-14: cree un objeto a través del constructor. Si el objeto entrante tiene un campo, configúrelo. Si no, dejaremos lo que hay en la base de datos. Ahora mira nuestra prueba:
@RunWith(MockitoJUnitRunner.class)
public class RobotServiceImplTest {
@Mock
private RobotDAO robotDAO;
private RobotServiceImpl robotService;
private static Robot testRobot;
@BeforeClass
public static void prepareTestData() {
testRobot = Robot
.builder()
.id(123L)
.name("testRobotMolly")
.cpu("Intel Core i7-9700K")
.producer("China")
.build();
}
@Before
public void init() {
robotService = new RobotServiceImpl(robotDAO);
}
Línea 1: nuestro Runner. Línea 4: aislamos el servicio de la capa DAO sustituyéndolo por un simulacro. Línea 11: configuramos una entidad de prueba (la que usaremos como conejillo de indias) para la clase. Línea 22: configuramos el objeto de servicio, que es lo que probaremos.
@Test
public void updateTest() {
when(robotDAO.findById(any(Long.class))).thenReturn(testRobot);
when(robotDAO.update(any(Robot.class))).then(returnsFirstArg());
Robot robotForUpdate = Robot
.builder()
.name("Vally")
.cpu("AMD Ryzen 7 2700X")
.build();
Robot resultRobot = robotService.update(123L, robotForUpdate);
assertNotNull(resultRobot);
assertSame(resultRobot.getId(),testRobot.getId());
assertThat(resultRobot.getName()).isEqualTo(robotForUpdate.getName());
assertTrue(resultRobot.getCpu().equals(robotForUpdate.getCpu()));
assertEquals(resultRobot.getProducer(),testRobot.getProducer());
}
Aquí vemos que la prueba tiene tres divisiones claras: Líneas 3-9 — especificando accesorios. Línea 11 — ejecutando el código bajo prueba. Líneas 13-17: comprobación de los resultados. En mayor detalle: Líneas 3-4: establezca el comportamiento para el simulacro de DAO. Línea 5: configure la instancia que actualizaremos además de nuestro estándar. Línea 11: use el método y tome la instancia resultante. Línea 13: comprueba que no sea nulo. Línea 14: compare la ID del resultado y los argumentos del método dado. Línea 15: compruebe si se actualizó el nombre. Línea 16: vea el resultado de la CPU. Línea 17: no especificamos este campo en la instancia, por lo que debería permanecer igual. Verificamos esa condición aquí. Vamos a ejecutarlo:¡La prueba es verde! Podemos respirar aliviados :) En resumen, las pruebas mejoran la calidad del código y hacen que el proceso de desarrollo sea más flexible y confiable. Imagínese cuánto esfuerzo se necesita para rediseñar el software que involucra cientos de archivos de clase. Cuando tenemos pruebas unitarias escritas para todas estas clases, podemos refactorizar con confianza. Y lo más importante, nos ayuda a encontrar errores fácilmente durante el desarrollo. Chicos y chicas, eso es todo lo que tengo hoy. Dale me gusta y deja un comentario :)