Examen de la unidad - Unit testing

En programación de computadoras , la prueba unitaria es un método de prueba de software mediante el cual se prueban unidades individuales de código fuente (conjuntos de uno o más módulos de programas de computadora junto con datos de control asociados, procedimientos de uso y procedimientos operativos) para determinar si son aptos para su uso .

Descripción

Las pruebas unitarias son típicamente pruebas automatizadas escritas y ejecutadas por desarrolladores de software para garantizar que una sección de una aplicación (conocida como la "unidad") cumpla con su diseño y se comporte según lo previsto. En la programación de procedimientos , una unidad podría ser un módulo completo, pero más comúnmente es una función o procedimiento individual. En la programación orientada a objetos , una unidad suele ser una interfaz completa, como una clase o un método individual. Al escribir pruebas primero para las unidades comprobables más pequeñas, luego los comportamientos compuestos entre ellas, se pueden construir pruebas integrales para aplicaciones complejas.

Para aislar los problemas que puedan surgir, cada caso de prueba debe probarse de forma independiente. Se pueden utilizar sustitutos como resguardos de métodos , objetos simulados , falsificaciones y arneses de prueba para ayudar a probar un módulo de forma aislada.

Durante el desarrollo, un desarrollador de software puede codificar criterios, o resultados que se sabe que son buenos, en la prueba para verificar la exactitud de la unidad. Durante la ejecución del caso de prueba, los marcos registran las pruebas que fallan en cualquier criterio y las informan en un resumen. Para ello, el enfoque más utilizado es prueba - función - valor esperado.

La escritura y el mantenimiento de pruebas unitarias se pueden agilizar mediante el uso de pruebas parametrizadas . Estos permiten la ejecución de una prueba varias veces con diferentes conjuntos de entrada, lo que reduce la duplicación de códigos de prueba. A diferencia de las pruebas unitarias tradicionales, que generalmente son métodos cerrados y prueban condiciones invariantes, las pruebas parametrizadas toman cualquier conjunto de parámetros. Las pruebas parametrizadas son compatibles con TestNG , JUnit y su contraparte .Net, XUnit . Los parámetros adecuados para las pruebas unitarias se pueden suministrar manualmente o, en algunos casos, el marco de pruebas los genera automáticamente. 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 usando datos de prueba generados en tiempo de ejecución, a diferencia de las pruebas parametrizadas regulares que usan los mismos pasos de ejecución con conjuntos de entrada que están predefinidos.

Ventajas

El objetivo de las pruebas unitarias es aislar cada parte del programa y mostrar que las partes individuales son correctas. Una prueba unitaria proporciona un contrato escrito estricto que el fragmento de código debe cumplir. Como resultado, ofrece varios beneficios.

Las pruebas unitarias encuentran problemas al principio 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, definir de manera más nítida el comportamiento deseado de la unidad. El costo de encontrar un error antes de que comience la codificación 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. 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.

En el desarrollo impulsado por pruebas (TDD), que se usa con frecuencia tanto en programación extrema como en scrum , las pruebas unitarias se crean antes de que se escriba el código en sí. Cuando pasan las pruebas, ese código se considera completo. Las mismas pruebas unitarias se ejecutan contra esa función con frecuencia a medida que se desarrolla la base de código más grande, ya sea a medida que se cambia el código o mediante un proceso automatizado con la compilación. Si las pruebas unitarias fallan, se considera un error en el código modificado o en las pruebas mismas. Luego, las pruebas unitarias permiten rastrear fácilmente la ubicación de la falla o falla. Dado que las pruebas unitarias alertan al equipo de desarrollo del problema antes de entregar el código a los probadores o clientes, los problemas potenciales se detectan al principio del proceso de desarrollo.

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 unidades mismas y pueden usarse en un enfoque de estilo de prueba ascendente . Al probar las partes de un programa primero y luego probar la suma de sus partes, las pruebas de integración se vuelven mucho más fáciles.

Las pruebas unitarias proporcionan una especie de documentación viva del sistema. Los desarrolladores que deseen saber qué funcionalidad proporciona una unidad y cómo usarla, pueden consultar las pruebas de la unidad para obtener una comprensión básica de la interfaz de la unidad ( API ).

Los casos de prueba unitaria incorporan características que son críticas para el éxito de la unidad. Estas características pueden indicar un uso apropiado / inapropiado de una unidad, así como comportamientos negativos que la unidad debe atrapar. Un caso de prueba unitario, en sí mismo, 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.

