Pruebas y calidad de software

Fundamentos del testing de software: niveles, técnicas y el vocabulario que divide

Principiante12 min de lecturaActualizado

Esta página se tradujo del inglés con ayuda de IA y se cotejó con el original. Leer la versión en inglés

Probar es una actividad de diseño mucho antes que una cuestión de herramientas. Esta guía explica qué pueden y qué no pueden demostrar las pruebas, los niveles por los que pasa un cambio, las técnicas formales que convierten un espacio de entradas infinito en un conjunto finito de casos, y el vocabulario compartido que evita que las discusiones sobre defectos acaben en peleas por palabras.

Lo que aprenderás

  • Explicar qué pueden demostrar las pruebas y qué no podrán probar nunca
  • Distinguir los niveles unitario, de integración, de sistema y de aceptación, y para qué sirve mejor cada uno
  • Aplicar partición de equivalencia, valores límite, tablas de decisión y transición de estados para derivar casos
  • Usar error, defecto y fallo, y verificación y validación, en su sentido preciso
  • Valorar la pirámide de pruebas por sus méritos y reconocer dónde tienen razón sus críticos

Todo programa no trivial tiene comportamientos que sus autores nunca contemplaron. Las pruebas son la forma en que un equipo descubre parte de ese comportamiento antes que los clientes y, lo que es más importante para quienes mantienen el código, la forma de conservar la libertad de cambiar las cosas más adelante. Una suite que falla ruidosamente cuando un refactor rompe una regla vale más que una suite que solo demuestra que el código funcionó una vez, porque la primera sigue dando dividendos durante años.

La parte de herramientas del testing se aprende rápido y se sobrevalora con facilidad. Saber escribir una aserción de JUnit no te dice nada sobre qué aserciones merece la pena escribir. Esta guía cubre lo que viene antes: qué pueden establecer las pruebas, cómo se reparte el trabajo entre niveles, cómo derivar casos de forma sistemática en lugar de adivinar, y el pequeño vocabulario que permite a un equipo hablar de un defecto sin que tres personas entiendan tres cosas distintas.

Qué puede establecer una prueba y qué no

Una prueba ejecuta un programa con entradas elegidas en un estado elegido y compara el resultado observado con uno esperado. Como el espacio de entradas incluso de un servicio modesto es astronómicamente grande —cada cadena, cada fecha, cada orden posible de peticiones concurrentes, cada fallo parcial de una dependencia—, las pruebas exhaustivas sencillamente no existen. Lo que construyes en realidad es una muestra, elegida para maximizar la probabilidad de sacar a la luz fallos por unidad de esfuerzo.

De ahí se siguen dos consecuencias prácticas. Primero, una suite en verde no es una prueba de corrección; significa que los fallos que se te ocurrió buscar no ocurrieron en las condiciones que creaste. Segundo, el valor de la muestra depende casi por completo de cómo se eligió. Mil pruebas sobre el mismo camino feliz valen menos que veinte que sondean, cada una, un límite distinto, una rama de error o una suposición sobre un colaborador.

Por eso también el momento en que se ejecuta una prueba importa tanto como su contenido. El coste de un defecto crece con la distancia entre el momento en que se introduce y el momento en que se encuentra. Un error detectado por el compilador cuesta segundos. Detectado por una prueba unitaria en la máquina del desarrollador, minutos. En un pipeline, una hora de cambio de contexto. En producción, cuesta un incidente, un hotfix, un plan de rollback, quizá una reparación de datos y la atención de gente que estaba haciendo otra cosa. Los multiplicadores exactos que cita la literatura varían muchísimo y conviene mirarlos con recelo, pero la forma de la curva no está en discusión y justifica adelantar la detección todo lo posible.

Palabras que deben significar una sola cosa

La mayoría de las discusiones acaloradas sobre pruebas son, en realidad, discusiones sobre definiciones. Merece la pena fijar cuatro distinciones.

Error, defecto, fallo. Una persona comete un error (una equivocación de razonamiento). Ese error puede dejar un defecto en un artefacto: código, un archivo de configuración, un requisito. Cuando el código defectuoso se ejecuta en condiciones que lo exponen, se produce un fallo: un comportamiento observable distinto del esperado. No todo defecto provoca un fallo, y un fallo también puede venir del entorno o de los datos sin que haya defecto alguno. Ser preciso aquí ahorra tiempo en las revisiones de incidentes: «tuvimos un fallo» y «tuvimos un defecto» requieren investigaciones distintas.

