Tema 32. Técnicas para la verificación, prueba y documentación de programas
1. Índice
2. Vinculación curricular
El contenido de este tema se vincula directamente con los siguientes currículos de la familia profesional de Informática y Comunicaciones:
- En Desarrollo de Aplicaciones Multiplataforma, dentro del módulo Entornos de desarrollo, se exige el alumnado aprende a diseñar planes de prueba unitaria y a aplicar técnicas de caja negra y blanca, permitiéndole entregar software comercial libre de fallos de diseño y de lógica de forma directa y profesional.
- En Desarrollo de Aplicaciones Web, dentro del módulo Entornos de desarrollo, se exige el alumnado adquiere destrezas para automatizar pruebas de regresión y refactorizar código, garantizando la mantenibilidad del software en entornos de producción y despliegue continuo.
- En Administración de Sistemas Informáticos en Red, dentro del módulo Gestión de bases de datos, se exige el alumnado aprende a verificar la consistencia del almacenamiento mediante guiones de prueba, facilitando el mantenimiento preventivo de los sistemas de información corporativos.
3. Introducción
La transición desde la programación artesanal de la década de 1950 hasta la ingeniería del software —disciplina que aplica un enfoque sistemático y cuantificable al desarrollo— sentó las bases teóricas para comprobar el código. La verificación de programas define el proceso de demostrar que el software cumple con las especificaciones del diseño técnico.
Este cambio metodológico resuelve el problema de la detección tardía de fallos. Tradicionalmente, descubrir un error en la fase de explotación o mantenimiento multiplicaba de forma exponencial los costes de reparación. El coste de subsanar un defecto aumenta a medida que el proyecto avanza en su ciclo de vida.
Podemos ver este comportamiento mediante una relación matemática. Si representamos el coste de corrección de un error como y el tiempo transcurrido desde su introducción como , el coste de reparación sigue una función creciente de tipo exponencial:
Donde y representan constantes positivas específicas de la estructura del proyecto informático.
En la formación profesional, el aprendizaje de estas técnicas enseña al alumnado a alejarse del enfoque de ensayo y error. Conviene sustituir la corrección intuitiva por un método estructurado de control de calidad. Este control de calidad asegura que las aplicaciones respeten las especificaciones iniciales del cliente.
El control de calidad comprende el conjunto de técnicas operativas que los programadores usan para verificar los requisitos del producto. Esto incluye la verificación (comprobar si el producto se construye de acuerdo a las especificaciones) y la validación (comprobar si el producto satisface las necesidades reales del usuario). La documentación técnica actúa como registro de estas actividades.
La omisión de estos procesos conlleva consecuencias graves en sistemas reales. En 1962, la sonda espacial Mariner 1 se desvió de su trayectoria y el control de misión la destruyó poco después del lanzamiento. La investigación posterior reveló que la causa fue la ausencia de un guion en el código del sistema de guiado.
El equipo retiró el insecto, lo pegó en el diario de operaciones y anotó que habían realizado la primera "depuración" de la máquina. Desde entonces, el término describe la detección y eliminación de fallos de software.
4. Desarrollo
4.1. Verificación y validación de programas
4.1.1. Concepto de verificación de software
La verificación de software consiste en comprobar que el sistema cumple con las especificaciones de diseño establecidas en las fases previas del desarrollo. Este proceso busca asegurar que el producto se construye de acuerdo con los requisitos técnicos definidos. Es decir, determina si la implementación se ajusta a los modelos y planos conceptuales creados por el equipo de diseño.
Para realizar esta comprobación, el equipo técnico analiza los distintos artefactos que componen el sistema en cada fase del ciclo de vida. Esto incluye la revisión del código fuente, la arquitectura del software y los esquemas de bases de datos. La verificación evalúa si las salidas de una etapa concreta del desarrollo coinciden con las entradas de la etapa inmediatamente anterior.
La verificación emplea métodos estáticos y métodos dinámicos de análisis. Los métodos estáticos evalúan el código o el diseño sin ejecutar el programa, buscando fallos de sintaxis o incumplimiento de estándares de programación. Los métodos dinámicos requieren la ejecución del programa con un conjunto de datos de prueba para observar su comportamiento en tiempo de ejecución.
Podemos ver la verificación como una actividad interna que requiere un conocimiento detallado de la arquitectura del sistema. Los desarrolladores y el equipo técnico de pruebas ejecutan estas tareas para certificar que el código no contiene fallos respecto a su definición formal. El éxito de este proceso reside en detectar desviaciones técnicas de forma temprana antes de integrar el sistema completo.
La verificación consiste en comprobar si la instalación eléctrica cumple con ese requisito escrito. Si el electricista pulsa el interruptor y los focos se encienden de forma independiente, la verificación es positiva. El sistema se ha construido de acuerdo con el diseño técnico.
La validación, en cambio, se realiza cuando los ponentes utilizan la sala. Si estos necesitan un regulador de intensidad lumínica para que el público pueda tomar notas mientras ve la proyección, el sistema fallará la validación. Aunque el interruptor independiente funciona según el plano, el diseño original no satisface la necesidad real de los usuarios en su entorno de trabajo.
4.1.2. Concepto de validación de software
La validación del software es el proceso de evaluar si el producto final satisface las necesidades de uso del usuario final. Este proceso comprueba si la aplicación cumple su propósito en un entorno real de operación. El objetivo principal es asegurar que el cliente obtenga la utilidad esperada de la aplicación durante su explotación cotidiana.
A diferencia de la verificación, la validación se centra en las expectativas de los usuarios y en las reglas de negocio de la organización. Este análisis requiere un conocimiento profundo del dominio de aplicación donde funcionará el software. Por ejemplo, para validar una aplicación de control de vuelo, se requiere la participación de pilotos y expertos en navegación.
Las pruebas de aceptación y las pruebas de usabilidad constituyen los principales mecanismos de validación. Las pruebas de aceptación evalúan si el sistema completo realiza las tareas demandadas por el usuario. Las pruebas de usabilidad miden la facilidad con la que los usuarios interactúan con la interfaz gráfica de usuario.
La validación descubre errores en la especificación de requisitos que la verificación suele pasar por alto. Un sistema puede funcionar perfectamente según los documentos de diseño, pero resultar inútil si esos documentos no reflejaban las verdaderas necesidades del cliente. Por tanto, la validación aporta una visión externa enfocada en la satisfacción del cliente.
4.1.3. Diferencias conceptuales fundamentales
La distinción entre verificación y validación se resume clásicamente mediante dos preguntas formuladas por los autores de referencia en la ingeniería del software:
- Verificación: ¿Estamos construyendo el producto correctamente?
- La validación: ¿Estamos construyendo el producto adecuado?
Conviene recordar esta separación para estructurar los procesos de control de calidad. Si definimos como el conjunto de especificaciones formales de diseño, como la implementación real del programa y como el conjunto de necesidades reales del usuario final, podemos modelar ambos procesos de la siguiente forma:
La verificación asume que las especificaciones de diseño son correctas y estables. Se limita a buscar discrepancias entre el código y los planos de desarrollo. Por el contrario, la validación asume que las especificaciones de diseño pueden contener errores o lagunas que conviene subsanar durante el contacto con el cliente.
| Criterio | Verificación | Validación |
|---|---|---|
| Pregunta de referencia | ¿Construimos el producto correctamente? | ¿Construimos el producto adecuado? |
| Enfoque principal | Cumplimiento de especificaciones de diseño. | Satisfacción de las necesidades del usuario. |
| Conocimiento requerido | Técnico, arquitectura y código fuente. | Dominio de aplicación y flujo de negocio. |
| Actores implicados | Programadores y equipo técnico de pruebas. | Clientes, usuarios finales y expertos del dominio. |
| Fase temporal | Durante todas las fases del ciclo de desarrollo. | Principalmente al final de las fases de desarrollo. |
4.1.4. El modelo en V
El modelo en V es un marco clásico de relación entre las fases de desarrollo y los niveles de prueba correspondientes. Este modelo representa el ciclo de desarrollo en forma de una letra "V", donde la rama izquierda representa la descomposición del sistema y el diseño, y la rama derecha representa la integración y comprobación del sistema.
La rama izquierda se compone de fases descendentes, desde el análisis de requisitos hasta el diseño detallado. La rama derecha se compone de fases ascendentes de pruebas, desde las pruebas unitarias hasta las pruebas de aceptación. Las líneas horizontales que unen ambas ramas representan la correspondencia directa entre los artefactos de diseño y las pruebas asociadas.
El modelo en V demuestra que las actividades de pruebas no se limitan a la fase final de ejecución de código. El diseño de los casos de prueba debe realizarse en paralelo con cada fase de desarrollo. Por ejemplo, el equipo diseña el plan de pruebas de aceptación durante la fase de análisis de requisitos, antes de escribir el código fuente.
Esta planificación anticipada permite identificar fallos de diseño y contradicciones en los requisitos de manera temprana. Corregir un error en la fase de análisis de requisitos es mucho menos costoso que corregirlo durante la fase de implantación final, ya que los fallos no detectados se propagan a través de todas las fases de diseño y codificación del software.
4.1.5. Niveles de prueba en el modelo en V
Cada nivel de prueba en la rama derecha de la "V" tiene objetivos específicos y se apoya en los artefactos generados en su fase homóloga de la rama izquierda.
- Pruebas unitarias: Se centran en verificar de forma aislada el correcto funcionamiento de las unidades de código más pequeñas, como funciones, métodos o clases independientes.
- Pruebas de integración: Evalúan la interacción entre los diferentes módulos y subsistemas cuando se combinan entre sí para formar el sistema completo.
- Pruebas de sistema: Comprueban el comportamiento del sistema completo respecto a la arquitectura global y los requisitos funcionales y extrafuncionales del proyecto.
- Pruebas de aceptación: Validan que el sistema satisfaga los requisitos del cliente y del negocio con la participación de los propios usuarios finales.
4.1.6. Técnicas de verificación estática y dinámica
La verificación se divide tradicionalmente en dos grandes grupos de técnicas: estáticas y dinámicas. Las técnicas estáticas analizan los artefactos del desarrollo sin ejecutar el software. Incluyen las inspecciones de código, las revisiones de diseño y el análisis estático automatizado mediante herramientas especializadas.
Los analizadores estáticos examinan el texto fuente buscando anomalías lógicas, variables no inicializadas o posibles desbordamientos de memoria. El compilador realiza una primera verificación estática mediante el chequeo de tipos obligatorios. El siguiente ejemplo en lenguaje C muestra el uso de una directiva de preprocesado para realizar una verificación dinámica mediante aserciones de control:
#include <assert.h> #include <limits.h> #define MAX_ELEMENTOS 100 void procesar_vector(int tamano) { // Verificación en tiempo de ejecución mediante aserciones assert(tamano > 0 && tamano <= MAX_ELEMENTOS); // Algoritmo de procesamiento del vector de datos }
Las técnicas dinámicas requieren la ejecución del programa con un conjunto de datos de prueba específicos. Cada caso de prueba se compone de unas condiciones de entrada, una secuencia de acciones y un resultado esperado. Estas técnicas permiten medir la cobertura de código y evaluar la tolerancia a fallos ante datos de entrada erróneos.
La complementariedad de ambas técnicas es un hecho constatado en los entornos de desarrollo profesionales. Las revisiones humanas y el análisis estático son muy eficaces para detectar fallos de lógica y variables sin inicializar. Las pruebas dinámicas, por su parte, resultan indispensables para identificar problemas de rendimiento, concurrencia o fugas de memoria activa.
4.1.7. Vinculación con el currículo de formación profesional
El aprendizaje de los conceptos de verificación y validación no solo se inscribe en el marco epistemológico de la ingeniería del software, sino que constituye una competencia técnica del currículo oficial de Formación Profesional.
En particular, esta materia se imparte de forma directa en el módulo profesional de Entornos de Desarrollo. Este módulo pertenece a los títulos de Técnico Superior en Desarrollo de Aplicaciones Multiplataforma (DAM) y Técnico Superior en Desarrollo de Aplicaciones Web (DAW), ambos actualizados de conformidad con la normativa estatal de ordenación de la Formación Profesional.
El Resultado de Aprendizaje 3 (RA3) de este módulo establece de manera explícita que el alumnado debe verificar el funcionamiento de los programas mediante el diseño y la realización de pruebas. Los criterios de evaluación asociados exigen la identificación de los diferentes tipos de pruebas, la definición de casos de prueba específicos y el uso de herramientas de depuración del entorno integrado de desarrollo.
Asimismo, las actividades prácticas promueven la automatización de pruebas unitarias y el uso de dobles de prueba para aislar componentes, tal como se define en la ingeniería del software moderna. De este modo, los conceptos teóricos se trasladan al aula mediante un enfoque práctico y orientado al ejercicio profesional de los futuros desarrolladores de aplicaciones.
4.2. Pruebas de programas: principios básicos
4.2.1. Definición y propósito de las pruebas de software
Una prueba de software consiste en la ejecución de un programa bajo condiciones controladas utilizando datos de entrada seleccionados, con el objetivo de observar su comportamiento y detectar fallos de funcionamiento. Tradicionalmente, se definía este proceso como la demostración de que el código funciona de manera adecuada.
No obstante, los autores clásicos de la ingeniería de software, como Glenford Myers, proponen una visión inversa: probar es el proceso de ejecutar un programa con la intención explícita de encontrar errores. Un caso de prueba es una estructura compuesta por un conjunto de valores de entrada, unas condiciones de ejecución y un resultado esperado. Un oráculo de prueba es el mecanismo que determina si el resultado obtenido coincide con el comportamiento esperado.
Bajo este enfoque, un diseño de pruebas tiene éxito cuando destruye la presunción de corrección del software al provocar un fallo en la ejecución. Si una prueba no expone ninguna discrepancia, no aporta información nueva sobre la presencia de defectos ocultos en el código.
Por el contrario, la confirmación de una anomalía permite guiar los esfuerzos posteriores de depuración, la cual es la tarea de localizar y corregir la causa del fallo. Las pruebas de software, por tanto, actúan como un control de calidad reactivo que mide el comportamiento real del sistema frente a su especificación de comportamiento.
4.2.2. La limitación de las pruebas y la completitud
El principio limitativo de las pruebas de software establece que estas solo pueden demostrar la presencia de fallos, pero nunca su ausencia. Esta máxima, enunciada por Edsger Dijkstra, se fundamenta en la naturaleza del espacio de entrada de cualquier sistema. El espacio de entrada es la totalidad de los datos y secuencias de control que un programa puede recibir. Excepto en programas triviales, este espacio es de tamaño tan grande que resulta inmanejable para su procesamiento.
Consideremos una función matemática simple que recibe tres parámetros numéricos de tipo entero de 32 bits para calcular el área de un triángulo. El número de combinaciones posibles de entrada se calcula mediante la fórmula de la combinación del producto:
Evaluar de manera automatizada cada una de estas opciones requeriría más tiempo del tiempo de vida de la propia infraestructura de cómputo. A esta imposibilidad física se le denomina explosión combinatoria.
Dado que la comprobación completa es impracticable, los diseñadores de pruebas seleccionan un subconjunto representativo de datos de entrada. Para lograrlo, emplean técnicas como la partición de equivalencia, que divide el dominio de entrada en clases donde se asume un comportamiento idéntico del sistema. De este modo, una prueba solo mitiga el riesgo de fallo en producción, pero no garantiza la perfección absoluta del código.
4.2.3. Diferencias entre defecto, error y fallo
Para realizar un control de calidad adecuado, conviene diferenciar con precisión los términos utilizados en la literatura técnica. La comunidad de confiabilidad de sistemas define tres conceptos fundamentales pero distintos: el defecto, el error y el fallo.
Un defecto (también denominado falta o errata) es una imperfección física o lógica estática presente en un artefacto de desarrollo, como el código fuente, debido a un error humano. Un error representa un estado interno incorrecto del sistema que se genera al ejecutar la porción de código que contiene el defecto.
Un fallo es la manifestación externa y observable de un comportamiento anómalo que desvía al sistema de sus requisitos. Un defecto en el código fuente no produce consecuencias de manera inmediata.
Si las condiciones de uso no provocan la ejecución del código defectuoso, el defecto permanece latente. Sin embargo, si el flujo de control del procesador alcanza la línea con la errata, el estado de la memoria del proceso se altera, generando un error. Si este error se transmite a las salidas visibles o a los servicios del sistema, se materializa finalmente el fallo observable por el usuario.
| Concepto | Ubicación | Naturaleza | Manifestación |
|---|---|---|---|
| Defecto (falta) | Estática (código, diseño) | Errata o decisión incorrecta | Invisible hasta la ejecución |
| Error | Dinámica (memoria, estado) | Estado de ejecución alterado | Interna (variables incorrectas) |
| Fallo | Externa (comportamiento) | Desviación de los requisitos | Visible para el usuario |
4.2.4. El modelo de propagación RIPR
La transición de un defecto estático a un fallo visible se describe mediante el modelo RIPR (por sus siglas en inglés: Reachability, Infection, Propagation, Revealability). Este marco conceptual establece que deben cumplirse cuatro condiciones secuenciales para que un evaluador observe un fallo durante la ejecución de una prueba.
La primera condición es la ejecución (alcance): el caso de prueba debe forzar al programa a alcanzar la línea de código exacta que contiene el defecto. Si el flujo de ejecución del procesador no pasa por ese punto, la falta permanece inactiva.
La segunda condición es la infección: una vez alcanzada la instrucción defectuosa, la ejecución debe alterar el estado interno del programa, asignando un valor incorrecto a una variable o alterando el contador de programa.
La tercera condición es la propagación: el estado infectado de los datos debe transferirse a través de las operaciones sucesivas del algoritmo hasta el resultado final de la ejecución. Si una instrucción posterior limpia la variable afectada asignándole un valor válido antes de que se use para producir una salida, el error se extingue y no se propaga.
La cuarta condición es la revelación: el evaluador o el oráculo de prueba debe inspeccionar la porción del estado de salida que ha sido infectada. Si el sistema produce una salida incorrecta pero la prueba solo evalúa parámetros no afectados, el fallo no se revela.
4.2.5. Planificación estratégica de pruebas
La planificación de pruebas es el proceso que establece las directrices, los recursos, las herramientas y la agenda temporal para evaluar un sistema informático. Esta actividad se registra en un documento denominado plan de pruebas, el cual vincula de manera directa los requisitos del sistema con los casos de verificación correspondientes.
La ingeniería de software moderna promueve el diseño de pruebas en las fases tempranas del ciclo de vida, de manera concurrente con la toma de requisitos y el diseño arquitectónico. Este enfoque se representa de manera formal mediante el modelo en V, donde cada fase de desarrollo tiene una fase de pruebas correspondiente.
La creación temprana de pruebas permite detectar contradicciones, ambigüedades y omisiones en las especificaciones antes de codificar una sola instrucción. El coste de corregir una errata identificada durante la fase de análisis de requisitos es mínimo.
Sin embargo, si ese mismo defecto supera el desarrollo y se detecta en la fase de explotación o despliegue, el esfuerzo económico requerido para repararlo se incrementa de manera notable debido a las dependencias de diseño acumuladas. Por tanto, la planificación debe priorizar los esfuerzos de prueba en las etapas iniciales del proyecto.
4.2.6. Criterios de inicio, suspensión y aceptación
Un plan de pruebas estructurado debe definir con precisión los criterios que controlan el paso entre las diferentes actividades de evaluación.
Los criterios de inicio determinan las condiciones mínimas que debe satisfacer el software y el entorno de evaluación antes de comenzar una fase de pruebas. Por ejemplo, para iniciar las pruebas unitarias se exige que el código fuente compile de manera exitosa y que el entorno disponga de los dobles de prueba necesarios para simular las dependencias externas no desarrolladas.
Los criterios de suspensión establecen las condiciones bajo las cuales el equipo de control de calidad debe detener las pruebas y devolver el software al equipo de desarrollo. Esta situación ocurre cuando se detecta un fallo que bloquea la ejecución del sistema o altera la integridad de la base de datos de pruebas.
Los criterios de aceptación definen las condiciones cuantitativas que el sistema debe cumplir para que los usuarios o los clientes autoricen su despliegue y puesta en producción segura. Estos criterios suelen medirse mediante métricas de cobertura de código o tasas aceptables de fallos residuales.
4.2.7. Las metodologías de desarrollo guiado por pruebas
El enfoque de pruebas tempranas desplaza las actividades de evaluación hacia la izquierda en el ciclo de vida del desarrollo, un principio conocido como shift-left. Esta filosofía se manifiesta en metodologías como el desarrollo guiado por pruebas (o TDD, por sus siglas en inglés: Test-Driven Development).
En el proceso TDD, el programador escribe los casos de prueba unitaria antes de implementar la funcionalidad del componente correspondiente. El ciclo de desarrollo de TDD sigue una secuencia repetitiva de tres pasos básicos: escribir una prueba que falle, escribir el código de producción mínimo para que la prueba pase y refactorizar para mejorar la estructura interna del código.
Esta disciplina de desarrollo cambia la percepción de las pruebas, pasando de ser una herramienta de verificación tardía a una técnica de diseño arquitectónico.
Al escribir las pruebas primero, se obliga al programador a definir interfaces limpias, cohesivas y de bajo acoplamiento para facilitar la testabilidad de los módulos. Aunque este enfoque requiere una inversión inicial de tiempo superior a la codificación tradicional, reduce de forma drástica el tiempo dedicado a la depuración y corrección de fallos en las fases avanzadas del proyecto, optimizando el coste total de desarrollo.
4.3. Tipos y técnicas de prueba de software
Las pruebas de software consisten en ejecutar un programa para encontrar fallos en su comportamiento. Para diseñar estas pruebas con eficacia, conviene clasificar los métodos disponibles según el acceso al código fuente y los objetivos específicos del diseño de los casos de prueba.
4.3.1. Tipos de pruebas según su conocimiento del código
La clasificación más extendida divide las pruebas según el nivel de visibilidad de la estructura interna del sistema. Esta distinción permite separar las actividades enfocadas en la especificación de aquellas centradas en la implementación detallada del código fuente.
4.3.2. Pruebas funcionales de caja negra
Las pruebas funcionales, conocidas también como pruebas de caja negra, evalúan el comportamiento externo del sistema. El diseñador de la prueba ignora la estructura interna del código fuente y los detalles de implementación de la aplicación.
El objetivo principal consiste en verificar si el sistema cumple los requisitos especificados en la documentación de diseño o en los casos de uso. El probador considera la aplicación como una entidad cerrada que recibe unas entradas y devuelve unas salidas.
Estas pruebas detectan funciones incorrectas o ausentes, errores en las interfaces, fallos en estructuras de datos externas y problemas de rendimiento. Podemos ver este flujo de transformación de datos mediante un esquema sencillo.
El proceso de diseño se centra en encontrar de forma directa discrepancias entre la salida real y la salida esperada. Al no depender de la codificación interna, podemos planificar y diseñar estas pruebas antes de iniciar la escritura del código fuente.
4.3.3. Pruebas estructurales de caja blanca
Las pruebas estructurales, también llamadas pruebas de caja blanca o de caja de cristal, evalúan la estructura lógica interna del programa. El diseñador de la prueba requiere acceso completo al código fuente para examinar sus componentes de forma interna.
Este enfoque busca recorrer y evaluar los caminos lógicos de ejecución, los flujos de control y las condiciones lógicas internas del código. El probador diseña casos de prueba para garantizar que se ejecutan líneas específicas de código o condiciones lógicas.
Las métricas habituales en estas pruebas incluyen el cubrimiento de sentencias, que evalúa el porcentaje de líneas ejecutadas, y el cubrimiento de decisiones, que comprueba cada opción de una bifurcación condicional. Podemos ilustrar el control de flujo con el siguiente fragmento en lenguaje C.
int evaluar_descuento(int edad, int socio) { int descuento = 0; if (edad > 65 && socio == 1) { descuento = 20; } else { descuento = 5; } return descuento; }
Para lograr un cubrimiento de decisiones en este fragmento, el programador debe generar casos de prueba que fuercen tanto el camino verdadero como el falso del condicional. Vemos la estructura lógica correspondiente en el siguiente diagrama de flujo de control.
Estas pruebas estructurales revelan variables no inicializadas, caminos de código inalcanzables, bifurcaciones incorrectas y errores de asignación en cálculos matemáticos internos.
4.3.4. Comparación de enfoques de prueba
Ambos enfoques presentan ventajas y desventajas que los hacen complementarios en un plan de verificación. Mientras la caja negra destaca en la validación de requisitos de usuario, la caja blanca ayuda a asegurar la limpieza y corrección técnica del código.
La siguiente tabla sintetiza las principales diferencias técnicas entre ambos tipos de prueba de software.
| Característica | Pruebas de caja negra | Pruebas de caja blanca |
|---|---|---|
| Base del diseño | Requisitos y especificaciones | Código fuente y flujo lógico |
| Conocimiento del código | Ninguno | Completo y detallado |
| Personal habitual | Probadores independientes y usuarios | Desarrolladores de software |
| Momento de diseño | Temprano, durante el análisis | Tardío, durante la codificación |
| Objetivo principal | Validar "qué" hace el sistema | Verificar "cómo" está construido |
Conviene recordar que el uso exclusivo de un solo enfoque deja huecos de calidad en el producto final. Los autores clásicos como Glenford Myers recomiendan combinar ambas técnicas para maximizar la detección de fallos de forma óptima.
4.3.5. Técnicas de diseño de pruebas de caja negra
Debido a que el espacio de entrada de un programa suele ser inmenso, resulta imposible probar todas las combinaciones posibles de datos. Este fenómeno se conoce como explosión combinatoria de los casos de prueba.
Para evitar este problema de coste y tiempo, los ingenieros de software utilizan técnicas matemáticas para seleccionar un subconjunto idóneo de datos. Las dos técnicas clásicas más utilizadas son la partición de equivalencia y el análisis de valores límite.
4.3.6. Partición de equivalencia
La partición de equivalencia es una técnica que divide el dominio de entrada de un programa en clases de datos llamadas clases de equivalencia o bloques. Con esta división, asumimos que el programa se comporta de forma similar para cualquier valor dentro de una misma clase.
Si un caso de prueba de una clase detecta un fallo, los demás valores de esa misma clase también lo detectarán. Por el contrario, si el caso de prueba no detecta fallos, asumimos que ningún otro valor de la clase los encontrará.
Para que una partición del dominio de entrada sea matemáticamente válida, debe cumplir dos propiedades geométricas de las clases : la completitud y la exclusión mutua. La unión de todas las clases debe cubrir el dominio entero:
Además, las clases deben ser disjuntas entre sí, lo que significa que ningún valor puede pertenecer a más de una clase simultáneamente:
Conviene ver un ejemplo práctico. Supongamos una función en C que procesa la edad de registro de un conductor, donde la edad permitida debe estar en el rango de 18 a 99 años inclusive.
int registrar_conductor(int edad) { if (edad >= 18 && edad <= 99) { return 1; // Registro aceptado } return 0; // Registro rechazado }
A partir de esta especificación, podemos construir las clases de equivalencia de entrada válidas e inválidas, como muestra la siguiente tabla de planificación.
| Condición de entrada | Clase de equivalencia válida | Clase de equivalencia inválida |
|---|---|---|
| Rango de edad | (Clase 1) | (Clase 2) (Clase 3) |
Para cubrir estas clases de forma económica, el probador selecciona un único valor representativo de cada bloque. Por ejemplo, elegimos los valores de prueba: (válido), (inválido por debajo) y (inválido por encima).
4.3.7. Análisis de valores límite
El análisis de valores límite es una técnica de diseño que complementa la partición de equivalencia mediante la selección de casos de prueba situados en los extremos de las clases de equivalencia. La experiencia demuestra que la mayor densidad de fallos se concentra en las fronteras de los rangos de entrada.
Los programadores suelen cometer errores de "desfase en uno" u off-by-one, como escribir un operador mayor estricto > cuando se requiere un operador mayor o igual >=. Estos descuidos lógicos se detectan de forma directa al forzar la evaluación en las fronteras de los bloques.
Para cada límite de una clase de equivalencia, conviene diseñar casos de prueba con tres valores específicos: el valor del límite exacto, un valor inmediatamente inferior y un valor inmediatamente superior.
Si retomamos el ejemplo del registro de conductores con el rango , los límites lógicos se sitúan en los valores y . La siguiente tabla muestra los casos de prueba necesarios para realizar un análisis de valores límite completo de estas fronteras.
| Límite evaluado | Caso de prueba (Edad) | Comportamiento esperado | Tipo de entrada |
|---|---|---|---|
| Frontera inferior | 17 | Rechazo de registro | Inválido |
| Frontera inferior | 18 | Aceptación de registro | Válido (Límite) |
| Frontera inferior | 19 | Aceptación de registro | Válido |
| Frontera superior | 98 | Aceptación de registro | Válido |
| Frontera superior | 99 | Aceptación de registro | Válido (Límite) |
| Frontera superior | 100 | Rechazo de registro | Inválido |
Conviene recordar que esta técnica también se aplica a los rangos de las variables de salida. Si un programa de facturación debe generar un informe detallado cuando el importe total supera los euros, el probador debe buscar entradas que fuercen al sistema a calcular importes de , y euros para comprobar que la lógica de salida funciona de forma adecuada.
4.4. Estrategias de integración de pruebas
4.4.1. Enfoques de integración de componentes
La integración de software consiste en ensamblar los diferentes módulos programados de manera aislada para verificar que interactúan de forma correcta. Un módulo es una porción de código independiente que realiza una tarea específica y que se comunica con otros mediante interfaces definidas.
Durante el desarrollo, los programadores suelen probar cada módulo individualmente para asegurar su funcionamiento interno. Sin embargo, el ensamblado de estas partes suele revelar fallos en la comunicación, inconsistencias en los datos o supuestos incorrectos sobre el comportamiento de otros componentes.
Existen dos aproximaciones metodológicas para llevar a cabo este proceso: el enfoque no incremental y el enfoque incremental. El diseño de la estrategia define el orden en el que se combinan los módulos y el tipo de código de soporte necesario para realizar las verificaciones.
El número de combinaciones de interfaces posibles entre módulos se puede aproximar mediante el análisis combinatorio. Si tenemos un sistema con módulos interconectados, el número potencial de acoplamientos de comunicación se modela mediante la siguiente expresión:
Este modelo muestra cómo la complejidad del ensamblado crece de forma cuadrática al añadir nuevos componentes. Para un sistema con solo cinco módulos, el número de interfaces potenciales es de 10. Si el sistema escala a diez módulos, el número de interacciones posibles asciende a 45. Esta progresión combinatoria justifica la necesidad de adoptar enfoques estructurados y ordenados para el diseño del plan de pruebas, evitando realizar conexiones de forma arbitraria o sin una planificación previa.
4.4.2. Integración no incremental o de gran explosión
La integración no incremental, conocida técnicamente como integración de gran explosión o big-bang, consiste en conservar todos los módulos aislados hasta que el desarrollo de cada uno de ellos está completamente terminado. En ese momento, el equipo de desarrollo reúne todos los componentes del sistema y los ejecuta conjuntamente en una única sesión de pruebas global. Este enfoque prescinde de fases intermedias de acoplamiento progresivo.
Aunque este método reduce el tiempo dedicado a escribir código temporal de soporte, presenta dificultades severas cuando aparecen fallos. Al ejecutar toda la aplicación simultáneamente, resulta sumamente complejo rastrear el origen de un comportamiento erróneo. Los defectos pueden encontrarse en cualquiera de los módulos individuales o en las interacciones complejas entre varios de ellos. Esto suele provocar sesiones prolongadas de depuración y un aumento de los costes de corrección.
| Aspecto | Integración no incremental (big-bang) | Integración incremental |
|---|---|---|
| Momento de inicio | Al finalizar todo el desarrollo | Progresivo durante el desarrollo |
| Facilidad de depuración | Baja, el fallo puede estar en cualquier punto | Alta, el fallo suele estar en la última pieza |
| Código de soporte necesario | Mínimo | Medio o alto (drivers y stubs) |
| Visibilidad del progreso | Tardía, solo al final del proyecto | Temprana, mediante entregas funcionales |
| Detección de fallos de diseño | Muy tardía | Temprana |
Este enfoque resulta tentador para equipos pequeños debido a su aparente simplicidad inicial. Sin embargo, el esfuerzo de depuración al final del proyecto suele superar con creces el tiempo que se ahorra al no escribir andamiajes de prueba. La acumulación de múltiples errores simultáneos puede enmascarar fallos de diseño que obliguen a reescribir porciones significativas de la aplicación, retrasando la entrega del software de manera indefinida.
4.4.3. Integración incremental descendente
La integración incremental propone el ensamblado y la verificación del sistema en pasos pequeños y controlados. La integración incremental descendente o top-down inicia el proceso desde el módulo superior de la jerarquía de control, que suele contener la función principal del programa. Desde este nodo raíz, el sistema se va completando hacia abajo, incorporando los módulos de niveles inferiores de forma progresiva.
Dado que los componentes de bajo nivel todavía no se han desarrollado, este enfoque requiere el uso de dobles de prueba pasivos o stubs. Un stub es un componente simulado que imita el comportamiento de un módulo real devolviendo valores fijos o respuestas predeterminadas. El módulo superior invoca al stub como si fuera el componente definitivo, lo que permite verificar la lógica de control y el flujo de llamadas de alto nivel de manera prematura.
Podemos ilustrar el flujo de control en una prueba descendente en lenguaje C. Supongamos que tenemos una función de control que calcula el salario neto llamando a un módulo de persistencia de base de datos que todavía no está implementado. El programador sustituye el módulo real por un doble de prueba pasivo:
#include <stdio.h> // Stub que simula el acceso a la base de datos float obtener_salario_base_stub(int empleado_id) { // Devuelve un valor fijo predeterminado para las pruebas if (empleado_id == 101) { return 1500.00f; } return 1000.00f; } // Módulo de control bajo prueba float calcular_salario_neto(int empleado_id, float retencion) { float salario_base = obtener_salario_base_stub(empleado_id); return salario_base * (1.0f - retencion); } int main() { int emp_id = 101; float ret = 0.15f; // 15% de retención float neto = calcular_salario_neto(emp_id, ret); printf("Empleado: %d, Neto: %.2f\n", emp_id, neto); return 0; }
La ventaja de este enfoque es la obtención temprana de un esqueleto funcional de la aplicación. Esto permite mostrar el progreso del proyecto a los usuarios de manera rápida y evaluar problemas de usabilidad o de flujo de control. Sin embargo, presenta el inconveniente de que las interfaces de bajo nivel, que suelen realizar las operaciones de entrada y salida más complejas y costosas, se prueban al final del desarrollo.
4.4.4. Integración incremental ascendente
La integración incremental ascendente o bottom-up realiza el ensamblado en el sentido inverso. El proceso comienza con la verificación de los módulos situados en el nivel más bajo de la jerarquía de llamadas, es decir, aquellos que no dependen de ningún otro componente. Una vez probados, se integran con los módulos del nivel inmediatamente superior que los invocan.
Para ejecutar los módulos de bajo nivel de forma aislada, es necesario utilizar controladores de prueba o drivers. Un driver es un programa auxiliar diseñado específicamente para invocar al componente bajo prueba, suministrarle parámetros de entrada y mostrar los resultados obtenidos para su verificación. A medida que se desarrollan los módulos de niveles superiores, estos reemplazan progresivamente a los controladores de prueba auxiliares.
graph TD
subgraph Integración Descendente (Top-Down)
A[Módulo Principal] --> B[Stub B]
A --> C[Stub C]
style B stroke-dasharray: 5 5
style C stroke-dasharray: 5 5
end
subgraph Integración Ascendente (Bottom-Up)
D[Driver Auxiliar] --> E[Módulo Operativo]
D --> F[Módulo Operativo]
style D stroke-dasharray: 5 5
end
Este enfoque facilita la detección de fallos en el manejo de recursos físicos y periféricos de forma temprana, ya que estos módulos se ubican en la base del sistema. Escribir controladores de prueba suele ser más sencillo que programar dobles de prueba pasivos complejos, puesto que el flujo de datos es directo. Su principal desventaja es la ausencia de una versión funcional global de la aplicación hasta que se alcanza el nodo superior de la jerarquía.
4.4.5. Pruebas unitarias como base organizativa
El plan de pruebas de una organización se estructura en niveles bien definidos que corresponden a distintas etapas del ciclo de vida del desarrollo de software. Las pruebas unitarias constituyen el primer nivel de este plan y se centran en la verificación aislada de la unidad de código más pequeña posible. En lenguajes estructurados como C, esta unidad es la función; en lenguajes orientados a objetos, corresponde al método o a la clase.
El objetivo de las pruebas unitarias es garantizar que cada pieza de código cumple con su especificación de manera independiente. El programador escribe estos casos de prueba antes o durante la fase de codificación. Para mantener el aislamiento, las pruebas unitarias deben evitar accesos a bases de datos, sistemas de archivos o conexiones de red reales. Cualquier dependencia externa se gestiona mediante dobles de prueba para asegurar que la prueba se ejecuta de forma rápida y determinista.
Las pruebas unitarias reducen el coste de corrección de fallos, ya que localizan los defectos de forma inmediata tras escribir el código. Al verificar el comportamiento de cada función ante valores límite y entradas no válidas, se construye una base firme que facilita los niveles de integración posteriores. Este nivel organizativo suele ser responsabilidad directa del equipo de programación, que utiliza marcos de pruebas específicos para automatizar su ejecución.
4.4.6. Pruebas de regresión y control de cambios
Las pruebas de regresión consisten en la ejecución repetida de un conjunto de pruebas ya existentes sobre un sistema de software que ha sido modificado. Cuando un programador corrige un defecto o añade una nueva característica, existe la posibilidad de introducir errores accidentales en partes del código que antes funcionaban de manera correcta. Las pruebas de regresión mitigan este riesgo al asegurar que las modificaciones no han provocado un retroceso en la calidad global del sistema.
Para que esta estrategia sea viable en proyectos de tamaño medio o grande, resulta obligatorio automatizar la ejecución de las pruebas. Los sistemas de integración continua ejecutan la batería de pruebas de regresión de forma automática cada vez que se registra un cambio en el repositorio de control de versiones. Esto proporciona una retroalimentación rápida a los desarrolladores y evita que los fallos pasen desapercibidos a fases posteriores.
Las pruebas de regresión no representan un nivel de pruebas adicional, sino un proceso transversal que se aplica a todos los niveles existentes. Se pueden realizar pruebas de regresión unitarias, de integración o de sistema. Mantener un conjunto actualizado de pruebas de regresión permite realizar refactorizaciones de código con seguridad, facilitando la evolución del diseño del software sin temor a romper funcionalidades consolidadas.
4.4.7. Pruebas de sistema y de aceptación final
Una vez completada la integración de todos los módulos, el software entra en la fase de pruebas de sistema.
Estas pruebas evalúan el comportamiento de la aplicación de manera global para comprobar que cumple con los requisitos del diseño de la arquitectura. A diferencia de los niveles anteriores, las pruebas de sistema se enfocan en comprobar aspectos no funcionales, tales como el rendimiento del programa bajo carga, el uso de memoria o el comportamiento del software en su entorno físico real.
El último nivel organizativo corresponde a las pruebas de aceptación. Estas pruebas validan el producto final con respecto a las necesidades reales del usuario y los requisitos del negocio. En esta fase, los usuarios finales o clientes ejecutan el sistema utilizando datos reales para validar si la aplicación realiza las tareas de la forma esperada. Un resultado satisfactorio en las pruebas de aceptación representa la conformidad formal para el despliegue final de la aplicación en producción.
Las pruebas de aceptación actúan como el oráculo final de validación del proyecto. Mientras que los niveles anteriores comprueban si el software se ha construido correctamente según las especificaciones técnicas, la aceptación verifica si se ha construido el software adecuado para satisfacer las necesidades operativas de la organización. Ambos niveles completan el ciclo de verificación y validación indispensable para asegurar la calidad del producto final.
4.5. Proceso de prueba y diseño de casos
El proceso de prueba de software comprende un conjunto estructurado de actividades dirigidas a evaluar la calidad de un sistema y detectar la presencia de fallos en su comportamiento. Una prueba consiste en la ejecución metódica de un componente para contrastar su funcionamiento real con los requerimientos acordados. Conviene recordar que las pruebas no demuestran la ausencia de errores, sino su presencia en el sistema.
4.5.1. Fases del proceso de prueba de software
La fase de planificación constituye la etapa de base donde se define el alcance general de las actividades de verificación y validación de software. Durante este período, el equipo técnico analiza los requerimientos funcionales del sistema para identificar y priorizar los riesgos potenciales asociados al desarrollo del software. Un riesgo de software representa la probabilidad de que ocurra un fallo que afecte de manera negativa al usuario o al funcionamiento del negocio.
Para seleccionar las prioridades de prueba, los analistas estiman la exposición al riesgo () mediante un análisis de dos dimensiones que asocia la probabilidad de ocurrencia () con el impacto del fallo ():
La probabilidad de ocurrencia indica la frecuencia con la que un defecto puede activarse durante la operación normal del software. El impacto cuantifica las consecuencias económicas, operativas o de seguridad que sufriría el entorno en caso de producirse dicho fallo. El cálculo de la exposición permite ordenar los elementos del sistema para asignar los recursos de prueba a las áreas con mayor probabilidad de fallo.
Por ejemplo, si un error en la pasarela de pago presenta un impacto de valor y una probabilidad de , la exposición al riesgo es de . En cambio, si un defecto cosmético en la interfaz visual tiene un impacto de y una probabilidad de , su exposición es de . El equipo de control de calidad asigna mayor prioridad de prueba al primer escenario debido a su mayor exposición estimada.
La fase de ejecución se centra en la aplicación de las pruebas dinámicas. Una prueba dinámica es aquella que requiere la ejecución del código fuente del software utilizando un conjunto de datos simulados para observar el comportamiento dinámico del sistema. Durante la ejecución, el sistema operativo o el entorno de ejecución procesa los estímulos de entrada y genera respuestas que el equipo técnico debe registrar de manera sistemática.
El registro sistemático consiste en la documentación estructurada de cada discrepancia identificada entre el comportamiento observado del software y el comportamiento esperado que define la especificación de requerimientos. El equipo de control de calidad documenta cada anomalía en un registro de defectos que facilita la posterior depuración y corrección del código por parte de los desarrolladores.
| Campo de registro | Tipo de dato | Descripción del elemento |
|---|---|---|
| Identificador de defecto | Alfanumérico | Código único que identifica la anomalía registrada. |
| Fecha de detección | Fecha | Día y hora en que el probador detectó el fallo. |
| Prioridad de corrección | Entero | Escala que indica la urgencia de la resolución del error. |
| Pasos de reproducción | Texto | Secuencia detallada de acciones para activar el fallo. |
| Resultado esperado | Texto | Respuesta que debería producir el sistema según la especificación. |
| Resultado obtenido | Texto | Respuesta real o error que arrojó el sistema durante la prueba. |
| Estado del reporte | Enum | Situación del defecto: Registrado, En proceso, Corregido, Verificado. |
La comparación entre los comportamientos reales y los esperados se apoya en el concepto de oráculo de prueba. Un oráculo de prueba es un mecanismo que permite determinar si el software ha superado con éxito un caso de prueba mediante la comparación de la salida obtenida frente a los resultados esperados. Si los valores obtenidos difieren de los previstos por la especificación, el probador registra un fallo.
La fase de evaluación representa la etapa de cierre de la batería de pruebas y se inicia cuando la ejecución ha completado su planificación temporal o ha consumido los recursos asignados. En esta fase, los responsables del aseguramiento de la calidad evalúan los criterios de salida. Un criterio de salida es una condición de carácter cuantitativo que determina si el proceso de prueba ha alcanzado un nivel de cobertura satisfactorio para finalizar las actividades.
Los criterios de salida se definen habitualmente mediante métricas de cobertura de código, como el porcentaje de instrucciones ejecutadas o el porcentaje de ramas lógicas recorridas durante la ejecución de las pruebas. Si el sistema alcanza el umbral de cobertura acordado y no se identifican defectos pendientes de alta prioridad, el proceso se da por finalizado y se procede a la redacción del informe final de pruebas.
El informe final de pruebas recopila de forma detallada las actividades desarrolladas, la cantidad de fallos encontrados clasificados por su nivel de gravedad, los errores que han sido corregidos y el estado general de calidad del producto de software. Este documento sirve de apoyo para que la dirección del proyecto tome la decisión de autorizar la entrega del software o suspender su despliegue hasta resolver las anomalías identificadas.
4.5.2. Fases del diseño de casos de prueba
El diseño de casos de prueba es el proceso sistemático de traducir las especificaciones de requerimientos y modelos de diseño en escenarios de prueba ejecutables. Un caso de prueba es una estructura compuesta por un conjunto de valores de entrada, precondiciones de ejecución, una secuencia de pasos operativos y un resultado esperado, diseñados con el fin de verificar un requerimiento o revelar un fallo del software.
La primera fase del diseño de casos de prueba corresponde a la identificación y caracterización del escenario de prueba. Esta actividad se fundamenta en el análisis de los diagramas de casos de uso y en las especificaciones funcionales que describen el comportamiento esperado del sistema ante las interacciones del usuario. Un diagrama de casos de uso es un modelo gráfico de UML que representa a los actores externos y sus objetivos funcionales al interactuar con el sistema.
Un escenario de prueba representa un camino específico de ejecución a través de un caso de uso. Podemos ver que un caso de uso con flujos alternativos o excepciones dará origen a múltiples escenarios de prueba. La caracterización consiste en describir cada escenario de manera precisa, identificando el flujo normal de éxito y los flujos excepcionales de error que el software debe controlar de forma segura.
La definición formal representa la segunda etapa del diseño, donde cada escenario caracterizado se convierte en una especificación técnica detallada y ejecutable. En esta fase, los ingenieros de pruebas aplican técnicas de diseño como la partición de equivalencia y el análisis de valores límite para determinar las condiciones de entrada de datos que maximicen la detección de defectos con el menor número posible de casos de prueba.
La especificación formal de un caso de prueba requiere la definición de tres elementos del patrón Arrange-Act-Assert (organizar, actuar y verificar):
- Condiciones de entrada y precondiciones: Representan los valores de datos que el software procesará y el estado inicial en el que debe encontrarse el entorno antes de iniciar la prueba.
- Procedimiento de ejecución paso a paso: Describe la secuencia exacta de acciones y comandos que el probador o la herramienta de automatización deben ejecutar sobre la interfaz o el componente bajo prueba.
- Especificación del resultado esperado: Define de manera inequívoca la respuesta esperada del software, incluyendo salidas visuales, modificaciones en bases de datos o excepciones lógicas que el sistema debe lanzar.
import unittest class TestProcesadorDePagos(unittest.TestCase): def test_pago_con_saldo_suficiente(self): # 1. Arrange: Configuración de condiciones de entrada y precondiciones saldo_inicial = 150.0 monto_transaccion = 50.0 cuenta = CuentaBancaria(saldo=saldo_inicial) # 2. Act: Ejecución del procedimiento paso a paso resultado_operacion = cuenta.procesar_debito(monto_transaccion) # 3. Assert: Verificación frente al resultado esperado (oráculo) self.assertTrue(resultado_operacion) self.assertEqual(cuenta.obtener_saldo(), 100.0)
La última etapa del diseño corresponde a la trazabilidad de los casos de prueba. Esta actividad garantiza que todos los requerimientos especificados por el usuario cuenten con al menos un caso de prueba diseñado para verificar su cumplimiento en el software entregado. La trazabilidad se documenta mediante una matriz de trazabilidad, que consiste en una tabla bidimensional que vincula de manera directa cada caso de prueba con su requerimiento de origen.
La matriz de trazabilidad permite evaluar la cobertura de las pruebas y facilita la gestión de cambios durante el ciclo de vida del desarrollo. Si un requerimiento es modificado por el usuario, el equipo técnico utiliza la matriz para identificar de inmediato qué casos de prueba se ven afectados y deben ser actualizados o reejecutados. Esta práctica es un soporte importante para las pruebas de regresión, asegurando que los cambios no introduzcan fallos en funciones previamente verificadas.
| Código de requerimiento | Descripción del requerimiento | Código de caso de prueba | Escenario de prueba | Resultado esperado |
|---|---|---|---|---|
| REQ-01 | Procesamiento de pagos con saldo suficiente | TC-01-A | Transacción exitosa de débito | Confirmación del pago y saldo final actualizado. |
| REQ-01 | Procesamiento de pagos con saldo suficiente | TC-01-B | Intento de pago con saldo de cero | Rechazo del pago y alerta de saldo insuficiente. |
| REQ-02 | Registro de nuevos usuarios en el sistema | TC-02-A | Registro con correo electrónico válido | Cuenta creada y envío de correo de confirmación. |
| REQ-02 | Registro de nuevos usuarios en el sistema | TC-02-B | Registro con formato de correo inválido | Mensaje de error en pantalla y rechazo del registro. |
Podemos ver que la matriz de trazabilidad también permite detectar casos de prueba redundantes que no aportan valor al proceso de verificación.
Al organizar los casos de esta manera, se garantiza una cobertura óptima de los requerimientos de usuario sin incurrir en una duplicación de esfuerzos de ejecución. El equipo de control de calidad mantiene este documento actualizado como evidencia formal de que el sistema cumple con todas las especificaciones de comportamiento acordadas en las fases previas del ciclo de vida del desarrollo.
En el contexto de la formación profesional, conviene recordar que la trazabilidad y el diseño de casos de prueba constituyen resultados de aprendizaje establecidos en los currículos oficiales de los ciclos formativos de grado superior, tales como el de Administración de Sistemas Informáticos en Red o Desarrollo de Aplicaciones Multiplataforma. El dominio de estas técnicas proporciona los fundamentos conceptuales necesarios para asegurar la calidad de las soluciones informáticas implantadas en el entorno empresarial.
4.6. Documentación de programas
La documentación de programas constituye el conjunto de descripciones, explicaciones y registros visuales que definen la estructura, el funcionamiento y el despliegue de un sistema informático. Este soporte facilita la comprensión de la lógica interna para futuros desarrollos y garantiza un uso correcto de las aplicaciones. Conviene recordar que la documentación se divide en dos grandes familias: la interna, integrada en el código fuente, y la externa, organizada en manuales independientes.
4.6.1. Documentación interna y autodocumentación
La documentación interna reside en el código fuente, que es el conjunto de instrucciones escritas en un lenguaje de programación. Su propósito principal es guiar a los desarrolladores en la modificación y corrección del programa. La calidad de este registro depende directamente del estilo de escritura y la estructuración del código.
Directrices de nombrado, estructura e indentación
El nombrado de variables es el proceso de asignar un identificador (nombre descriptivo) a un espacio de almacenamiento de memoria. Un buen identificador debe representar fielmente el dato que contiene.
Las directrices actuales sugieren evitar nombres genéricos como o , optando por términos semánticos específicos. El ámbito de una variable, que define el espacio del programa donde es accesible, influye en la longitud del identificador; las variables locales requieren nombres cortos, mientras que las de ámbito global o de clase demandan nombres más largos y descriptivos.
La estructuración de clases organiza los métodos y atributos de un objeto siguiendo una jerarquía coherente. Conviene colocar los componentes públicos en la parte superior para definir la interfaz accesible, relegando los elementos privados de implementación a la parte inferior. Este ordenamiento visual preserva la encapsulación, que es el principio de ocultar los detalles internos de un componente para reducir el acoplamiento entre módulos de software.
La indentación consiste en insertar espacios en blanco al inicio de una línea para reflejar la jerarquía lógica del código. Las sentencias subordinadas a un ciclo o a una condición se desplazan hacia la derecha, usualmente cuatro espacios. La indentación permite a los desarrolladores comprender la estructura lógica de un bloque de un solo vistazo, reduciendo la fatiga mental durante la lectura.
Un indicador de legibilidad común es el índice de complejidad de McCabe, conocido como complejidad ciclomática (), que calcula el número de caminos independientes a través del código mediante la fórmula:
donde representa el número de aristas o enlaces del diagrama de flujo de control, el número de nodos y el número de componentes conexos.
Principios de autodocumentación
La autodocumentación define el diseño de un código que resulta comprensible por sí mismo, reduciendo la necesidad de añadir comentarios explicativos. El código debe leerse como prosa bien estructurada. Para lograr esto, podemos descomponer funciones complejas en subrutinas pequeñas y especializadas, cuyo nombre declare explícitamente su objetivo.
La sustitución de números mágicos (valores numéricos sin significado explícito en el código) por constantes nombradas aclara el flujo de ejecución. Asimismo, la extracción de expresiones lógicas complejas en funciones de tipo predicado (métodos que devuelven un valor booleano) elimina la ambigüedad de las estructuras condicionales.
El siguiente ejemplo muestra la diferencia entre un código que requiere comentarios para ser entendido y un código autodocumentado:
## Código con comentarios redundantes (no recomendado) def calc(x, y): # s es el sueldo base multiplicando horas por la tarifa estándar de 30 s = x * 30 # si los días de ausencia son mayores a dos, se aplica penalización if y > 2: d = s * 0.1 else: d = 0 return s - d
## Código autodocumentado (recomendado) TARIFA_HORA_ESTANDAR = 30.0 PORCENTAJE_PENALIZACION = 0.10 LIMITE_DIAS_AUSENCIA = 2 def calcular_salario_neto(horas_trabajadas, dias_ausencia): sueldo_base = horas_trabajadas * TARIFA_HORA_ESTANDAR descuento = calcular_descuento_ausencia(sueldo_base, dias_ausencia) return sueldo_base - descuento def calcular_descuento_ausencia(sueldo_base, dias_ausencia): if dias_ausencia > LIMITE_DIAS_AUSENCIA: return sueldo_base * PORCENTAJE_PENALIZACION return 0.0
Comentarios técnicos integrados
Los comentarios técnicos integrados son anotaciones estructuradas dentro del código fuente diseñadas para ser procesadas por herramientas automáticas como Javadoc o Doxygen. Estos programas analizan las etiquetas específicas de los comentarios y generan automáticamente páginas de ayuda que describen la API (Application Programming Interface), que es la interfaz que define cómo interactuar con el componente de software.
Estas anotaciones describen la firma de las funciones, detallando los parámetros de entrada mediante la etiqueta @param, los valores de salida con @return y las condiciones de error o excepciones lanzadas a través de @throws.
/** * Calcula el cociente entre dos números reales. * @param dividendo El número real que será dividido. * @param divisor El número real por el cual se divide. * @return El cociente resultante de la división. * @throws ArithmeticException Si el divisor es equivalente a cero. */ public double dividir(double dividendo, double divisor) throws ArithmeticException { if (divisor == 0) { throw new ArithmeticException("División por cero no permitida"); } return dividendo / divisor; }
4.6.2. Documentación externa del software
La documentación externa comprende los documentos que se desarrollan fuera del entorno del código fuente. Su objetivo es dar soporte a los diferentes perfiles que interactúan con el sistema, incluyendo usuarios finales, administradores de sistemas y técnicos de soporte.
El manual de usuario
El manual de usuario describe el funcionamiento general de la aplicación de manera accesible. Está dirigido a personas sin formación técnica especializada, por lo que evita jergas complejas. Este documento detalla las funciones principales del sistema mediante tutoriales paso a paso y pantallas explicativas.
Un manual adecuado debe incluir un índice temático, una sección de inicio rápido para tareas comunes, una descripción detallada de la interfaz gráfica y un listado de preguntas frecuentes. El enfoque debe centrarse en los objetivos del usuario y no en la arquitectura interna del sistema.
Guía de instalación y configuración
La guía de instalación está orientada a los administradores de sistemas responsables del despliegue del software en entornos de producción. Especifica detalladamente los requisitos previos de hardware (procesador, memoria RAM mínima y espacio de almacenamiento en disco) y las dependencias de software (sistema operativo compatible, librerías, intérpretes de comandos y servidores de aplicaciones).
El documento detalla de manera secuencial los comandos de instalación, la configuración de variables de entorno y los puertos de red que la aplicación utilizará para comunicarse. También incluye pruebas de verificación para confirmar que el despliegue se ha realizado con éxito.
Manual de mantenimiento y administración
El manual de mantenimiento se dirige al personal técnico encargado de velar por la estabilidad operativa y la evolución del sistema a largo plazo. Este documento detalla la arquitectura técnica global, incluyendo la estructura de persistencia de datos, que es la forma en que el sistema conserva la información en soportes de almacenamiento no volátiles.
Un componente de este manual es el diccionario de datos, que define la estructura lógica de las bases de datos de la aplicación. En él se especifican las tablas, las columnas, sus tipos de datos correspondientes y las restricciones de integridad (como claves primarias o foráneas).
| Campo | Tipo de dato | Longitud | Restricciones | Descripción |
|---|---|---|---|---|
id_usuario |
INT |
N/A | PRIMARY KEY |
Identificador único del usuario |
nombre |
VARCHAR |
50 | NOT NULL |
Nombre completo del usuario |
correo |
VARCHAR |
100 | UNIQUE |
Dirección de correo electrónico |
fecha_registro |
DATE |
N/A | NOT NULL |
Fecha de alta en el sistema |
El manual de mantenimiento describe el funcionamiento del registro de fallos (logs del sistema) indicando la localización de los archivos de registro y la codificación de las alertas. Asimismo, detalla los planes de recuperación ante desastres, especificando las frecuencias de las copias de seguridad y los comandos necesarios para restaurar el sistema en caso de una parada imprevista.
4.7. Automatización de pruebas y actualidad
4.7.1. Introducción a la automatización de pruebas
La automatización de pruebas consiste en el uso de herramientas de software para controlar la ejecución de pruebas y comparar los resultados esperados con los reales de forma sistemática. Este proceso sustituye la ejecución manual de casos de prueba, que consume tiempo y genera errores humanos frecuentes.
Para medir el alcance de este proceso se utiliza la cobertura de código, que define el porcentaje de líneas de código o de ramas lógicas que las pruebas han ejercitado. Para cuantificar la cobertura de forma objetiva, empleamos el índice de efectividad de las pruebas (Test Effectiveness Ratio o TER).
El cálculo de este índice se basa en la relación entre los elementos de prueba ejecutados y el total de requisitos de prueba especificados por un criterio. Si definimos como el número de sentencias ejecutadas y como el total de sentencias del programa, la cobertura de sentencias se expresa mediante la fórmula:
Para la cobertura de ramas, donde representa las ramas ejecutadas tras tomar decisiones condicionales y el total de ramas lógicas del sistema, la relación matemática se define como:
La automatización de estas métricas permite realizar comprobaciones de forma constante tras realizar cualquier modificación en el software.
4.7.2. Herramientas de automatización de pruebas unitarias
La prueba unitaria es la verificación del comportamiento de un componente individual de software de manera aislada. Los desarrolladores utilizan los marcos de prueba (test frameworks o de la familia xUnit), que son entornos de software que proporcionan clases y utilidades para estructurar y ejecutar aserciones de forma repetible.
Las aserciones son expresiones lógicas que comprueban si el valor real obtenido de una operación coincide con el valor esperado que define el diseño. Si la condición lógica de la aserción se evalúa como falsa, el marco de prueba registra un fallo en la prueba unitaria y notifica al programador.
Para estructurar estas pruebas se utiliza de forma general el patrón AAA (Arrange-Act-Assert), un método que divide la lógica de la prueba en tres etapas secuenciales y diferenciadas. La primera fase prepara el entorno; la segunda ejecuta la acción bajo análisis; y la tercera comprueba que los resultados coincidan con las expectativas.
El siguiente ejemplo escrito en lenguaje Java ilustra el uso del marco JUnit para verificar la suma de dos números:
import org.junit.jupiter.api.Test; import static org.junit.jupiter.api.Assertions.assertEquals; public class CalculadoraTest { @Test public void testSumar() { // Arrange: Preparación del escenario Calculadora calc = new Calculadora(); int sumandoA = 5; int sumandoB = 3; // Act: Ejecución de la lógica del software int resultadoReal = calc.sumar(sumandoA, sumandoB); // Assert: Verificación del resultado mediante aserción assertEquals(8, resultadoReal, "La suma de 5 y 3 debe ser 8"); } }
4.7.3. Dobles de prueba
Los dobles de prueba (test doubles) son componentes de simulación que sustituyen dependencias reales del sistema durante la ejecución de las pruebas. El uso de estos elementos facilita el aislamiento, una técnica que separa el componente bajo análisis de bases de datos, sistemas de archivos o servicios de red externos.
Podemos clasificar los dobles de prueba en diferentes categorías según su comportamiento y el objetivo que persiguen en la verificación del software:
- Fakes: dobles de prueba con una implementación funcional simplificada que no es adecuada para producción, como una base de datos en memoria.
- Stubs: dobles de prueba que retornan respuestas predefinidas (canned responses) para simular estados o datos específicos del sistema externo.
- Spies: dobles de prueba que, además de retornar valores, registran información sobre las llamadas recibidas, como los parámetros empleados.
- Mocks: dobles de prueba que verifican la interacción y comprueban de forma activa si el sistema bajo análisis realiza las llamadas esperadas.
- Dummies: objetos que se pasan como argumentos en una llamada pero que no participan en la lógica de la prueba ni sufren interacciones.
El siguiente diagrama de secuencia representa la interacción entre el sistema bajo prueba, un doble de prueba de tipo stub y el entorno para simular una consulta:
La diferencia de comportamiento y propósito de cada tipo de doble se resume en la siguiente tabla comparativa:
| Tipo de doble | ¿Tiene lógica de negocio? | ¿Almacena estado de llamadas? | Propósito principal |
|---|---|---|---|
| Dummy | No | No | Rellenar parámetros de firmas de métodos |
| Stub | No | No | Proveer datos de entrada predefinidos al SUT |
| Spy | No | Sí | Registrar las llamadas que el SUT realiza |
| Mock | No | Sí | Comprobar que las llamadas se realicen según lo planeado |
| Fake | Sí (simplificada) | No | Sustituir un servicio lento o no disponible |
4.7.4. Analizadores estáticos de código
El análisis estático de código es la evaluación del texto fuente del software sin realizar su ejecución en un entorno en tiempo de ejecución. Este proceso inspecciona el texto fuente de forma automatizada mediante reglas de gramática formal para identificar problemas antes de compilar o distribuir el programa.
Las herramientas de análisis estático se encargan de detectar la presencia de code smells, que son patrones de codificación que sugieren problemas de diseño. También buscan reducir la deuda técnica, definida como el coste acumulado por decisiones rápidas o código de baja calidad que dificultan modificaciones futuras.
Además, los analizadores identifican de forma temprana las vulnerabilidades, que son fallos de seguridad en el código que un atacante puede explotar. Ejemplos de estos fallos incluyen el desbordamiento de búfer en lenguaje C o la falta de validación en consultas de bases de datos.
Herramientas de actualidad como Error Prone para Java o Splint para C comprueban la presencia de variables no inicializadas o flujos lógicos imposibles de alcanzar. Estas utilidades facilitan el mantenimiento del código de forma independiente al criterio de cada desarrollador.
4.7.5. Integración continua
La integración continua (CI, por sus siglas en inglés) es la práctica de desarrollo que consiste en fusionar las modificaciones de código de todos los programadores en una rama principal común de forma frecuente. La integración se acompaña de procesos automáticos que mitigan fallos provocados por la interacción entre desarrolladores.
Un servidor de integración continua monitoriza el repositorio de control de versiones de forma constante. Al recibir un cambio, el servidor realiza los pasos de obtención de dependencias, compilación del código fuente, ejecución de las pruebas unitarias y generación de informes de cobertura de forma autónoma.
Si la compilación falla o alguna prueba del sistema no supera las aserciones, se genera una construcción fallida (broken build). El servidor de integración continua alerta a los programadores para detener los envíos hasta restaurar el estado estable de la rama común.
El flujo de trabajo automatizado que sigue una plataforma de integración continua al procesar los cambios de código se representa a continuación:
5. Conclusiones
La transición desde las pruebas manuales hacia la automatización continua consolida la fiabilidad de los productos de software comerciales. El error en pruebas manuales alcanza tasas elevadas, similares a las del propio código en desarrollo.
La automatización de la prueba de regresión—reincorporación de pruebas anteriores para verificar que los cambios no dañan funciones existentes—evita descuidos y reduce costos. Los sistemas de integración continua (continuous integration), definidos como plataformas que compilan y prueban el software automáticamente con cada cambio, ejecutan estas comprobaciones de forma inmediata.
La adopción de metodologías como el desarrollo guiado por pruebas o TDD (test-driven development) desplaza el control de calidad hacia las etapas iniciales de diseño. TDD define un flujo de trabajo donde el programador escribe una prueba unitaria antes de implementar el código funcional.
Esta práctica obliga a adoptar la perspectiva de quien invoca la función, facilitando un diseño modular. Para ser legible y comprobable, el software debe reducir su acoplamiento—la medida de dependencia mutua entre dos módulos—. Un diseño con bajo acoplamiento resulta sencillo de mantener y aislar.
El futuro de la validación asocia el análisis dinámico con el análisis estático de código, técnica que inspecciona el texto fuente sin ejecutar el programa para detectar anomalías semánticas.
Herramientas de análisis inteligente utilizan heurísticas y aprendizaje automático (machine learning) para predecir fallos de lógica complejos, como fugas de memoria o accesos fuera de rango, antes de la compilación. Esta detección anticipada evita que las discrepancias progresen a entornos de producción, reduciendo el costo de reparación de errores.
Conviene recordar que la documentación técnica complementa este proceso. La autodocumentación consiste en estructurar y comentar el código fuente para que explique por sí mismo el diseño de la aplicación.
Por su parte, la documentación externa recopila manuales de usuario y especificaciones de instalación. Un conjunto de pruebas automatizadas bien estructurado sirve como especificación viva de bajo nivel, ya que describe de forma exacta el comportamiento del sistema y permanece libre de desactualizaciones respecto a los documentos tradicionales de texto.
En la Formación Profesional, estas disciplinas se enseñan en módulos como Entornos de Desarrollo de los ciclos de grado superior en desarrollo de aplicaciones multiplataforma (DAM) y desarrollo de aplicaciones web (DAW).
Los estudiantes asimilan competencias sobre refactorización—reorganización del código para mejorar su diseño interno sin alterar su comportamiento externo—y el diseño de pruebas automáticas. Podemos ver que estas prácticas alinean la formación académica con los estándares productivos de la industria informática.
6. Bibliografía
- Sommerville, I. (2016). Software Engineering. Pearson. — Permite estructurar la teoría general de la verificación, validación y las fases del proceso de pruebas dentro del ciclo de vida del software.
- Myers, G. J., Sandler, C., & Badgett, T. (2011). The Art of Software Testing. John Wiley & Sons. — Proporciona el marco teórico clásico para el diseño de casos de prueba de caja negra y caja blanca.
- McConnell, S. (2004). Code Complete: A Practical Handbook of Software Construction. Microsoft Press. — Sirve de guía de referencia para el estudio de la documentación interna, la autodocumentación y las revisiones de código de forma práctica.
- Martin, R. C. (2008). Clean Code: A Handbook of Agile Software Craftsmanship. Prentice Hall. — Permite detallar las buenas prácticas de nomenclatura, comentarios y estilos de codificación de la documentación interna del programa.
- IEEE Computer Society. (2008). IEEE Std 829-2008: Standard for Software and System Test Documentation. IEEE. — Define los estándares industriales para la documentación formal de pruebas de programas, incluyendo planes y casos de prueba.
- ISO/IEC/IEEE. (2017). ISO/IEC/IEEE 12207:2017 Systems and software engineering — Software life cycle processes. ISO. — Proporciona el marco normativo internacional de referencia para los procesos de verificación, validación y documentación de software.