Resumen
Un programa que compila y escribe algo razonable no está, por eso, probado. Esta clase enseña a probar con método: a derivar los casos de la especificación, por regiones y bordes; a reunirlos en un programa de pruebas automáticas que cuenta los fallos y los informa; a comprobar con assert lo que no puede ocurrir si el programa es correcto, y con static_assert, al compilar, lo que una función constexpr debe calcular; y a ejecutar las pruebas con los detectores, que descubren el comportamiento indefinido que un resultado correcto oculta. Todo descansa en una advertencia: una prueba puede mostrar la presencia de un error, nunca su ausencia.
Objetivos de aprendizaje
- Derivar los casos de prueba de la especificación de una función: las particiones de su entrada, los bordes entre ellas y los casos especiales, con el resultado que la poscondición exige en cada uno.
- Escribir un programa de pruebas automáticas que compare los resultados obtenidos con los esperados, informe cada fallo sin detenerse y termine con un resumen y un código de terminación que lo refleje.
- Usar
assertpara comprobar precondiciones e invariantes, y explicar por qué desaparece al compilar con-DNDEBUGy por qué su condición no debe tener efectos secundarios. - Usar
static_assertcon funcionesconstexprpara probar resultados al compilar, y ejecutar las pruebas con los detectores para descubrir el comportamiento indefinido que los resultados no revelan.
Qué es probar
Probar un programa es ejecutarlo con entradas elegidas y comparar lo que hace con lo que debía hacer. Cada caso de prueba consta de dos partes: una entrada que cumple la precondición, y el resultado que la poscondición exige para ella, calculado de antemano y por otro camino (a mano, con otra fórmula, con una tabla conocida). Sin el resultado esperado no hay prueba, sino una ejecución: mirar la salida y encontrarla «razonable» es una impresión, no una comparación.
Ahora bien, una función de un argumento int admite más de cuatro mil millones de entradas, y una de dos argumentos, unos pares; ninguna batería de pruebas las recorre todas. Si esto es así, las pruebas examinan una parte ínfima de los casos posibles, y de que pasen todas no se sigue que la función sea correcta. Edsger Dijkstra lo formuló en 1970: las pruebas pueden mostrar la presencia de errores, nunca su ausencia. La distinción es la que el curso de Lógica Matemática I establece entre verificar casos y demostrar: comprobar que una fórmula es verdadera para algunos valores no prueba que lo sea para todos, mientras que una demostración (en este curso, el invariante de la clase Diseñar bucles correctos) cubre todos los casos a la vez. Las pruebas no reemplazan ese razonamiento: encuentran los errores que pasó por alto.
De ello se sigue el criterio para elegir los casos. Si las pruebas no pueden confirmar la corrección, su valor está en encontrar errores, y un buen caso de prueba es el que tiene más probabilidad de fallar. Un caso que el autor eligió porque sabía que funcionaría no informa nada. El programa siguiente lo muestra con una función que debe devolver el mayor elemento de un vector no vacío.
#include <print>#include <vector>// Precondición: values no está vacío.// Poscondición: devuelve el mayor elemento de values.int largest(const std::vector<int>& values){ int best{0}; for (const int v : values) { if (v > best) { best = v; } } return best;}int main(){ std::println("{}", largest({3, 9, 4})); std::println("{}", largest({7})); std::println("{}", largest({1, 2, 3, 4})); std::println("{}", largest({-5, -2, -8}));}9740Los tres primeros casos son los que cualquiera escribe primero, y la función los pasa: 9, 7 y 4 son correctos. El cuarto da 0, un valor que ni siquiera está en el vector, cuando debía dar . El error está en la línea 8: best empieza en 0 y no en el primer elemento, de modo que la función es correcta solo si algún elemento es positivo o cero. Los casos típicos compartían esa propiedad sin que nadie la eligiera; el cuarto la viola a propósito. La corrección es int best{values[0]};, que la precondición hace legítima, porque garantiza que values[0] existe.
Cargando el contenido…