Verificación y validación. La verificación contrasta el producto con su especificación. La validación contrasta la especificación con la necesidad. Las pruebas automatizadas son abrumadoramente de verificación; las demos, las sesiones de aceptación y las pruebas de usabilidad son donde ocurre la validación.

Pruebas y aseguramiento de la calidad. Las pruebas miden un producto. El aseguramiento de la calidad es el conjunto más amplio de prácticas —revisiones, estándares, análisis estático, definition of done— que hacen menos probables los defectos desde el principio.

Caso de prueba, suite de pruebas, ejecución. Un caso es una especificación concreta de entradas, precondiciones y resultado esperado. Una suite agrupa casos. Una ejecución es una pasada de una suite contra un build en un entorno. Decir «la prueba falló» sin indicar qué ejecución, sobre qué build, es la forma en que los equipos acaban persiguiendo fantasmas.

Los niveles por los que pasa un cambio

Los niveles describen el alcance de lo que se prueba, no la herramienta utilizada.

NivelAlcanceNormalmente lo escribenResponde a
Componente (unitaria)Una clase o un grupo pequeño, aislado de colaboradores lentosDesarrolladores¿Se comporta esta lógica según lo especificado?
IntegraciónDos o más componentes, o un componente y un sistema externo realDesarrolladores¿Coinciden estas partes en su contrato?
SistemaLa aplicación desplegada en su conjuntoDesarrolladores y testers¿Cumple el producto ensamblado sus requisitos?
AceptaciónEl producto en su contexto de negocioEquipo de producto, usuarios¿Es esto lo que necesitábamos y podemos lanzarlo?

Las fronteras entre niveles son convenciones, no leyes, y los equipos discuten sin fin si una prueba que arranca una base de datos es «unitaria» o «de integración». La discusión suele ser inútil; lo que importa es que cada prueba tenga un sujeto claramente identificado y que un fallo apunte a un sitio concreto. Una prueba cuyo fallo puede significar nueve cosas distintas está mal acotada, la llames como la llames. La guía de pruebas unitarias y la guía de pruebas de integración explican cómo se construye cada una en Java.

Las pruebas de aceptación tienen variantes que conviene nombrar: las pruebas de aceptación de usuario (UAT), a cargo de quienes usarán el sistema; la aceptación operativa, que cubre copias de seguridad, restauración, monitorización y runbooks; la aceptación contractual o regulatoria, cuando hay que demostrar el cumplimiento de una norma externa; y las pruebas alfa/beta, en las que usuarios reales ponen a prueba una versión previa.

Lo funcional y todo lo demás

Las pruebas funcionales se preguntan si el sistema hace lo correcto. Las no funcionales se preguntan lo bien que lo hace, y es ahí donde de verdad se originan la mayoría de los incidentes en producción. Las familias principales:

  • Eficiencia de desempeño — tiempo de respuesta, throughput, consumo de recursos bajo carga. Se trata en la guía de pruebas de rendimiento.
  • Fiabilidad — comportamiento cuando falla una dependencia, recuperación tras una caída, durabilidad de los datos.
  • Seguridad — autenticación, autorización, inyección, gestión de secretos, vulnerabilidades en dependencias.
  • Usabilidad y accesibilidad — si una persona puede completar la tarea, también con un lector de pantalla o solo con teclado.
  • Compatibilidad — navegadores, dispositivos, versiones de base de datos, clientes de API antiguos.
  • Mantenibilidad y portabilidad — cualidades que se miden sobre el código y el despliegue, no en tiempo de ejecución.

Los requisitos no funcionales suelen redactarse como adjetivos («el sistema debe ser rápido»), y eso no se puede probar. Convertirlos en números con contexto —«latencia del percentil 95 por debajo de 300 ms para el endpoint de búsqueda con 200 usuarios concurrentes en el tamaño de instancia estándar de producción»— es la acción de mayor impacto que un equipo puede emprender por la calidad no funcional, porque convierte una opinión en una comprobación de pasa/no pasa.

Caja negra, caja blanca y lo que hay en medio

Las pruebas de caja negra (basadas en la especificación) derivan los casos de lo que el sistema debe hacer, sin referencia alguna a cómo está escrito. Es el único tipo posible para un componente de terceros y sobrevive intacto a los refactors, por eso la mayoría de las pruebas valiosas son de caja negra respecto a su sujeto.

Las pruebas de caja blanca (basadas en la estructura) derivan los casos del código: sentencias, ramas, condiciones, caminos. Su medida es la cobertura, la fracción de sentencias o ramas ejecutadas. El pensamiento de caja blanca es el que te dice que una suite nunca ejercita la rama else de una comprobación de validez.

