Las pruebas unitarias , también conocidas como pruebas de componentes o módulos , son una forma de prueba de software mediante la cual se prueba un código fuente aislado para validar el comportamiento esperado. [1]
Las pruebas unitarias describen las pruebas que se ejecutan a nivel de unidad para contrastar las pruebas a nivel de integración o de sistema .
Las pruebas unitarias, como principio para probar por separado partes más pequeñas de sistemas de software grandes, se remontan a los primeros días de la ingeniería de software. En junio de 1956, HD Benington presentó en el Simposio de la Marina de los EE. UU. sobre métodos avanzados de programación para computadoras digitales el proyecto SAGE y su enfoque basado en especificaciones, donde la fase de codificación era seguida por una "prueba de parámetros" para validar los subprogramas de los componentes en relación con su especificación, seguida luego por una "prueba de ensamblaje" para las partes ensambladas. [2] [3]
En 1964, se describe un enfoque similar para el software del proyecto Mercury , donde las unidades individuales desarrolladas por diferentes programas se sometieron a "pruebas unitarias" antes de integrarse entre sí. [4] En 1969, las metodologías de prueba aparecen más estructuradas, con pruebas unitarias, pruebas de componentes y pruebas de integración con el propósito de validar partes individuales escritas por separado y su ensamblaje progresivo en bloques más grandes. [5] Algunas normas públicas adoptadas a finales de los años 60, como MIL-STD-483 [6] y MIL-STD-490 contribuyeron aún más a una amplia aceptación de las pruebas unitarias en proyectos de gran tamaño.
En aquella época, las pruebas unitarias eran interactivas [3] o automatizadas [7], y se utilizaban pruebas codificadas o herramientas de prueba de captura y reproducción . En 1989, Kent Beck describió un marco de pruebas para Smalltalk (posteriormente llamado SUnit ) en "Simple Smalltalk Testing: With Patterns". En 1997, Kent Beck y Erich Gamma desarrollaron y lanzaron JUnit , un marco de pruebas unitarias que se hizo popular entre los desarrolladores de Java . [8] Google adoptó las pruebas automatizadas alrededor de 2005-2006. [9]
La unidad se define como un comportamiento único exhibido por el sistema bajo prueba (SUT), que generalmente corresponde a un requisito. Si bien puede implicar que se trata de una función o un módulo (en programación procedimental ) o un método o una clase (en programación orientada a objetos ), no significa que las funciones/métodos, módulos o clases siempre correspondan a unidades. Desde la perspectiva de los requisitos del sistema, solo el perímetro del sistema es relevante, por lo tanto, solo los puntos de entrada a los comportamientos del sistema visibles externamente definen unidades. [10]
Las pruebas unitarias se pueden realizar de forma manual o mediante la ejecución automática de pruebas . Las pruebas automáticas incluyen ventajas como: ejecución frecuente de pruebas, ejecución sin costos de personal y pruebas consistentes y repetibles.
Las pruebas suelen ser realizadas por el programador que escribe y modifica el código que se está probando. Las pruebas unitarias pueden considerarse parte del proceso de escritura del código.
Durante el desarrollo, un programador puede codificar criterios o resultados que se sabe que son buenos en la prueba para verificar la corrección de la unidad.
Durante la ejecución de las pruebas, los marcos registran las pruebas que no cumplen algún criterio y las informan en un resumen.
Para ello, el enfoque más utilizado es prueba-función-valor esperado.
Una prueba parametrizada es una prueba que acepta un conjunto de valores que se pueden usar para permitir que la prueba se ejecute con varios valores de entrada diferentes. Un marco de prueba que admita pruebas parametrizadas admite una forma de codificar conjuntos de parámetros y ejecutar la prueba con cada conjunto.
El uso de pruebas parametrizadas puede reducir la duplicación del código de prueba.
Las pruebas parametrizadas son compatibles con TestNG , JUnit , [13] XUnit y NUnit , así como con varios marcos de prueba de JavaScript. [ cita requerida ]
Los parámetros para las pruebas unitarias pueden codificarse manualmente o, en algunos casos, generarse automáticamente mediante el marco de pruebas. En los últimos años, se agregó soporte para escribir pruebas (unitarias) más potentes, aprovechando el concepto de teorías, casos de prueba que ejecutan los mismos pasos, pero utilizando datos de prueba generados en tiempo de ejecución, a diferencia de las pruebas parametrizadas regulares que utilizan los mismos pasos de ejecución con conjuntos de entrada que están predefinidos. [ cita requerida ]
A veces, en el desarrollo ágil de software, las pruebas unitarias se realizan por historia de usuario y se realizan en la segunda mitad del sprint, una vez que se completan la recopilación de requisitos y el desarrollo. Normalmente, los desarrolladores u otros miembros del equipo de desarrollo, como los consultores , escriben "scripts de prueba" paso a paso para que los desarrolladores los ejecuten en la herramienta. Los scripts de prueba se escriben generalmente para demostrar el funcionamiento técnico y efectivo de características desarrolladas específicas en la herramienta, a diferencia de los procesos comerciales completos que serían interconectados por el usuario final , lo que generalmente se hace durante las pruebas de aceptación del usuario . Si el script de prueba se puede ejecutar completamente de principio a fin sin incidentes, se considera que la prueba unitaria ha "pasado", de lo contrario, se anotan los errores y la historia de usuario se devuelve al desarrollo en un estado "en progreso". Las historias de usuario que pasan con éxito las pruebas unitarias pasan a los pasos finales del sprint: revisión de código, revisión por pares y, por último, una sesión de "presentación" que demuestra la herramienta desarrollada a las partes interesadas.
En el desarrollo basado en pruebas (TDD), las pruebas unitarias se escriben mientras se escribe el código de producción. A partir del código funcional, el desarrollador agrega el código de prueba para un comportamiento requerido, luego agrega el código suficiente para que la prueba pase, luego refactoriza el código (incluido el código de prueba) según tenga sentido y luego repite agregando otra prueba.
Las pruebas unitarias tienen como objetivo garantizar que las unidades cumplan con su diseño y se comporten según lo previsto. [14]
Al escribir pruebas primero para las unidades comprobables más pequeñas y luego para los comportamientos compuestos entre ellas, se pueden crear pruebas integrales para aplicaciones complejas. [14]
Uno de los objetivos de las pruebas unitarias es aislar cada parte del programa y demostrar que cada una de ellas es correcta. [1] Una prueba unitaria proporciona un contrato escrito estricto que el fragmento de código debe satisfacer.
Las pruebas unitarias detectan problemas en las primeras fases del ciclo de desarrollo . Esto incluye tanto errores en la implementación del programador como fallas o partes faltantes de la especificación de la unidad. El proceso de escribir un conjunto completo de pruebas obliga al autor a pensar en las entradas, salidas y condiciones de error y, por lo tanto, a definir con mayor precisión el comportamiento deseado de la unidad. [ cita requerida ]
El costo de encontrar un error antes de comenzar a codificar o cuando el código se escribe por primera vez es considerablemente menor que el costo de detectar, identificar y corregir el error más tarde. Los errores en el código publicado también pueden causar problemas costosos para los usuarios finales del software. [15] [16] [17] El código puede ser imposible o difícil de probar unitariamente si está mal escrito, por lo que las pruebas unitarias pueden obligar a los desarrolladores a estructurar funciones y objetos de mejores maneras.
Las pruebas unitarias permiten lanzamientos más frecuentes en el desarrollo de software. Al probar componentes individuales de forma aislada, los desarrolladores pueden identificar y solucionar problemas rápidamente, lo que conduce a ciclos de iteración y lanzamiento más rápidos. [18]
Las pruebas unitarias permiten al programador refactorizar el código o actualizar las bibliotecas del sistema en una fecha posterior y asegurarse de que el módulo aún funciona correctamente (por ejemplo, en las pruebas de regresión ). El procedimiento consiste en escribir casos de prueba para todas las funciones y métodos de modo que, siempre que un cambio provoque una falla, se pueda identificar rápidamente.
Las pruebas unitarias detectan cambios que pueden romper un contrato de diseño .
Las pruebas unitarias pueden reducir la incertidumbre en las propias unidades y pueden utilizarse con un enfoque de pruebas ascendente . Al probar primero las partes de un programa y luego la suma de sus partes, las pruebas de integración se vuelven mucho más fáciles. [ cita requerida ]
Algunos programadores sostienen que las pruebas unitarias proporcionan una forma de documentación del código. Los desarrolladores que quieran saber qué funcionalidad proporciona una unidad y cómo utilizarla pueden revisar las pruebas unitarias para comprenderlas. [ cita requerida ]
Los casos de prueba pueden incorporar características que son fundamentales para el éxito de la unidad. Estas características pueden indicar el uso apropiado o inadecuado de una unidad, así como los comportamientos negativos que la unidad debe detectar. Un caso de prueba documenta estas características críticas, aunque muchos entornos de desarrollo de software no dependen únicamente del código para documentar el producto en desarrollo. [ cita requerida ]
En algunos procesos, el acto de escribir pruebas y el código bajo prueba, más la refactorización asociada, pueden reemplazar al diseño formal. Cada prueba unitaria puede verse como un elemento de diseño que especifica clases, métodos y comportamiento observable. [ cita requerida ]
Las pruebas no detectarán todos los errores del programa, porque no pueden evaluar cada ruta de ejecución en ningún programa, excepto en los más triviales. Este problema es un superconjunto del problema de detención , que es indecidible . Lo mismo es cierto para las pruebas unitarias. Además, las pruebas unitarias, por definición, solo prueban la funcionalidad de las unidades mismas. Por lo tanto, no detectarán errores de integración o errores más amplios a nivel de sistema (como funciones realizadas en varias unidades o áreas de prueba no funcionales como el rendimiento ). Las pruebas unitarias deben realizarse junto con otras actividades de prueba de software , ya que solo pueden mostrar la presencia o ausencia de errores particulares; no pueden probar una ausencia completa de errores. Para garantizar el comportamiento correcto para cada ruta de ejecución y cada entrada posible, y asegurar la ausencia de errores, se requieren otras técnicas, a saber, la aplicación de métodos formales para demostrar que un componente de software no tiene un comportamiento inesperado. [ cita requerida ]
Una jerarquía elaborada de pruebas unitarias no equivale a pruebas de integración. La integración con unidades periféricas debería incluirse en las pruebas de integración, pero no en las pruebas unitarias. [ cita requerida ] Las pruebas de integración generalmente todavía dependen en gran medida de que las personas realicen pruebas manuales ; las pruebas de alto nivel o de alcance global pueden ser difíciles de automatizar, de modo que las pruebas manuales a menudo parecen más rápidas y económicas. [ cita requerida ]
Las pruebas de software son un problema combinatorio. Por ejemplo, cada sentencia de decisión booleana requiere al menos dos pruebas: una con un resultado de "verdadero" y otra con un resultado de "falso". Como resultado, por cada línea de código escrita, los programadores a menudo necesitan de 3 a 5 líneas de código de prueba. [ cita requerida ] Obviamente, esto lleva tiempo y su inversión puede no valer la pena el esfuerzo. Hay problemas que no se pueden probar fácilmente en absoluto, por ejemplo, aquellos que no son deterministas o involucran múltiples subprocesos . Además, el código para una prueba unitaria tiene la misma probabilidad de tener errores que el código que está probando. Fred Brooks en The Mythical Man-Month cita: "Nunca te hagas a la mar con dos cronómetros; lleva uno o tres". [19] Es decir, si dos cronómetros se contradicen, ¿cómo sabes cuál es el correcto?
Otro desafío relacionado con la escritura de pruebas unitarias es la dificultad de configurar pruebas realistas y útiles. Es necesario crear condiciones iniciales relevantes para que la parte de la aplicación que se está probando se comporte como parte del sistema completo. Si estas condiciones iniciales no se configuran correctamente, la prueba no estará ejercitando el código en un contexto realista, lo que disminuye el valor y la precisión de los resultados de las pruebas unitarias. [ cita requerida ]
Para obtener los beneficios previstos de las pruebas unitarias, se necesita una disciplina rigurosa durante todo el proceso de desarrollo de software.
Es esencial mantener registros detallados no sólo de las pruebas que se han realizado, sino también de todos los cambios que se han hecho al código fuente de esta o cualquier otra unidad del software. El uso de un sistema de control de versiones es esencial. Si una versión posterior de la unidad no pasa una prueba particular que había aprobado previamente, el software de control de versiones puede proporcionar una lista de los cambios del código fuente (si los hubiera) que se han aplicado a la unidad desde entonces. [ cita requerida ]
También es esencial implementar un proceso sostenible para garantizar que las fallas de los casos de prueba se revisen regularmente y se aborden de inmediato. [20] Si dicho proceso no se implementa y se arraiga en el flujo de trabajo del equipo, la aplicación evolucionará fuera de sincronía con el conjunto de pruebas unitarias, lo que aumentará los falsos positivos y reducirá la efectividad del conjunto de pruebas.
Las pruebas unitarias de software de sistemas integrados presentan un desafío único: debido a que el software se desarrolla en una plataforma diferente de aquella en la que finalmente se ejecutará, no se puede ejecutar fácilmente un programa de prueba en el entorno de implementación real, como es posible con los programas de escritorio. [21]
Las pruebas unitarias tienden a ser más fáciles cuando un método tiene parámetros de entrada y alguna salida. No es tan fácil crear pruebas unitarias cuando una función principal del método es interactuar con algo externo a la aplicación. Por ejemplo, un método que funcionará con una base de datos podría requerir la creación de una maqueta de interacciones con la base de datos, que probablemente no será tan completa como las interacciones reales con la base de datos. [22] [ se necesita una mejor fuente ]
A continuación se muestra un ejemplo de un conjunto de pruebas JUnit. Se centra en la Adder
clase.
clase Adder { público int add ( int a , int b ) { devolver a + b ; } }
El conjunto de pruebas utiliza declaraciones de afirmación para verificar el resultado esperado de varios valores de entrada al sum
método.
importar org.junit.Assert.assertEquals estático ; importar org.junit.Test ; clase pública AdderUnitTest { @Test public void sumReturnsZeroForZeroInput () { Sumador sumador = nuevo Adder (); assertEquals ( 0 , sumador . add ( 0 , 0 )); } @Test public void sumReturnsSumOfTwoPositiveNumbers () { Sumador sumador = new Sumador (); assertEquals ( 3 , sumador . add ( 1 , 2 )); } @Test public void sumReturnsSumOfTwoNegativeNumbers () { Sumador sumador = new Sumador (); assertEquals ( - 3 , sumador . add ( - 1 , - 2 )); } @Test public void sumReturnsSumOfLargeNumbers () { Sumador sumador = new Sumador (); assertEquals ( 2222 , sumador . add ( 1234 , 988 )); } }
El uso de pruebas unitarias como especificación de diseño tiene una ventaja significativa sobre otros métodos de diseño: el documento de diseño (las pruebas unitarias en sí) se puede utilizar para verificar la implementación. Las pruebas nunca se aprobarán a menos que el desarrollador implemente una solución de acuerdo con el diseño.
Las pruebas unitarias carecen de la accesibilidad de una especificación diagramática como un diagrama UML , pero pueden generarse a partir de la prueba unitaria utilizando herramientas automatizadas. La mayoría de los lenguajes modernos tienen herramientas gratuitas (normalmente disponibles como extensiones de los IDE ). Las herramientas gratuitas, como las basadas en el marco xUnit , externalizan a otro sistema la representación gráfica de una vista para el consumo humano.
Las pruebas unitarias son la piedra angular de la programación extrema , que se basa en un marco de trabajo de pruebas unitarias automatizadas . Este marco de trabajo de pruebas unitarias automatizadas puede ser de terceros, por ejemplo, xUnit , o creado dentro del grupo de desarrollo.
La programación extrema utiliza la creación de pruebas unitarias para el desarrollo basado en pruebas . El desarrollador escribe una prueba unitaria que expone un requisito de software o un defecto. Esta prueba fallará porque el requisito aún no se implementó o porque expone intencionalmente un defecto en el código existente. Luego, el desarrollador escribe el código más simple para que la prueba, junto con otras pruebas, pase.
La mayor parte del código de un sistema se prueba unitariamente, pero no necesariamente todas las rutas a través del código. La programación extrema exige una estrategia de "probar todo lo que pueda romperse", en lugar del método tradicional de "probar cada ruta de ejecución". Esto lleva a los desarrolladores a desarrollar menos pruebas que los métodos clásicos, pero esto no es realmente un problema, sino más bien una reafirmación de un hecho, ya que los métodos clásicos rara vez se han seguido con la metódica suficiente como para que se hayan probado a fondo todas las rutas de ejecución. [ cita requerida ] La programación extrema simplemente reconoce que las pruebas rara vez son exhaustivas (porque a menudo son demasiado caras y requieren mucho tiempo para ser económicamente viables) y proporciona orientación sobre cómo concentrar eficazmente los recursos limitados.
Fundamentalmente, el código de prueba se considera un artefacto de proyecto de primera clase, ya que se mantiene con la misma calidad que el código de implementación, y se elimina toda duplicación. Los desarrolladores publican el código de prueba unitaria en el repositorio de código junto con el código que prueba. Las pruebas unitarias exhaustivas de la programación extrema permiten los beneficios mencionados anteriormente, como un desarrollo y una refactorización de código más simples y confiables , una integración de código simplificada, una documentación precisa y diseños más modulares. Estas pruebas unitarias también se ejecutan constantemente como una forma de prueba de regresión .
Las pruebas unitarias también son fundamentales para el concepto de diseño emergente . Como el diseño emergente depende en gran medida de la refactorización, las pruebas unitarias son un componente integral. [ cita requerida ]
Un marco de pruebas automatizadas proporciona funciones para automatizar la ejecución de pruebas y puede acelerar la escritura y la ejecución de pruebas. Se han desarrollado marcos para una amplia variedad de lenguajes de programación .
Generalmente, los frameworks son de terceros y no se distribuyen con un compilador o un entorno de desarrollo integrado (IDE).
Las pruebas se pueden escribir sin utilizar un marco de trabajo para ejercitar el código bajo prueba utilizando aserciones , manejo de excepciones y otros mecanismos de flujo de control para verificar el comportamiento e informar de los fallos. Algunos señalan que realizar pruebas sin un marco de trabajo es valioso, ya que existe una barrera de entrada para la adopción de un marco de trabajo; que tener algunas pruebas es mejor que ninguna, pero una vez que se implementa un marco de trabajo, agregar pruebas puede ser más fácil. [23]
En algunos marcos faltan funciones de prueba avanzadas y deben codificarse manualmente.
Algunos lenguajes de programación admiten directamente las pruebas unitarias. Su gramática permite la declaración directa de pruebas unitarias sin importar una biblioteca (ya sea de terceros o estándar). Además, las condiciones booleanas de las pruebas unitarias se pueden expresar con la misma sintaxis que las expresiones booleanas utilizadas en el código de pruebas no unitarias, como por ejemplo, las declaraciones if
y que se utilizan para while
.
Los lenguajes con soporte para pruebas unitarias incorporado incluyen:
Los lenguajes con soporte para el marco de pruebas unitarias estándar incluyen:
Algunos lenguajes no cuentan con compatibilidad integrada con pruebas unitarias, pero cuentan con bibliotecas o marcos de trabajo de pruebas unitarias establecidos. Entre estos lenguajes se incluyen:
El contratista deberá codificar y probar las Unidades de software, e ingresar el código fuente y el código objeto, y los listados asociados de cada Unidad probada con éxito en la Configuración de desarrollo.
{{cite book}}
: CS1 maint: date and year (link)