Diseño de pruebas
Los casos deciden lo que el conjunto puede detectar. Todo lo demás — el arnés, el ejecutor, el conteo — solo ejecuta esa decisión.
Los momentos que esto reemplaza
| A punto de… | En su lugar… |
|---|---|
| escribir el camino feliz y parar | recorre el espacio de entrada: para cada parámetro, sus clases y sus bordes |
| derivar casos de la implementación | derívalos de la especificación, y deja que la implementación te sorprenda |
| probar con un valor cómodo | prueba el último valor dentro y el primer valor fuera de cada límite |
| probar el límite que la especificación nombra | cada rango tiene dos extremos; el que la oración omitió sigue siendo un límite |
| enumerar los roles que la especificación lista | añade el llamador que olvidó: sin rol, no autenticado, no asignado |
| variar una condición desde el camino feliz | construye la tabla de decisión y cubre las reglas donde dos condiciones fallan juntas |
| dibujar una tabla de estados y seguir adelante | pon un id de prueba en cada fila; la fila que dice "no permitido" más que ninguna |
| escribir "rechazado, o aceptado siempre que…" como resultado esperado | un resultado por caso; una especificación que no puede decidir es una pregunta abierta, no una prueba |
| saltar caminos de error como improbables | cada modo de fallo es una clase: entrada inválida, recurso faltante, denegado, tiempo de espera, medio hecho |
| dejar un camino sin implementar y sin prueba, un marcador de salto o un stub que nadie afirma | escribe una prueba de que el camino se rechaza como no implementado hoy; pasa ahora y se vuelve roja el día que alguien lo construye (abajo) |
| probar una característica por su flujo principal | lee los flujos alternativos y de excepción del caso de uso; cada uno es una clase |
| calificar un conjunto leyéndolo | cuéntalo contra la especificación: clases, ambos límites, reglas, transiciones |
| añadir una quinta prueba a una clase cubierta | gástala en una clase no cubierta — la quinta compra tiempo de ejecución y no cobertura |
Ataca la especificación, no la implementación
Una prueba escrita leyendo el código afirma lo que el código hace. Pasa al llegar, codifica los errores actuales como expectativas, y sus fallos después de un cambio dicen "el código cambió", lo que ya se sabía. Esto se mide, no se sospecha: en 24 repositorios Java, los oráculos generados por LLM tendían a capturar el comportamiento real del programa en lugar de su comportamiento esperado (Konstantinou, Degiovanni, Papadakis, arXiv:2410.21136).
Deriva de lo que la cosa debe hacer — la especificación, el formato de cable, el contrato — y solo entonces ejecútala. La única excepción legítima es la prueba de caracterización que deliberadamente fija el comportamiento actual, y está marcada como tal para que nadie confunda después un error fijado con un requisito.
Un indicio útil: si cada prueba estaba verde en su primera ejecución, el conjunto fue derivado de la implementación, cualquiera que fuera la intención.
Clases de equivalencia: cuenta clases, no pruebas
Las entradas que el código no puede distinguir forman una clase, y una prueba por clase es suficiente — la segunda a la quinta cuestan tiempo de ejecución y no prueban nada nuevo. La disciplina opera en ambas direcciones: ninguna clase queda sin cubrir, ninguna clase cubierta dos veces por accidente.
Un conjunto de diecisiete pruebas se lee como exhaustivo. Si lo es depende de cuántas clases caen dentro de esas diecisiete, y ese número no aparece en la salida de ningún ejecutor. Debe contarse contra el espacio de entrada a mano — que es lo que es un plan de pruebas.
Las clases que la especificación lista son la mitad fácil. Una tabla de roles con tres filas tiene una cuarta clase, la solicitud que no coincide con ninguna de ellas, y es la que un conjunto construido a partir de la tabla nunca contiene. Para cada parámetro, escribe las clases inválidas junto a las válidas antes de escribir cualquier caso: tipo incorrecto, ausente, fuera de cada valor listado, el llamador sin ningún derecho.
El temario establece esto como una regla más que como un hábito: en la partición de equivalencia los elementos de cobertura son las particiones, al 100% significa cada partición identificada ejercitada "incluyendo particiones inválidas", y las particiones "no deben solaparse y deben ser conjuntos no vacíos" (ISTQB CTFL v4.0.1 §4.2.1). Con varios parámetros nombra el criterio más débil Cada Elección — cada partición de cada parámetro una vez, combinaciones ignoradas — que es el nivel que la mayoría de los conjuntos alcanza sin saber su nombre, y donde comienza la tabla de decisión abajo.
Límites, porque ahí es donde están los fallos
El error por uno es el fallo aritmético más común,
y es invisible en todas partes excepto en el borde.
El par que importa en cada límite es el valor límite y su vecino más cercano al cruzar la línea,
que es el BVA de 2 valores (CTFL v4.0.1 §4.2.2);
el BVA de 3 valores añade también al vecino del interior,
y el temario muestra lo que eso compra:
una comprobación pensada como x ≤ 10 pero escrita como x = 10 pasa tanto 10 como 11,
y solo 9 la detecta.
Los bordes recurrentes: vacío, exactamente uno, muchos; cero y negativo;
el máximo declarado y uno más allá;
el valor que es exactamente el búfer, el límite, el tiempo de espera.
Un par concreto de esta máquina: una comprobación de PATH que está bien en 8.190 caracteres y debe fallar en 8.192 tiene que probarse en esos dos valores. Pasar en 4.000 y 12.000 establece casi nada sobre la línea entre ellos.
Donde el límite es una precondición declarada,
el valor más allá es un error del llamador más que un caso con un resultado esperado;
design-by-contract decide cuál de los dos es un borde dado,
y la prueba entonces afirma el que aplica.
El lado que la especificación no nombró
"Hasta 10 MB" nombra un extremo de un rango. El otro extremo existe tanto si la oración lo menciona como si no, y los casos derivados del texto se detienen donde el texto se detiene: un conjunto con el límite superior probado en −1, 0 y +1 bytes y nada por debajo de 1.024 ha hecho análisis de límites en la mitad de un rango. La definición no tiene lado: "los valores mínimo y máximo de una partición son sus valores límite" (CTFL v4.0.1 §4.2.2). Escribe ambos para cada partición primero, luego toma cada uno con su vecino más cercano en la partición adyacente. El lado no nombrado suele ser cero, vacío o uno, y la especificación suele callarlo porque nadie decidió.
Mueve dos condiciones a la vez
Un conjunto construido cambiando una condición del camino feliz cubre cada valor de cada condición y casi ninguna de sus combinaciones: formato incorrecto, tamaño excedido, cuenta excedida, cada uno solo, y ningún caso donde dos fallen juntas. Qué error gana cuando ambos aplican rara vez está en la especificación tampoco, así que el caso faltante esconde una decisión faltante.
Dispón las condiciones como una tabla de decisión y cuenta las reglas,
luego cubre las reglas donde dos condiciones fallan a la vez y la regla donde una restricción de rol y una restricción de formato aplican ambas.
En las pruebas con tabla de decisión los elementos de cobertura son las columnas que contienen combinaciones factibles,
y al 100% significa cada columna de este tipo ejercitada (CTFL v4.0.1 §4.2.3);
el temario añade que la tabla "ayuda a encontrar cualquier vacío o contradicción en los requisitos",
que es la decisión faltante de arriba que surge por sí sola.
El mismo documento puede leerse como completamente cubierto bajo Cada Elección y un cuarto cubierto bajo las columnas,
y solo el segundo número dice algo sobre las combinaciones.
Cuando el conteo de reglas es el producto de un conjunto de banderas,
la corrección más barata está arriba:
state-first colapsa las banderas en los estados que pueden ocurrir,
y la tabla se reduce a ellos.
Una tabla es una afirmación sobre el conjunto
Una tabla de estados, una tabla de decisión, la lista de flujos de un caso de uso: cada fila afirma que existe una prueba para ella. Pon el id de la prueba en la fila. Una fila sin id es una clase no cubierta, y la fila que dice "no permitido" es la que más probablemente está vacía, porque escribir la prohibición sintió como haberla manejado. La transición prohibida — el segundo envío mientras uno está en vuelo — es la prueba, y la guarda que se supone debe detenerlo es la cosa bajo prueba.
La tabla de estados hace esa fila explícita: una transición inválida es una celda vacía, y el más fuerte de los tres criterios que define el temario, cobertura de todas las transiciones, requiere intentar cada una de ellas — una por caso de prueba, para que un defecto no pueda enmascarar a otro (CTFL v4.0.1 §4.2.4). La cobertura de transiciones válidas, el interruptor 0 en el que la mayoría de los conjuntos se detienen, nunca toca esas celdas, que es cómo una fila "no permitido" no probada se mantiene verde.
Luego lee los dos documentos uno contra el otro. Una prueba cuyo camino discrepa de la tabla (la tabla dice fallido → enviando, la prueba va fallido → seleccionado → enviando) significa que uno de ellos está equivocado, y nada más que la comparación descubre cuál. Las auto-transiciones y las transiciones fuera del estado de fallo también cuentan como filas.
Un resultado por caso
"Rechazado, o si se acepta, no quedan datos corruptos después del envío" no puede pasar ni fallar. Aparece honestamente, cuando la especificación nunca dijo qué sucede con un archivo de cero bytes y el autor se negó a inventar una respuesta, y sobrevive a la revisión porque el caso está visiblemente presente. La disyunción se esconde en la columna esperada. La forma correcta es una pregunta a quien sea dueño de la especificación, listada como tal, y ninguna prueba para esa clase hasta que se responda.
Las rutas de error también son clases
Los caminos infelices son donde se acumula código no probado, porque nada los ejercita en uso normal — un titular de bloqueo que muere a medio camino, una carpeta mapeada que no existe, entrada con el campo esperado faltante. Cada una es una clase de equivalencia con el mismo derecho a una prueba que cualquier rango de entrada. Un conjunto que solo prueba que la herramienta funciona cuando todo coopera ha probado la demo, no la herramienta.
Los casos de uso enumeran las clases antes de que haya código
Un caso de uso es una especificación ya cortada en clases: un flujo principal, cada flujo alternativo, cada flujo de excepción, y la precondición bajo la cual se puede entrar a cada uno. Quien lo escribió hizo la enumeración que esta habilidad pide — los flujos alternativos y de excepción son las rutas de error — así que un conjunto que ejercita el flujo principal ha cubierto una clase de las N que el documento lista.
Lee los flujos del caso de uso y dale a cada uno su prueba, luego verifica las precondiciones, porque un flujo es alcanzable solo cuando su precondición puede cumplirse. Una verificación de paridad para páginas traducidas tiene un flujo principal (ambos idiomas editados y confirmados), un flujo alternativo (una edición unilateral declarada como excepción), y dos flujos de excepción que solo aparecen una vez que las precondiciones están escritas: el árbol está sucio, por lo que la desactualización aún no puede juzgarse; la historia fue reescrita, por lo que una confirmación declarada ya no se resuelve. Ambos se alcanzaron en un sitio dentro de una semana. El camino feliz solo no habría mostrado ninguno.
Si un flujo de excepción puede realmente ocurrir no se resuelve por lo improbable que se sienta.
El sobre operativo dice qué está fuera del alcance y el análisis de peligros dice qué queda;
ambos viven en assurance-case,
y esta habilidad toma la lista que producen en lugar de podarla por intuición.
La cobertura es un número bajo un criterio nombrado
Un documento de prueba medido de tres maneras: 100% de condiciones, 90,9% MC/DC,
25% de reglas de decisión.
Las tres son ciertas del mismo archivo,
así que una cifra de cobertura sin su criterio no dice nada,
y una cifra cuyo criterio se elige después del conteo dice menos.
Nómbralo antes de contar,
en los propios términos del temario para que el número pueda ser verificado:
cobertura de partición y Cada Elección (§4.2.1),
BVA de 2 o 3 valores (§4.2.2), columnas de tabla de decisión (§4.2.3),
todos los estados, transiciones válidas o todas las transiciones (§4.2.4).
MC/DC no está en el temario Foundation;
viene de los estándares de seguridad (DO-178C, ISO 26262),
y listarlo junto a los demás sin decirlo es el tipo de cita mixta que se lee como rigor.
El resto de lo que un número le debe a su lector está en measured-claims.
Revisar un conjunto: cuenta, no juzgues
Preguntar si un conjunto es adecuado devuelve los puntos ciegos que lo produjeron.
Una segunda sesión sobre el mismo modelo los comparte,
y también lo hace la misma persona un día después.
El temario llama a eso "cierta independencia",
el segundo de sus cuatro niveles, y dice de dónde viene el valor:
"diferencias entre los sesgos cognitivos del autor y del probador" (CTFL v4.0.1 §1.5.3).
Una segunda sesión sobre el mismo modelo comparte los sesgos y conserva solo la etiqueta.
El paso que no los comparte es contar:
lista las clases de la especificación y marca las que tienen una prueba,
lista ambos límites de cada rango, lista las reglas de la tabla de decisión,
lista las filas de cada tabla de estados,
y cita el archivo y línea de cada vacío.
Si un valor esperado es correcto sigue siendo un juicio y permanece con un lector que no lo escribió;
si una clase tiene una prueba es aritmética.
Si el conteo fue muestreado en lugar de exhaustivo,
el informe dice muestreado.
La forma de refutar por defecto para el veredicto está en adversarial-verify;
esta habilidad suministra qué contar.
Cuando la solicitud elimina una clase
"El doble envío no puede ocurrir porque el botón está deshabilitado, elimina esa fila" elimina una clase del conjunto basándose en la fuerza de una guarda en una capa. Haz lo que se pidió, y di en una oración qué clase ahora está sin cubrir y qué la alcanzaría: la máquina de estados aún acepta un segundo envío de cualquier llamador que no sea el botón. Una fila eliminada con esa oración es una decisión; una fila eliminada sin ella es un vacío que nadie encontrará de nuevo.
Un camino no implementado tiene una prueba que lo dice
Un camino dejado fuera de este cambio a propósito,
un rol que este lanzamiento no soporta, un formato aún no analizado,
una rama que lanza NotImplementedException o llama a todo!(),
sigue siendo una clase de la entrada,
y el conjunto registra lo que sucede allí hoy.
Escribe la prueba de que el camino se rechaza como no implementado:
la excepción o el valor de error que levanta,
el elemento del menú que está ausente, la solicitud que responde "no soportado".
Esa prueba pasa ahora y hace dos trabajos. Pone el vacío en la lista del conjunto, donde un lector que cuenta clases lo encuentra, en lugar de en un comentario o un ticket. Y se vuelve roja el día que alguien implementa el camino, que es el momento en que sus casos reales tienen que ser escritos; la prueba fallida es el recordatorio, y se reemplaza, no se elimina.
Un marcador de salto o una prueba pendiente no es lo mismo. Un salto es el propio equipo diciendo que la ejecución es opcional; una prueba pendiente no tiene nada que fallar. Ninguno de los dos aparece como una clase en el conteo, y ninguno se vuelve rojo cuando el camino aparece. El mecanismo que entrega esa señal es la prueba que afirma el rechazo, y ese es el único mecanismo que funciona.
Cuenta cada tipo de cobertura una vez
La cobertura de ramas mide qué ramas de la implementación se ejecutaron.
No mide qué casos se diseñaron — una rama puede estar cubierta por una prueba que nunca se propuso probarla,
y una clase puede estar sin probar mientras cada rama que la implementa fue golpeada.
El número que importa para el diseño de pruebas es la cobertura de la especificación:
clases, límites, reglas de decisión, transiciones enumeradas en la especificación y verificadas.
La cobertura de código dice si la implementación es alcanzable,
lo que es una propiedad diferente que pertenece a structural-coverage.
El mismo archivo puede tener cobertura de ramas al 100% y cobertura de especificación al 30%.
La práctica completa
Para un solo caso de uso:
- Lee los flujos principal, alternativos y de excepción del caso de uso. Para cada flujo, identifica sus condiciones de entrada y resultados.
- Para cada parámetro de entrada, enumera las clases de equivalencia válidas e inválidas. No olvides: tipo incorrecto, ausente, fuera de rango, sin derechos.
- Para cada clase con un rango continuo, encuentra ambos límites. Prueba el límite inferior y superior con 2 valores (el límite y su vecino exterior) o 3 valores (incluyendo el vecino interior).
- Para condiciones lógicas, construye la tabla de decisión. Cubre todas las reglas factibles, especialmente aquellas donde dos condiciones fallan.
- Para máquinas de estado, enumera todos los estados y transiciones. Prueba todas las transiciones válidas e inválidas (criterio de todas las transiciones).
- Escribe una prueba por caso. Cada prueba tiene un resultado esperado único. Sin disyunciones, sin 'o' en la columna esperada.
- Para caminos no implementados, escribe una prueba que afirme el rechazo. Pasa ahora; se vuelve roja cuando se implementa.
- Cuenta la cobertura: clases cubiertas / clases totales, límites cubiertos / límites totales, reglas cubiertas / reglas totales, transiciones cubiertas / transiciones totales. Nombra el criterio.
- Documenta cualquier vacío con una justificación. Si se elimina una clase, di qué la alcanzaría.
Esta rutina se puede ejecutar para cualquier especificación, antes o después del código. Cuando se aplica al código existente, revela las clases que las pruebas existentes pasan por alto, y da una lista de casos para añadir.