CodeGym/Blog Java/Random-ES/Todo sobre pruebas unitarias: técnicas, conceptos, prácti...
John Squirrels
Nivel 41
San Francisco

Todo sobre pruebas unitarias: técnicas, conceptos, práctica

Publicado en el grupo Random-ES
Hoy en día no encontrará una aplicación que no esté repleta de pruebas, por lo que este tema será más relevante que nunca para los desarrolladores novatos: no puede tener éxito sin pruebas. Consideremos qué tipos de pruebas se utilizan en principio y luego estudiaremos en detalle todo lo que hay que saber sobre las pruebas unitarias. Todo sobre las pruebas unitarias: técnicas, conceptos, práctica - 1

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.
Todo esto suena bien, pero ¿cómo funciona en la práctica? ¡Simple! Usamos la pirámide de prueba de Mike Cohn: Todo sobre las pruebas unitarias: técnicas, conceptos, práctica - 2Esta es una versión simplificada de la pirámide: ahora está dividida en partes aún más pequeñas. Pero hoy no nos pondremos demasiado sofisticados. Consideraremos la versión más simple.
  1. 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).

  2. 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.

  3. 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. Todo sobre las pruebas unitarias: técnicas, conceptos, práctica - 3El 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.
TDD se basa en pruebas unitarias, ya que son los bloques de construcción más pequeños en la pirámide de automatización de pruebas. Con las pruebas unitarias, podemos probar la lógica empresarial de cualquier clase. BDD significa desarrollo impulsado por el comportamiento. Este enfoque se basa en TDD. Más específicamente, utiliza ejemplos en lenguaje sencillo que explican el comportamiento del sistema para todos los involucrados en el desarrollo. No profundizaremos en este término, ya que afecta principalmente a testers y analistas de negocio. Un caso de prueba es un escenario que describe los pasos, las condiciones específicas y los parámetros necesarios para verificar el código bajo prueba. Un accesorio de prueba es un código que configura el entorno de prueba para que tenga el estado necesario para que el método bajo prueba se ejecute correctamente. Es un conjunto predefinido de objetos y su comportamiento bajo condiciones específicas.

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.
Todo sobre las pruebas unitarias: técnicas, conceptos, práctica - 4Para garantizar la modularidad de las pruebas, debe aislarse de otras capas de la aplicación. Esto se puede hacer usando stubs, simulacros y espías. Los simulacros son objetos que se pueden personalizar (por ejemplo, a medida para cada prueba). Nos permiten especificar lo que esperamos de las llamadas a métodos, es decir, las respuestas esperadas. Usamos objetos simulados para verificar que obtenemos lo que esperamos. Los stubs proporcionan una respuesta codificada a las llamadas durante las pruebas. También pueden almacenar información sobre la llamada (por ejemplo, parámetros o el número de llamadas). Estos a veces se conocen como espías. A veces, las personas confunden los términos stub y mock: la diferencia es que un stub no verifica nada, solo simula un estado determinado. Un simulacro es un objeto que tiene expectativas. Por ejemplo, que un método dado debe llamarse un cierto número de veces. En otras palabras,

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: Todo sobre pruebas unitarias: técnicas, conceptos, práctica - 5Una 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.
Echemos un vistazo a algunos métodos utilizados para comparar resultados:

  • 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.
AssertJ también proporciona un método de comparación útil: assertThat(firstObject).isEqualTo(secondObject) . Aquí mencioné los métodos básicos; los otros son variaciones de los anteriores.

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:Todo sobre las pruebas unitarias: técnicas, conceptos, práctica - 6¡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 :)
Comentarios
  • Populares
  • Nuevas
  • Antiguas
Debes iniciar sesión para dejar un comentario
Esta página aún no tiene comentarios