LaFHIS
Testing automático para APIs sobre bases de datos de grafos.
Cuando una herramienta genera tests automáticamente, el objetivo es cubrir código: pasar por la mayor cantidad posible de líneas y de ramas. El problema es que casi todas las ramas están detrás de una condición, y para entrar la condición tiene que cumplirse. Probar valores al azar casi nunca lo logra. Lo que cambia todo es saber qué tan cerca estuvo cada intento. Si el código pregunta si la edad es mayor o igual a 30 y le llegó 18, no falló de cualquier manera: estuvo a 12. Con 26 estuvo a 4. Ese número es una distancia, y una búsqueda puede seguirla como un gradiente: quedarse con los intentos que se acercan, descartar los que se alejan, hasta que la condición se cumple y la rama queda cubierta.
function getDiscountPerAge(age) {
if (age >= 30) {
return 20; // queremos un test que llegue acá
}
return 0;
}
getDiscountPerAge(18); // no entró · a 12
getDiscountPerAge(26); // más cerca · a 4
getDiscountPerAge(30); // entró · distancia 0En una API, la condición muchas veces no depende de un número que llega en el request sino de lo que hay en la base de datos: GET /users/22 responde 404 o 200 según exista o no el usuario 22. Para cubrir las dos ramas, la herramienta también tiene que generar datos, y para guiarse necesita lo mismo: una distancia entre lo que hay en la base y lo que la consulta necesita para devolver algo. Herramientas como EvoMaster hacen exactamente esto para APIs REST.
function getUser(id) {
// esto depende de lo que haya en la base ahora mismo
const userInDatabase = database.findUser(id);
if (userInDatabase == null) {
return 404; // rama A: nadie con ese id
}
return 200; // rama B: encontrado
}
// base vacía: getUser(22) → 404
// usuario 22 en la base: getUser(22) → 200En una base de grafos como Neo4j los datos son nodos con etiquetas y propiedades, unidos por relaciones, y una consulta describe la forma que quiere encontrar: este nodo, conectado con aquel, a través de tal tipo de relación, con ciertas propiedades.
Tres personas, cada una con su año de nacimiento, y un solo tipo de relación. La consulta pide una forma:
MATCH (:Person {name: "Ana"})-[:KNOWS]->(p:Person)Ana conoce a una Person pWHERE p.born < 1995y p nació antes de 1995RETURN p // devuelve a Luisdevolvé ese p
Ana conoce a dos personas. Luis nació en 1990, antes de 1995, así que la consulta lo devuelve. Sol tiene la misma forma, alguien que Ana conoce, pero nació en 2001: la estructura matchea y la propiedad no, así que queda afuera.
Acá hay un grafo y una consulta que todavía no encuentra nada en él. Probá acercarlos:
La consulta
Leída línea por línea:
MATCH (a:Person {name: "Ana"})-[:KNOWS]->(b:Person)buscá un nodo Ana con la etiqueta Person, unido por una relación KNOWS a otro nodo b, también PersonWHERE b.age > 30y ese b tiene que tener más de 30RETURN b // no devuelve nadadevolvé ese b
El grafo hoy
Tres nodos. Ana ya está en su lugar. Lo que falta es alguien conectado a ella que sea Person y tenga más de 30. El nodo resaltado es el candidato más cercano.
Cambialo
Qué tan lejos está
La distancia va de 0 a 1: 0 es que la consulta matchea, 1 es que nada encaja. Mira al candidato más cercano, Luis, puntúa cada pieza que necesita y las promedia. Una relación o una etiqueta que faltan valen 1 entero; una edad que no llega vale menos que 1, y menos cuanto más cerca está.
- Ana todavía no conoce a Luis1
- Luis es Person0
- Luis tiene 26, le faltan 50.83
Cada cambio es una pregunta distinta. ¿Una conexión que falta está tan lejos como una etiqueta equivocada? ¿Y una edad a la que le faltan cinco años? La búsqueda necesita un solo número para saber si el último cambio la acercó o la alejó, y decidir cómo se combinan esas piezas es la parte difícil.
Eso es mi tesis en LaFHIS, el laboratorio de ingeniería de software de la Universidad de Buenos Aires: definir esa distancia, enseñarle a EvoMaster a leer Cypher y usarla para generar tests de APIs que corren sobre Neo4j.