Cuando el software se desarrolla utilizando un enfoque basado en pruebas, la combinación de escribir la prueba unitaria para especificar la interfaz más las actividades de refactorización realizadas después de que la prueba ha pasado, puede reemplazar el diseño formal. Cada prueba unitaria puede verse como un elemento de diseño que especifica clases, métodos y comportamiento observable.

Limitaciones y desventajas

Las pruebas no detectarán todos los errores del programa, ya que no pueden evaluar todas las rutas de ejecución en los programas más triviales. Este problema es un superconjunto del problema de la detención , que es indecidible . Lo mismo ocurre con las pruebas unitarias. Además, las pruebas unitarias, por definición, solo prueban la funcionalidad de las propias unidades. Por lo tanto, no detectará errores de integración ni errores más amplios a nivel del 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 total de errores. Para garantizar un 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.

Una jerarquía elaborada de pruebas unitarias no equivale a pruebas de integración. La integración con unidades periféricas debe incluirse en las pruebas de integración, pero no en las pruebas unitarias. Las pruebas de integración generalmente todavía dependen en gran medida de las pruebas realizadas por humanos manualmente ; Las pruebas de alto nivel o de alcance global pueden ser difíciles de automatizar, por lo que las pruebas manuales a menudo parecen más rápidas y económicas.

Las pruebas de software son un problema combinatorio. Por ejemplo, cada declaración 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 escrito, los programadores a menudo necesitan de 3 a 5 líneas de código de prueba. Obviamente, esto lleva tiempo y es posible que su inversión no valga la pena. 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, es probable que el código para una prueba unitaria tenga al menos tantos errores como el código que está probando. Fred Brooks en The Mythical Man-Month cita: "Nunca vayas al mar con dos cronómetros; toma uno o tres". Es decir, si dos cronómetros se contradicen, ¿cómo saber cuál es el correcto?

Otro desafío relacionado con la redacción de las 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 establecen correctamente, la prueba no ejercitará el código en un contexto realista, lo que disminuye el valor y la precisión de los resultados de la prueba unitaria.

Para obtener los beneficios esperados de las pruebas unitarias, se necesita una disciplina rigurosa durante todo el proceso de desarrollo de software. Es esencial mantener registros cuidadosos no solo de las pruebas que se han realizado, sino también de todos los cambios que se han realizado en el 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 falla una prueba en particular que había pasado previamente, el software de control de versiones puede proporcionar una lista de los cambios en el código fuente (si los hay) que se han aplicado a la unidad desde ese momento.

También es esencial implementar un proceso sostenible para garantizar que las fallas de los casos de prueba se revisen con regularidad y se aborden de inmediato. Si dicho proceso no se implementa ni se integra en el flujo de trabajo del equipo, la aplicación evolucionará sin sincronizar con el conjunto de pruebas unitarias, aumentando los falsos positivos y reduciendo la eficacia del conjunto de pruebas.

La prueba unitaria del software del sistema integrado presenta un desafío único: debido a que el software se está desarrollando en una plataforma diferente a la que eventualmente se ejecutará, no puede ejecutar fácilmente un programa de prueba en el entorno de implementación real, como es posible con los programas de escritorio.

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 funcione con una base de datos puede requerir la creación de una maqueta de las interacciones de la base de datos, que probablemente no será tan completa como las interacciones reales de la base de datos.

Ejemplo

A continuación, se muestra un conjunto de casos de prueba en Java que especifican una serie de elementos de la implementación. Primero, que debe haber una interfaz llamada Adder y una clase de implementación con un constructor de argumento cero llamado AdderImpl. Continúa afirmando que la interfaz Adder debería tener un método llamado add, con dos parámetros enteros, que devuelve otro entero. También especifica el comportamiento de este método para un pequeño rango de valores en varios métodos de prueba.

import static org.junit.Assert.assertEquals;

import org.junit.Test;

public class TestAdder {

    @Test
    public void testSumPositiveNumbersOneAndOne() {
        Adder adder = new AdderImpl();
        assertEquals(2, adder.add(1, 1));
    }

    // can it add the positive numbers 1 and 2?
    @Test
    public void testSumPositiveNumbersOneAndTwo() {
        Adder adder = new AdderImpl();
        assertEquals(3, adder.add(1, 2));
    }

