La prueba y validación de la unidad (cumplimiento del contrato por parte del llamado) son cosas muy diferentes y complementarias. Ambos juntos dan como resultado un código MUCHO más confiable, con MUCHO menos tiempo dedicado a la depuración, de lo que uno podría ganar confiando solo en las pruebas unitarias o en la validación.
Para mostrar por qué las pruebas unitarias por sí solas no son suficientes, supongamos que tenemos el siguiente método:
public int methodM (int m)
- ¿Qué software se usa para crear programas?
- ¿Cuáles son las teorías y técnicas detrás de la integración de diversas tecnologías de software?
- Soy un ingeniero de software masculino. ¿Es correcto no querer trabajar en un equipo que no tiene ingenieras?
- ¿Cuál es la diferencia entre las pruebas de juegos y las pruebas de software?
- ¿Cómo se decide qué lenguaje de programación usar para un proyecto?
donde m puede estar válidamente en el rango [0, 100]. Supongamos que methodM es llamado por methodN:
public int methodN (int n)
donde n puede estar válidamente en el rango [100, 200].
Suponga que el método N ha sido probado por la unidad para los siguientes valores de n: 100, 150, 200, es decir, un caso promedio típico (150) y dos casos límite válidos (100, 200). Funciona bien en todos esos casos. Sin embargo, suponga también que cuando n está entre 110 y 120, methodN llama a methodM con valores negativos de m, que no son válidos. Este error en el método N PODRÍA ser detectado a través de más pruebas unitarias del método N. Sin embargo, un valor de prueba elegido al azar de n, dentro del rango oficialmente válido del métodoN [100, 200], tiene solo una probabilidad de 1 en 10 de activar el error. Luego, una vez que se solucione el error, necesitaríamos un total de 101 pruebas unitarias del método N, una prueba para cada valor posible de n, para estar absolutamente seguros de que el método N nunca hace nada malo.
De todos modos, supongamos que todavía no hemos activado el error, y ahora escribimos otro método que llama a methodN:
public int methodP (int p)
donde p puede estar válidamente en el rango [200, 300]. Ahora hay al menos 2 tipos de errores que methodP puede tener con respecto a sus llamadas a methodN: (1) puede llamar a methodN usando valores no válidos de n, o (2) puede llamar a methodN usando valores de n que son oficialmente válidos pero qué método de activación M se llamará con valores no válidos de m. De cualquier manera, las posibilidades de activar el error con solo tres pruebas unitarias del método P son muy pequeñas.
Supongamos ahora que escribimos otro método que llama a methodP:
public int methodQ (int q)
Ahora hay al menos 3 tipos de errores que methodQ puede tener con respecto a sus llamadas al método P: (1) puede llamar al método P usando valores no válidos de p, (2) puede llamar al método P usando valores de p que son oficialmente válidos pero qué método de activación N se llamará con valores no válidos de n. o (3) puede llamar a methodN usando valores de n que son oficialmente válidos pero que activan a methodM para que se llame con valores no válidos de m. En todos los casos, es probable que las posibilidades de desencadenar el método Q pero con solo tres unidades de prueba sean muy pequeñas, por lo que es muy probable que el error termine en el código de producción.
Supongamos que el error finalmente se activa en la producción. Probablemente llevará horas rastrear este error.
En el ejemplo anterior, cada método por encima del método de nivel inferior M llama a otro método, por lo que el número de posibles fuentes de errores en el método de nivel superior Q aumenta linealmente con el número de métodos.
En realidad, la mayoría de los métodos de alto nivel llaman a más de un método de nivel inferior, por lo que el número de posibles fuentes de errores de nivel inferior en el método de nivel superior aumenta EXPONENCIALMENTE con el número de métodos. Un sistema con solo, digamos, 100 métodos puede tener fácilmente millones de diferentes rutas lógicas posibles a través del código. Obviamente, cubrir todas y cada una de ellas con pruebas unitarias no es factible.
Por lo tanto, en un sistema grande, si no realiza ninguna validación interna, puede llevar muchos días, semanas, meses o incluso años localizar un error, INCLUSO con pruebas unitarias razonablemente buenas.
Por otro lado, si todos los métodos públicos validan sus parámetros, entonces la gran mayoría de los errores se pueden rastrear INSTANTÁNEAMENTE sin importar cuán grande sea el sistema, e incluso aquellos errores que no se pueden encontrar instantáneamente se pueden aislar mucho más rápido de lo que posiblemente podría ser de otra manera.
Fuera de línea, me mencionó que ha tenido algo de mala suerte al intentar hacer cumplir el contrato en C. Me interesaría saber más sobre esto.
Sin embargo, parece que el mecanismo de excepción de Java debe ser mucho más limpio y confiable que cualquier cosa que esté utilizando para la ejecución de contratos en C. Nunca he tenido NINGUNA mala experiencia con el uso de excepciones de Java para validar parámetros. En mi experiencia, son un ENORME ahorro de tiempo sin consecuencias negativas, excepto en situaciones donde el alto rendimiento es crítico.