Las técnicas basadas en la experiencia cubren los huecos que las otras dos dejan: la conjetura de errores (sondear los sitios donde históricamente viven los defectos: colecciones vacías, cero, Unicode, cambios de horario de verano), las pruebas exploratorias (aprendizaje, diseño y ejecución simultáneos en sesiones con tiempo acotado) y las checklists construidas a partir de incidentes pasados.

Derivar casos en lugar de adivinar

Las técnicas formales de diseño de pruebas existen para convertir un espacio de entradas infinito en un conjunto pequeño de casos con una justificación argumentada. Cuatro de ellas cubren casi todas las necesidades del día a día.

Partición de equivalencia. Divide cada entrada en clases cuyos miembros deberían tratarse igual y prueba un miembro por clase. Para un campo de edad que acepta de 18 a 65, las particiones son «menos de 18», «de 18 a 65» y «más de 65», además de las clases inválidas: no numérico, vacío, ausente. Tres o cuatro valores sustituyen a millones.

Análisis de valores límite. Los defectos se concentran en los bordes de las particiones, porque es ahí donde se confunden < y <=. Para cada límite, prueba el valor exacto y los inmediatamente adyacentes a cada lado: 17, 18, 19 y 64, 65, 66. Es la técnica con mayor rendimiento por prueba escrita, y por eso aparece en casi todas las especificaciones.

Tablas de decisión. Cuando el comportamiento depende de una combinación de condiciones, ponlas en una tabla. Una regla de envío basada en el importe del pedido, el nivel del cliente y el destino tiene un conjunto finito de combinaciones; escribirlas deja al descubierto las que nadie especificó.

Pedido ≥ 50Miembro premiumNacionalEnvío
sícualquierasígratis
sísínogratis
sínonotarifa plana
nosísígratis
nonocualquieratarifa estándar

Una tabla con cuatro condiciones binarias independientes tiene dieciséis filas, que suele ser el momento en que alguien se da cuenta de que la regla es demasiado complicada. Cuando el conjunto combinatorio completo no es viable, las pruebas por pares (pairwise) cubren cada par de valores de parámetros con muchos menos casos y detectan la mayoría de los defectos de interacción.

Pruebas de transición de estados. Para todo lo que tenga un ciclo de vida —un pedido, un pago, una suscripción—, dibuja los estados y los eventos permitidos. Después prueba cada transición válida y, sobre todo, las inválidas: cancelar un pedido ya enviado, pagar una factura ya reembolsada. En las transiciones inválidas es donde se esconden los defectos interesantes, porque la especificación rara vez las menciona.

La pirámide y sus críticos

La pirámide de pruebas es una heurística sobre la composición de la suite: muchas pruebas rápidas y aisladas en la base, menos pruebas de integración en el medio y un puñado de pruebas end-to-end en la cima. Su justificación es económica: las pruebas de abajo se ejecutan en milisegundos, fallan por un único motivo y rara vez son inestables, mientras que las de arriba son lentas, frágiles y poco claras cuando fallan.

Los críticos plantean dos objeciones justas. La primera, que la forma suele aplicarse sobre el eje equivocado: lo que importa no es el framework utilizado sino el aislamiento y la velocidad de la prueba, y una prueba «unitaria» que mockea todos sus colaboradores puede pasar mientras el sistema ensamblado está roto. La segunda, que en un servicio que es sobre todo pegamento entre una base de datos y un cliente HTTP hay muy poca lógica pura que probar de forma unitaria, y una forma más ancha en el medio (a veces dibujada como un trofeo o un panal) refleja mejor la realidad.

Ambas críticas se refieren a las proporciones concretas, no al compromiso de fondo, que nadie discute: las pruebas más amplias encuentran más tipos de problemas y cuestan más de ejecutar y mantener. La guía de estrategia de automatización explica cómo elegir una forma para un sistema concreto en lugar de heredarla de un diagrama.

Del defecto encontrado al defecto cerrado

Un informe de defecto es una herramienta de comunicación, y uno malo hace perder más tiempo que el propio defecto. Lo esencial: qué se hizo, qué pasó, qué se esperaba, en qué build y en qué entorno, con logs o una captura de pantalla y la secuencia más corta que lo reproduce. Después se le asignan dos atributos que se confunden a menudo. La severidad es el impacto técnico: una pérdida de datos es de severidad alta, la haya detectado quien la haya detectado. La prioridad es la urgencia de negocio de corregirlo. Un desajuste estético en la página de registro puede tener severidad baja y prioridad alta; una caída en una funcionalidad que usan tres personas puede ser justo lo contrario.