    // can it add the positive numbers 2 and 2?
    @Test
    public void testSumPositiveNumbersTwoAndTwo() {
        Adder adder = new AdderImpl();
        assertEquals(4, adder.add(2, 2));
    }

    // is zero neutral?
    @Test
    public void testSumZeroNeutral() {
        Adder adder = new AdderImpl();
        assertEquals(0, adder.add(0, 0));
    }

    // can it add the negative numbers -1 and -2?
    @Test
    public void testSumNegativeNumbers() {
        Adder adder = new AdderImpl();
        assertEquals(-3, adder.add(-1, -2));
    }

    // can it add a positive and a negative?
    @Test
    public void testSumPositiveAndNegative() {
        Adder adder = new AdderImpl();
        assertEquals(0, adder.add(-1, 1));
    }

    // how about larger numbers?
    @Test
    public void testSumLargeNumbers() {
        Adder adder = new AdderImpl();
        assertEquals(2222, adder.add(1234, 988));
    }
}

En este caso, las pruebas unitarias, que se escribieron primero, actúan como un documento de diseño que especifica la forma y el comportamiento de una solución deseada, pero no los detalles de implementación, que quedan para el programador. Siguiendo la práctica de "hacer lo más simple que pueda funcionar", a continuación se muestra la solución más sencilla que hará que la prueba pase.

interface Adder {
    int add(int a, int b);
}
class AdderImpl implements Adder {
    public int add(int a, int b) {
        return a + b;
    }
}

Como especificaciones ejecutables

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í mismas) se puede utilizar para verificar la implementación. Las pruebas nunca pasará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 de diagrama 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 (generalmente disponibles como extensiones para IDE ). Las herramientas gratuitas, como las basadas en el marco xUnit , subcontratan a otro sistema la representación gráfica de una vista para consumo humano.

Aplicaciones

Programación extrema

Las pruebas unitarias son la piedra angular de la programación extrema , que se basa en un marco de pruebas unitarias automatizado . Este marco de prueba unitario automatizado puede ser de un tercero, 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 ha implementado 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 en 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 romper", sobre el 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, más una reafirmación de un hecho, ya que los métodos clásicos rara vez se han seguido lo suficientemente metódicamente como para que todas las rutas de ejecución se hayan probado a fondo. La programación extrema simplemente reconoce que las pruebas rara vez son exhaustivas (porque a menudo son demasiado costosas y requieren mucho tiempo para ser económicamente viables) y brinda orientación sobre cómo enfocar de manera efectiva los recursos limitados.

Fundamentalmente, el código de prueba se considera un artefacto de proyecto de primera clase en el sentido de que se mantiene con la misma calidad que el código de implementación, con toda duplicación eliminada. Los desarrolladores lanzan el código de prueba de la unidad al 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 refactorización de código más simple y seguro , integración de código simplificada, 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 . Dado que el diseño emergente depende en gran medida de la refactorización, las pruebas unitarias son un componente integral.

Marcos de pruebas unitarias

Los frameworks de pruebas unitarias son, en la mayoría de los casos, productos de terceros que no se distribuyen como parte del conjunto de compiladores. Ayudan a simplificar el proceso de prueba unitaria, ya que se han desarrollado para una amplia variedad de idiomas .

Por lo general, es posible realizar pruebas unitarias sin el soporte de un marco específico escribiendo código de cliente que ejercita las unidades bajo prueba y usa aserciones , manejo de excepciones u otros mecanismos de flujo de control para señalar fallas. Las pruebas unitarias sin un marco son valiosas porque existe una barrera de entrada para la adopción de pruebas unitarias; tener pocas pruebas unitarias es apenas mejor que no tener ninguna, mientras que una vez que se implementa un marco, agregar pruebas unitarias se vuelve relativamente fácil. En algunos marcos, faltan muchas funciones avanzadas de prueba unitaria o deben codificarse manualmente.

Soporte de prueba de unidad a nivel de idioma

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 en la misma sintaxis que las expresiones booleanas utilizadas en el código de prueba no unitario, como lo que se utiliza para las declaraciones ify while.

Los idiomas con compatibilidad con pruebas unitarias incorporadas incluyen:

Algunos lenguajes sin soporte de prueba de unidad incorporado tienen bibliotecas / marcos de prueba de unidad muy buenos. Esos idiomas incluyen:

Ver también

Referencias

enlaces externos