JavaRush /Blog Java /Random-ES /Pruebas unitarias de Java: técnicas, conceptos, práctica....

Pruebas unitarias de Java: técnicas, conceptos, práctica.

Publicado en el grupo Random-ES
Hoy en día difícilmente encontrará una aplicación que no esté cubierta con pruebas, por lo que este tema será más relevante que nunca para los desarrolladores novatos: sin pruebas no se puede llegar a ninguna parte. Como publicidad, te sugiero que mires mis artículos anteriores. Algunos de ellos cubren pruebas (y aun así los artículos te serán de gran utilidad):
  1. Pruebas de integración de una base de datos usando MariaDB para reemplazar MySql
  2. Implementación de aplicación multilingüe.
  3. Guardar archivos en la aplicación y datos sobre ellos en la base de datos.
Consideremos qué tipos de pruebas se utilizan en principio y luego estudiaremos en detalle todo lo que necesita saber sobre las pruebas unitarias.

tipos de pruebas

¿Qué es una prueba? Como dice Wiki: " Una prueba o prueba es una forma de estudiar los procesos subyacentes de un sistema colocando el sistema en diferentes situaciones y rastreando los cambios observables en él". Es decir, se trata de una prueba del correcto funcionamiento de nuestro sistema en determinadas situaciones. Todo sobre Pruebas unitarias: técnicas, conceptos, práctica - 2Bueno, veamos qué tipos de pruebas hay:
  1. Las pruebas unitarias son pruebas cuya tarea es probar cada módulo del sistema individualmente. Es deseable que sean piezas del sistema mínimamente divisibles, por ejemplo, módulos.

  2. La prueba del sistema es una prueba de alto nivel para probar el funcionamiento de una parte más grande de una aplicación o del sistema en su conjunto.

  3. Las pruebas de regresión son pruebas que se utilizan para comprobar si las nuevas funciones o las correcciones de errores afectan la funcionalidad existente de la aplicación y si reaparecen errores antiguos.

  4. Las pruebas funcionales consisten en comprobar el cumplimiento de parte de la aplicación con los requisitos establecidos en especificaciones, historias de usuarios, etc.

    Tipos de pruebas funcionales:

    • prueba de “caja blanca” para comprobar el cumplimiento de parte de la aplicación con los requisitos con conocimiento de la implementación interna del sistema;
    • Prueba de “caja negra” para comprobar el cumplimiento de parte de la aplicación con los requisitos sin conocimiento de la implementación interna del sistema.
  5. Las pruebas de rendimiento son un tipo de pruebas escritas para determinar la velocidad a la que se ejecuta un sistema o parte de él bajo una determinada carga.
  6. Pruebas de carga : pruebas diseñadas para verificar la estabilidad del sistema bajo cargas estándar y encontrar el pico máximo posible en el que la aplicación funciona correctamente.
  7. Las pruebas de estrés son un tipo de prueba diseñada para verificar la funcionalidad de una aplicación bajo cargas no estándar y para determinar el pico máximo posible en el que el sistema no fallará.
  8. Pruebas de seguridad : pruebas utilizadas para comprobar la seguridad de un sistema (de ataques de piratas informáticos, virus, acceso no autorizado a datos confidenciales y otros placeres de la vida).
  9. Las pruebas de localización son pruebas de localización para una aplicación.
  10. Las pruebas de usabilidad son un tipo de prueba destinada a comprobar la usabilidad, la comprensibilidad, el atractivo y la capacidad de aprendizaje para los usuarios.
  11. Todo esto suena bien, pero ¿cómo funciona en la práctica? Es simple: se utiliza la pirámide de prueba de Mike Cohn: Todo sobre Pruebas unitarias: técnicas, conceptos, práctica - 4esta es una versión simplificada de la pirámide: ahora está dividida en partes más pequeñas. Pero hoy no pervertiremos y consideraremos la opción más sencilla.
    1. Unidad : pruebas unitarias utilizadas en varias capas de la aplicación, que prueban la lógica divisible más pequeña de la aplicación: por ejemplo, una clase, pero generalmente un método. Estas pruebas suelen intentar aislarse lo más posible de la lógica externa, es decir, crear la ilusión de que el resto de la aplicación está funcionando en modo estándar.

      Siempre debería haber muchos de estos tests (más que otros tipos), ya que prueban piezas pequeñas y son muy ligeros, no consumen muchos recursos (por recursos me refiero a RAM y tiempo).

    2. Integración - pruebas de integración. Verifica partes más grandes del sistema, es decir, si es una combinación de varias partes de lógica (varios métodos o clases) o la exactitud de trabajar con un componente externo. Por lo general, hay menos pruebas de este tipo que pruebas unitarias, ya que son más pesadas.

      Como ejemplo de pruebas de integración, puede considerar conectarse a una base de datos y verificar que los métodos que trabajan con ella funcionan correctamente .

    3. UI : pruebas que verifican el funcionamiento de la interfaz de usuario. Afectan la lógica en todos los niveles de la aplicación, por eso también se les llama de un extremo a otro. Como regla general, hay muchos menos, ya que son los más pesados ​​​​y deben revisar los caminos más necesarios (usados).

      En la figura anterior vemos la relación de las áreas de diferentes partes del triángulo: aproximadamente la misma proporción se mantiene en el número de estas pruebas en el trabajo real.

      Hoy veremos más de cerca las pruebas más utilizadas: las pruebas unitarias, ya que todos los desarrolladores de Java que se precien deberían poder utilizarlas en un nivel básico.

    Conceptos clave de las pruebas unitarias

    La cobertura de la prueba (cobertura del código) es una de las principales evaluaciones de la calidad de las pruebas de la aplicación. Este es el porcentaje de código cubierto por las pruebas (0-100%). En la práctica, mucha gente persigue este porcentaje, con el que no estoy de acuerdo, ya que empiezan a añadir pruebas donde no son necesarias. Por ejemplo, nuestro servicio tiene operaciones CRUD (crear/obtener/actualizar/eliminar) estándar sin lógica adicional. Estos métodos son puramente intermediarios que delegan el trabajo a la capa que trabaja con el repositorio. En esta situación, no tenemos nada que probar: tal vez si este método llama a un método del Tao, pero esto no es serio. Para evaluar la cobertura de las pruebas se suelen utilizar herramientas adicionales: JaCoCo, Cobertura, Clover, Emma, ​​​​etc. Para un estudio más detallado de este tema, conserve un par de artículos adecuados: TDD (desarrollo basado en pruebas) : desarrollo basado en pruebas. En este enfoque, en primer lugar, se escribe una prueba que verificará un código específico. Resulta ser una prueba de caja negra: sabemos qué hay en la entrada y sabemos qué debería suceder en la salida. Esto evita la duplicación de código. El desarrollo basado en pruebas comienza con el diseño y desarrollo de pruebas para cada pequeña funcionalidad de la aplicación. En el enfoque TDD, primero se desarrolla una prueba que define y verifica lo que hará el código. El objetivo principal de TDD es hacer que el código sea más claro, sencillo y libre de errores. Todo sobre las pruebas unitarias: métodos, conceptos, práctica - 6El enfoque consta de los siguientes componentes:
    1. Estamos escribiendo nuestra prueba.
    2. Ejecutamos la prueba, haya pasado o no (vemos que todo está en rojo, no te asustes: así debe ser).
    3. Agregamos el código que debería satisfacer esta prueba (ejecutar la prueba).
    4. Refactorizamos el código.
    Partiendo del hecho de que las pruebas unitarias son los elementos más pequeños de la pirámide de automatización de pruebas, TDD se basa en ellas. Con la ayuda de pruebas unitarias podemos probar la lógica empresarial de cualquier clase. BDD (Desarrollo impulsado por el comportamiento) : desarrollo a través del comportamiento. Este enfoque se basa en TDD. Más concretamente, utiliza ejemplos escritos en un lenguaje claro (normalmente en inglés) que ilustran el comportamiento del sistema para todos los implicados en el desarrollo. No profundizaremos en este término, ya que afecta principalmente a testers y analistas de negocio. Caso de prueba : un script que describe los pasos, condiciones específicas y parámetros necesarios para verificar la implementación del código bajo prueba. Accesorio es un estado del entorno de prueba que es necesario para la ejecución exitosa del método bajo prueba. Se trata de un conjunto predeterminado de objetos y su comportamiento en las condiciones utilizadas.

    Etapas de prueba

    La prueba consta de tres etapas:
    1. Especificación de los datos a probar (accesorios).
    2. Usando el código bajo prueba (llamando al método bajo prueba).
    3. Comprobando los resultados y comparándolos con los esperados.
    Todo sobre las pruebas unitarias: métodos, conceptos, práctica - 7Para garantizar la modularidad de la prueba, debe estar aislado de otras capas de la aplicación. Esto se puede hacer utilizando stubs, simulacros y espías. Los simulacros son objetos que se pueden personalizar (por ejemplo, específicos para cada prueba) y le permiten establecer expectativas para las llamadas a métodos en forma de respuestas que planeamos recibir. Las comprobaciones de expectativas se realizan mediante llamadas a objetos simulados. Talones : proporcionan una respuesta cableada a las llamadas durante las pruebas. También pueden almacenar información sobre la llamada (por ejemplo, parámetros o el número de estas llamadas). A veces se les llama con su propio término: espía ( Spy ). A veces se confunden estos términos stub y simulacro : la diferencia es que un stub no comprueba nada, sino que sólo simula un estado determinado. Un simulacro es un objeto que tiene expectativas. Por ejemplo, que un método de clase determinado debe llamarse un número determinado de veces. En otras palabras, su prueba nunca se romperá debido a un trozo, pero podría fallar debido a una simulación.

    Entornos de prueba

    Así que ahora vayamos al grano. Hay varios entornos de prueba (frameworks) disponibles para Java. Los más populares son JUnit y TestNG. Para nuestra revisión, utilizamos: Todo sobre pruebas unitarias: métodos, conceptos, práctica - 8Una prueba JUnit es un método contenido en una clase que se usa sólo para pruebas. Por lo general, una clase recibe el mismo nombre que la clase que está probando con +Test al final. Por ejemplo, CarService→ CarServiceTest. El sistema de compilación Maven incluye automáticamente dichas clases en el área de prueba. De hecho, esta clase se llama clase de prueba. Repasemos un poco las anotaciones básicas: @Test : definición de este método como método de prueba (de hecho, el método marcado con esta anotación es una prueba unitaria). @Before : marca el método que se ejecutará antes de cada prueba. Por ejemplo, completar datos de prueba de clase, leer datos de entrada, etc. @After : se coloca encima del método que se llamará después de cada prueba (limpiar datos, restaurar valores predeterminados). @BeforeClass - colocado encima del método - análogo a @Before. Pero este método se llama sólo una vez antes de todas las pruebas para una clase determinada y, por lo tanto, debe ser estático. Se utiliza para realizar operaciones más pesadas, como levantar una base de datos de prueba. @AfterClass es lo opuesto a @BeforeClass: se ejecuta una vez para una clase determinada, pero se ejecuta después de todas las pruebas. Se utiliza, por ejemplo, para limpiar recursos persistentes o desconectarse de la base de datos. @Ignore : señala que el método siguiente está deshabilitado y se ignorará al ejecutar pruebas en general. Se utiliza en diferentes casos, por ejemplo, si se cambió el método base y no hubo tiempo para rehacer la prueba. En tales casos, también es recomendable agregar una descripción: @Ignore("Alguna descripción"). @Test (esperado = Exception.class): se utiliza para pruebas negativas. Estas son pruebas que verifican cómo se comporta un método en caso de error, es decir, la prueba espera que el método arroje alguna excepción. Dicho método se indica mediante la anotación @Test, pero con un error que detectar. @Test(timeout=100) : comprueba que el método se ejecute en no más de 100 milisegundos. @Mock : se usa una clase sobre un campo para establecer un objeto determinado como simulado (esto no es de la biblioteca Junit, sino de Mockito), y si lo necesitamos, estableceremos el comportamiento del simulacro en una situación específica. , directamente en el método de prueba. @RunWith(MockitoJUnitRunner.class) : el método se coloca encima de la clase. Este es el botón para ejecutar pruebas en él. Los corredores pueden ser diferentes: por ejemplo, existen los siguientes: MockitoJUnitRunner, JUnitPlatform, SpringRunner, etc.). En JUnit 5, la anotación @RunWith fue reemplazada por la anotación @ExtendWith más poderosa. Echemos un vistazo a algunos métodos para comparar resultados:
    • assertEquals(Object expecteds, Object actuals)— comprueba si los objetos transmitidos son iguales.
    • assertTrue(boolean flag)— comprueba si el valor pasado devuelve verdadero.
    • assertFalse(boolean flag)— comprueba si el valor pasado devuelve falso.
    • assertNull(Object object)– comprueba si el objeto es nulo.
    • assertSame(Object firstObject, Object secondObject)— comprueba si los valores pasados ​​se refieren al mismo objeto.
    • assertThat(T t, Matcher<T> matcher)— comprueba si t satisface la condición especificada en el comparador.
    También hay un formulario de comparación útil de afirmarj: assertThat(firstObject).isEqualTo(secondObject) aquí hablé sobre los métodos básicos, ya que el resto son variaciones diferentes de los anteriores.

    Práctica de prueba

    Ahora veamos el material anterior usando un ejemplo específico. Probaremos el método del servicio: actualización. No consideraremos la capa dao, ya que es nuestra 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>
    Entonces, 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());
       }
    }
    8 - extraiga el objeto actualizado de la base de datos 9-14 - cree el objeto a través del constructor, si el objeto entrante tiene un campo - configúrelo, si no - deje lo que está en la base de datos Y mire 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);
       }
    1 - nuestro Runner 4 - aísla el servicio de la capa dao sustituyendo un simulacro 11 - establece una entidad de prueba para la clase (la que usaremos como hámster de prueba) 22 - establece un objeto de servicio 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 una clara división de la prueba en tres partes: 3-9 - configuración de accesorios 11 - ejecución de la parte probada 13-17 - verificación de los resultados Más detalles: 3-4 - configuración del comportamiento de moka dao 5 - configuración de una instancia que actualizaremos además de nuestro estándar 11 - use el método y tome la instancia resultante 13 - verifique que no sea cero 14 - verifique el ID del resultado y los argumentos del método especificado 15 - verifique si el nombre se ha actualizado 16 - mire en el resultado de la CPU 17: dado que no configuramos esto en el campo de instancia de actualización, debería permanecer igual, verifiquémoslo. Todo sobre pruebas unitarias: métodos, conceptos, práctica - 9Comencemos: Todo sobre Pruebas unitarias: técnicas, conceptos, práctica - 10la prueba está en verde, puede exhalar)) Entonces, resumamos: 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 tendríamos que dedicar al rediseñar un software con cientos de archivos de clase. Una vez que tengamos pruebas unitarias escritas para todas estas clases, podremos refactorizar con confianza. Y lo más importante, nos ayuda a encontrar errores fácilmente durante el desarrollo. Chicos, eso es todo para mí hoy: agreguen me gusta, escriban comentarios))) Todo sobre pruebas unitarias: métodos, conceptos, práctica - 11
Comentarios
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION