LaFHIS

Automated testing for APIs backed by graph databases.

When a tool generates tests automatically, the goal is coverage: running through as many lines and branches of the code as possible. The catch is that most branches sit behind a condition, and to get in, the condition has to hold. Random inputs almost never make it. What changes everything is knowing how close each attempt came. If the code asks whether the age is 30 or more and it got 18, it did not fail in some vague way: it was 12 away. With 26 it was 4 away. That number is a distance, and a search can follow it like a gradient: keep the attempts that get closer, drop the ones that drift, until the condition holds and the branch is covered.

function getDiscountPerAge(age) {
  if (age >= 30) {
    return 20;   // we want a test that gets here
  }
  return 0;
}

getDiscountPerAge(18);   // did not get in · 12 away
getDiscountPerAge(26);   // closer · 4 away
getDiscountPerAge(30);   // got in · distance 0

In an API, the condition often does not depend on a number in the request but on what is in the database: GET /users/22 answers 404 or 200 depending on whether user 22 exists. To cover both branches, the tool also has to generate data, and to steer that it needs the same thing: a distance between what the database holds and what the query needs to return something. Tools like EvoMaster do exactly this for REST APIs.

function getUser(id) {
  // this depends on what the database holds right now
  const userInDatabase = database.findUser(id);

  if (userInDatabase == null) {
    return 404;   // branch A: nobody with that id
  }

  return 200;   // branch B: found
}

// empty database:          getUser(22) → 404
// user 22 in the database: getUser(22) → 200

In a graph database like Neo4j, the data are nodes with labels and properties, joined by relationships, and a query describes the shape it wants to find: this node, connected to that one, through this kind of relationship, with certain properties.

Three people, each with a birth year, and one kind of relationship. The query asks for one shape:

  1. MATCH (:Person {name: "Ana"})-[:KNOWS]->(p:Person)Ana knows a Person p
  2. WHERE p.born < 1995and p was born before 1995
  3. RETURN p // returns Luisgive me that p

Ana knows two people. Luis was born in 1990, before 1995, so the query returns him. Sol has the same shape, someone Ana knows, but was born in 2001: the structure matches and the property does not, so she is left out.

Here is a graph and a query that still finds nothing in it. Try bringing them together:

The query

Read it line by line:

  1. MATCH (a:Person {name: "Ana"})-[:KNOWS]->(b:Person)find a node Ana labelled Person, linked by a KNOWS relationship to another node b, also labelled Person
  2. WHERE b.age > 30and that b has to be over 30
  3. RETURN b // returns nothinggive me that b

The graph today

Three nodes. Ana is already in place. What is missing is someone connected to her who is a Person and over 30. The highlighted node is the closest candidate.

Change it

add or remove a KNOWS relationship
give Luis the Person label, or take it away
change Luis's age

How far it is

The distance goes from 0 to 1: 0 means the query matches, 1 means nothing fits. It looks at the closest candidate, Luis, scores each piece it needs and averages them. A missing relationship or label scores a full 1; an age that falls short scores less than 1, the closer the smaller.

  • Ana does not know Luis yet1
  • Luis is a Person0
  • Luis is 26, 5 years short0.83
distance0.61

Each change is a different question. Is a missing connection as far away as a wrong label? What about an age that falls five years short? The search needs a single number to know whether the last change brought it closer or pushed it further, and deciding how those pieces combine is the hard part.

That is my thesis at LaFHIS, the software engineering lab at the University of Buenos Aires: defining that distance, teaching EvoMaster to read Cypher, and using it to generate tests for APIs that run on Neo4j.