Una prueba es mucho más difícil en el mundo de OOP debido a los efectos secundarios, la herencia sin restricciones y nullser miembro de todo tipo. La mayoría de las pruebas se basan en un principio de inducción para demostrar que ha cubierto todas las posibilidades, y las 3 cosas hacen que sea más difícil de probar.
Digamos que estamos implementando árboles binarios que contienen valores enteros (en aras de mantener la sintaxis más simple, no incluiré programación genérica en esto, aunque no cambiaría nada). En ML estándar, definiría eso como esta:
datatype tree = Empty | Node of (tree * int * tree)
Esto introduce un nuevo tipo llamado treecuyos valores pueden venir exactamente en dos variedades (o clases, que no deben confundirse con el concepto OOP de una clase): un Emptyvalor que no contiene información y Nodevalores que llevan una tupla 3 cuya primera y última los elementos son treesy cuyo elemento medio es an int. La aproximación más cercana a esta declaración en OOP se vería así:
public class Tree {
private Tree() {} // Prevent external subclassing
public static final class Empty extends Tree {}
public static final class Node extends Tree {
public final Tree leftChild;
public final int value;
public final Tree rightChild;
public Node(Tree leftChild, int value, Tree rightChild) {
this.leftChild = leftChild;
this.value = value;
this.rightChild = rightChild;
}
}
}
Con la advertencia de que las variables de tipo Árbol nunca pueden ser null.
Ahora escribamos una función para calcular la altura (o profundidad) del árbol, y supongamos que tenemos acceso a una maxfunción que devuelve el mayor de dos números:
fun height(Empty) =
0
| height(Node (leftChild, value, rightChild)) =
1 + max( height(leftChild), height(rightChild) )
Hemos definido la heightfunción por casos: hay una definición para Emptyárboles y una definición para Nodeárboles. El compilador sabe cuántas clases de árboles existen y emitiría una advertencia si no define ambos casos. La expresión Node (leftChild, value, rightChild)en la firma de la función une los valores de la 3-tupla a las variables leftChild, valuey rightChildrespectivamente, de manera que podemos hacer referencia a ellos en la definición de función. Es similar a haber declarado variables locales como esta en un lenguaje OOP:
Tree leftChild = tuple.getFirst();
int value = tuple.getSecond();
Tree rightChild = tuple.getThird();
¿Cómo podemos demostrar que hemos implementado heightcorrectamente? Podemos usar la inducción estructural , que consiste en: 1. Probar que heightes correcto en los casos base de nuestro treetipo ( Empty) 2. Suponiendo que las llamadas recursivas heightsean correctas, demuestre que heightes correcto para los casos no base ) (cuando el árbol es realmente a Node).
Para el paso 1, podemos ver que la función siempre devuelve 0 cuando el argumento es un Emptyárbol. Esto es correcto por definición de la altura de un árbol.
Para el paso 2, la función vuelve 1 + max( height(leftChild), height(rightChild) ). Suponiendo que las llamadas recursivas realmente devuelven la altura de los niños, podemos ver que esto también es correcto.
Y eso completa la prueba. Los pasos 1 y 2 combinados agotan todas las posibilidades. Tenga en cuenta, sin embargo, que no tenemos mutación, ni nulos, y que hay exactamente dos variedades de árboles. Elimine esas tres condiciones y la prueba rápidamente se vuelve más complicada, si no práctica.
EDITAR: Dado que esta respuesta ha llegado a la cima, me gustaría agregar un ejemplo menos trivial de una prueba y cubrir la inducción estructural un poco más a fondo. Arriba probamos que si heightdevuelve , su valor de retorno es correcto. Sin embargo, no hemos demostrado que siempre devuelva un valor. Podemos usar la inducción estructural para probar esto también (o cualquier otra propiedad). Nuevamente, durante el paso 2, se nos permite asumir las propiedades retenidas de las llamadas recursivas siempre que todas las llamadas recursivas operen en un hijo directo del árbol.
Una función puede fallar al devolver un valor en dos situaciones: si arroja una excepción y si se repite para siempre. Primero demostremos que si no se lanzan excepciones, la función termina:
Demuestre que (si no se lanzan excepciones) la función termina para los casos base ( Empty). Como incondicionalmente devolvemos 0, termina.
Probar que la función termina en los casos no base ( Node). Hay tres llamadas a funciones aquí: +, max, y height. Lo sabemos +y maxterminamos porque son parte de la biblioteca estándar del lenguaje y están definidos de esa manera. Como se mencionó anteriormente, se nos permite asumir que la propiedad que intentamos probar es verdadera en las llamadas recursivas siempre que operen en subárboles inmediatos, por lo que las llamadas heightfinalizarán también.
Eso concluye la prueba. Tenga en cuenta que no podrá probar la terminación con una prueba unitaria. Ahora todo lo que queda es mostrar que heightno arroja excepciones.
- Probar que
heightno arroja excepciones en el caso base ( Empty). Devolver 0 no puede arrojar una excepción, así que hemos terminado.
- Probar que
heightno arroja una excepción en el caso no base ( Node). Asuma una vez más que sabemos +y maxno arroje excepciones. Y la inducción estructural nos permite asumir que las llamadas recursivas tampoco se lanzarán (porque operan en los hijos inmediatos del árbol). ¡Pero esperen! Esta función es recursiva, pero no recursiva de cola . ¡Podríamos volar la pila! Nuestro intento de prueba ha descubierto un error. Podemos arreglarlo cambiando heightpara que sea recursivo en la cola .
Espero que esto muestre que las pruebas no tienen que ser aterradoras o complicadas. De hecho, cada vez que escribe un código, ha construido informalmente una prueba en su cabeza (de lo contrario, no estaría convencido de que acaba de implementar la función). Al evitar la mutación nula e innecesaria y la herencia sin restricciones, puede demostrar que su intuición es corregir con bastante facilidad. Estas restricciones no son tan duras como podría pensar:
null es un defecto del idioma y eliminarlo es incondicionalmente bueno.
- La mutación a veces es inevitable y necesaria, pero se necesita con mucha menos frecuencia de lo que parece, especialmente cuando tiene estructuras de datos persistentes.
- En cuanto a tener un número finito de clases (en el sentido funcional) / subclases (en el sentido OOP) frente a un número ilimitado de ellas, ese es un tema demasiado grande para una sola respuesta . Baste decir que hay un intercambio de diseño allí: la probabilidad de corrección frente a la flexibilidad de extensión.