El ciclo de vida es entonces una pequeña máquina de estados: nuevo, triado, asignado, corregido, listo para volver a probar, verificado, cerrado, con ramas laterales para rechazado, duplicado y aplazado. Dos reglas lo mantienen honesto: solo quien reportó el defecto (o la prueba que lo detectó) lo cierra, y todo defecto corregido recibe una prueba de regresión para que el mismo fallo no pueda volver en silencio.

Saber cuándo empezar y cuándo parar

Los criterios de entrada establecen qué debe cumplirse antes de que empiece una fase de pruebas: el build se despliega, los smoke tests pasan, los datos de prueba están cargados y el entorno coincide con la configuración objetivo. Su propósito es evitar que un equipo pierda días probando un build que nunca iba a funcionar.

Los criterios de salida establecen qué debe cumplirse para darla por terminada. Los útiles son concretos y medibles: todos los casos planificados ejecutados, ningún defecto de severidad alta abierto, cobertura del código nuevo por encima de un umbral acordado, todos los umbrales no funcionales cumplidos, defectos abiertos conocidos documentados y aceptados. Los inútiles son «las pruebas están completas» o «nos quedamos sin tiempo» disfrazados de criterio. En un pipeline de entrega continua estos criterios dejan de ser una puerta entre fases y se convierten en quality gates automatizadas: la misma idea, impuesta por una máquina en lugar de por una reunión. La guía de integración continua muestra cómo se ve en la práctica.

Puntos clave

  • Las pruebas muestrean el comportamiento; demuestran la presencia de fallos y nunca pueden probar su ausencia, así que importa mucho más cómo se eligen los casos que cuántos hay.
  • Los defectos se encarecen enormemente cuanto más tarde se encuentran, y ese es todo el argumento económico a favor de pruebas automatizadas que se ejecuten pronto y a menudo.
  • Los niveles describen alcance —componente, integración, sistema, aceptación— y cada prueba debe tener un único sujeto claramente identificado para que un fallo apunte a un sitio concreto.
  • Los requisitos no funcionales solo se vuelven comprobables cuando se escriben como números con contexto; un adjetivo no puede romper un build.
  • La partición de equivalencia, los valores límite, las tablas de decisión y las transiciones de estado convierten un espacio de entradas infinito en un conjunto de casos defendible; los límites son los que antes amortizan el esfuerzo.
  • Un vocabulario preciso —error frente a defecto frente a fallo, verificación frente a validación, severidad frente a prioridad— agiliza el triaje y acorta las discusiones.

Fuentes y lecturas recomendadas

Preguntas frecuentes

¿Pueden las pruebas demostrar que un software no tiene bugs?
No. Las pruebas muestrean el comportamiento de un programa con entradas elegidas, y el espacio de entradas, estados y secuencias temporales posibles es prácticamente infinito. Una suite que pasa demuestra que los fallos que buscabas no se produjeron en las condiciones que creaste. Es una evidencia valiosa, pero evidencia de presencia, nunca prueba de ausencia; por eso la cobertura y los builds en verde son señales, no garantías.
¿Qué diferencia hay entre verificación y validación?
La verificación se pregunta si el producto se construyó conforme a su especificación: ¿hace el código lo que decía el diseño? La validación se pregunta si esa especificación resuelve el problema real del usuario. Una funcionalidad puede superar todas las verificaciones y fallar la validación porque el requisito era erróneo. Las pruebas unitarias y de integración sobre todo verifican; las pruebas de aceptación de usuario, las demos y las sesiones de usabilidad validan.
¿Son lo mismo el aseguramiento de la calidad y las pruebas?
Las pruebas son una actividad dentro del aseguramiento de la calidad. La QA abarca todo lo que determina cómo se previenen y detectan los defectos: revisión de requisitos, estándares de código, pair programming, análisis estático, definition of done, retrospectivas y mejora de procesos. Las pruebas miden el producto; la QA actúa sobre el proceso que lo produce. Un equipo puede probar muchísimo y aun así tener un aseguramiento de la calidad pobre si nada cambia cuando los defectos se repiten.
¿Cuántos casos de prueba son suficientes?
No hay un número absoluto. Deriva los casos de la especificación con una técnica como la partición de equivalencia y el análisis de valores límite, añade casos para las zonas de riesgo conocidas y los defectos pasados, y detente cuando los casos nuevos ya no cambien tu nivel de confianza. Después mide el riesgo residual en lugar del recuento: qué requisitos no están probados, qué rutas de código nunca se ejecutan, qué modos de fallo nunca se simularon.